@fayz-ai/plugin-forms 0.15.14 → 0.15.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1678 +0,0 @@
1
- import React3, { createContext, useCallback, useContext } from 'react';
2
- import { Checkbox, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Input, Textarea, Button, PickerList } from '@fayz-ai/ui';
3
- import { describeError, useTranslation, formatCurrency } from '@fayz-ai/core';
4
- import { X, SwitchCamera, RotateCcw, Check, ImageUp, Camera, SkipForward, ShoppingBag, Plus, Trash2, Receipt, Package, ImagePlus, Video, ScanFace, AlertTriangle, Minus } from 'lucide-react';
5
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
- import { createPortal } from 'react-dom';
7
-
8
- // src/components/FormRenderer.tsx
9
-
10
- // src/lib/html-text.ts
11
- function looksLikeHtml(value) {
12
- return /<\/?[a-z][^>]*>/i.test(value);
13
- }
14
- var BLOCKS = "p, div, li, tr, h1, h2, h3, h4, h5, h6, blockquote, section, article";
15
- var NBSP = / /g;
16
- function htmlToPlainText(value) {
17
- if (!value) return "";
18
- if (typeof document === "undefined") {
19
- return value.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6]|blockquote)>/gi, "\n").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/\n{3,}/g, "\n\n").trim();
20
- }
21
- const el = document.createElement("div");
22
- el.innerHTML = value;
23
- for (const br of Array.from(el.querySelectorAll("br"))) {
24
- br.replaceWith("\n");
25
- }
26
- for (const block of Array.from(el.querySelectorAll(BLOCKS))) {
27
- block.append("\n");
28
- }
29
- return (el.textContent ?? "").replace(NBSP, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
30
- }
31
- var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
32
- "script",
33
- "style",
34
- "iframe",
35
- "object",
36
- "embed",
37
- "svg",
38
- "math",
39
- "link",
40
- "meta",
41
- "form",
42
- "input",
43
- "button",
44
- "textarea",
45
- "select",
46
- "template",
47
- "noscript"
48
- ]);
49
- var ALLOWED_TAGS = /* @__PURE__ */ new Set([
50
- "p",
51
- "br",
52
- "hr",
53
- "div",
54
- "span",
55
- "b",
56
- "strong",
57
- "i",
58
- "em",
59
- "u",
60
- "s",
61
- "sub",
62
- "sup",
63
- "mark",
64
- "small",
65
- "ul",
66
- "ol",
67
- "li",
68
- "blockquote",
69
- "pre",
70
- "code",
71
- "h1",
72
- "h2",
73
- "h3",
74
- "h4",
75
- "h5",
76
- "h6",
77
- "table",
78
- "thead",
79
- "tbody",
80
- "tfoot",
81
- "tr",
82
- "td",
83
- "th",
84
- "caption",
85
- "a"
86
- ]);
87
- var ALLOWED_ATTRS = {
88
- a: /* @__PURE__ */ new Set(["href", "title"]),
89
- td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
90
- th: /* @__PURE__ */ new Set(["colspan", "rowspan"])
91
- };
92
- var SAFE_HREF = /^(https?:|mailto:|tel:)/i;
93
- function cleanElement(el) {
94
- for (const child of Array.from(el.children)) {
95
- cleanElement(child);
96
- }
97
- const tag = el.tagName.toLowerCase();
98
- if (DROP_WITH_CONTENT.has(tag)) {
99
- el.remove();
100
- return;
101
- }
102
- for (const attr of Array.from(el.attributes)) {
103
- const name = attr.name.toLowerCase();
104
- const allowed = ALLOWED_ATTRS[tag]?.has(name) ?? false;
105
- if (!allowed) {
106
- el.removeAttribute(attr.name);
107
- continue;
108
- }
109
- if (name === "href" && !SAFE_HREF.test(attr.value.trim())) {
110
- el.removeAttribute(attr.name);
111
- }
112
- }
113
- if (!ALLOWED_TAGS.has(tag)) {
114
- el.replaceWith(...Array.from(el.childNodes));
115
- return;
116
- }
117
- if (tag === "a") {
118
- if (!el.getAttribute("href")) {
119
- el.replaceWith(...Array.from(el.childNodes));
120
- return;
121
- }
122
- el.setAttribute("target", "_blank");
123
- el.setAttribute("rel", "noopener noreferrer nofollow");
124
- }
125
- }
126
- function sanitizeHtml(value) {
127
- if (!value) return "";
128
- if (typeof DOMParser === "undefined") return "";
129
- const doc = new DOMParser().parseFromString(value, "text/html");
130
- for (const child of Array.from(doc.body.children)) {
131
- cleanElement(child);
132
- }
133
- return doc.body.innerHTML;
134
- }
135
- function fieldRichValue(value) {
136
- if (typeof value !== "string" || !value) return { text: "" };
137
- if (!looksLikeHtml(value)) return { text: value };
138
- const html = sanitizeHtml(value);
139
- return html ? { html } : { text: htmlToPlainText(value) };
140
- }
141
-
142
- // src/lib/plain-text.ts
143
- var ENTITIES = {
144
- amp: "&",
145
- lt: "<",
146
- gt: ">",
147
- quot: '"',
148
- apos: "'",
149
- nbsp: " ",
150
- // Os acentos do português, que é o que 23 moldes do Feegow trouxeram. Escrito
151
- // à mão e não resolvido pelo DOM porque esta função também roda em teste e
152
- // fora do navegador — e um `&ccedil;` visível na anamnese é o defeito que
153
- // ela veio consertar.
154
- ...Object.fromEntries(
155
- "agrave \xE0 aacute \xE1 acirc \xE2 atilde \xE3 auml \xE4 ccedil \xE7 egrave \xE8 eacute \xE9 ecirc \xEA euml \xEB igrave \xEC iacute \xED icirc \xEE iuml \xEF ntilde \xF1 ograve \xF2 oacute \xF3 ocirc \xF4 otilde \xF5 ouml \xF6 ugrave \xF9 uacute \xFA ucirc \xFB uuml \xFC".split(" ").reduce((acc, part, i, all) => {
156
- if (i % 2 === 0) acc.push([part, all[i + 1]]);
157
- return acc;
158
- }, []).flatMap(([name, char]) => [
159
- [name, char],
160
- [name.charAt(0).toUpperCase() + name.slice(1), char.toUpperCase()]
161
- ])
162
- )
163
- };
164
- function decodeEntities(value) {
165
- return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, body) => {
166
- if (body[0] === "#") {
167
- const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
168
- return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
169
- }
170
- const named = ENTITIES[body] ?? ENTITIES[body.toLowerCase()];
171
- if (named) return named;
172
- if (typeof document === "undefined") return whole;
173
- const el = document.createElement("textarea");
174
- el.innerHTML = whole;
175
- return el.value;
176
- });
177
- }
178
- function htmlToPlainText2(value) {
179
- if (!value || !/[<&]/.test(value)) return value;
180
- const text = decodeEntities(
181
- value.replace(/<\s*br\s*\/?\s*>/gi, "\n").replace(/<\s*\/\s*(p|div|li|h[1-6])\s*>/gi, "\n").replace(/<[^>]*>/g, "")
182
- );
183
- return text.split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n") || void 0;
184
- }
185
- var Ctx = createContext(null);
186
- function CustomFormsProvider({
187
- value,
188
- children
189
- }) {
190
- return React3.createElement(Ctx.Provider, { value }, children);
191
- }
192
- function useOptionalCustomFormsContext() {
193
- return useContext(Ctx);
194
- }
195
- function useMediaUrl(path) {
196
- const ctx = useOptionalCustomFormsContext();
197
- const [url, setUrl] = React3.useState(null);
198
- React3.useEffect(() => {
199
- if (!path) {
200
- setUrl(null);
201
- return;
202
- }
203
- if (/^(data:|blob:|https?:)/.test(path)) {
204
- setUrl(path);
205
- return;
206
- }
207
- let alive = true;
208
- Promise.resolve(ctx?.provider.mediaUrl?.(path) ?? null).then((u) => {
209
- if (alive) setUrl(u);
210
- }).catch(() => {
211
- if (alive) setUrl(null);
212
- });
213
- return () => {
214
- alive = false;
215
- };
216
- }, [path, ctx]);
217
- return url;
218
- }
219
- function useMediaUpload() {
220
- const ctx = useOptionalCustomFormsContext();
221
- const upload = ctx?.provider.uploadMedia;
222
- return React3.useMemo(() => upload ? (blob, folder, name) => upload.call(ctx.provider, blob, folder, name) : null, [upload, ctx]);
223
- }
224
- async function compressImage(file, maxSide = 1600, quality = 0.85) {
225
- if (!file.type.startsWith("image/") || file.type === "image/gif") return file;
226
- try {
227
- const bitmap = await createImageBitmap(file);
228
- const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
229
- if (scale === 1 && file.size < 9e5) return file;
230
- const canvas = document.createElement("canvas");
231
- canvas.width = Math.round(bitmap.width * scale);
232
- canvas.height = Math.round(bitmap.height * scale);
233
- canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
234
- const out = await new Promise((r) => canvas.toBlob(r, "image/jpeg", quality));
235
- return out ?? file;
236
- } catch {
237
- return file;
238
- }
239
- }
240
- function isMedia(v) {
241
- return !!v && typeof v === "object" && typeof v.path === "string";
242
- }
243
- function MediaImage({ value, alt, className, style }) {
244
- const path = typeof value === "string" ? value : value?.path;
245
- const url = useMediaUrl(path);
246
- if (!url) return /* @__PURE__ */ jsx("div", { className: `animate-pulse bg-muted ${className ?? ""}`, style, "aria-busy": "true" });
247
- return /* @__PURE__ */ jsx("img", { src: url, alt, className, style });
248
- }
249
- var brl = (n) => formatCurrency(n);
250
- var stencilOf = (f) => f.config?.stencil ?? {};
251
- var galleryOf = (f) => f.config?.gallery ?? {};
252
- var alertOf = (f) => {
253
- const a = f.config?.alert;
254
- return a?.trigger && a.trigger !== "never" ? a : null;
255
- };
256
- var aspectRatioOf = (c) => c.aspect === "1:1" ? 1 : c.aspect === "4:3" ? 4 / 3 : 3 / 4;
257
- function yesNoAnswer(v) {
258
- const a = typeof v === "string" ? v : v?.answer;
259
- return a === "sim" || a === "nao" ? a : "";
260
- }
261
- function answerAlerts(f, v) {
262
- const a = alertOf(f);
263
- return a && yesNoAnswer(v) === a.trigger ? a : null;
264
- }
265
- var ALERT_TONE = {
266
- danger: "border-destructive/40 bg-destructive/5 text-destructive",
267
- warning: "border-warning/40 bg-warning-soft text-warning-soft-foreground",
268
- info: "border-info/40 bg-info-soft text-info-soft-foreground"
269
- };
270
- function AnswerAlertNote({ alert }) {
271
- return /* @__PURE__ */ jsxs("p", { className: `mt-1.5 inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] font-medium ${ALERT_TONE[alert.severity ?? "danger"]}`, children: [
272
- /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }),
273
- alert.message || "Gera alerta na ficha do cliente"
274
- ] });
275
- }
276
- function YesNoButtons({ id, value, onChange, disabled }) {
277
- return /* @__PURE__ */ jsx("div", { role: "radiogroup", className: "inline-flex rounded-lg border bg-muted/40 p-0.5", children: ["sim", "nao"].map((opt) => /* @__PURE__ */ jsx(
278
- Button,
279
- {
280
- variant: "bare",
281
- size: "bare",
282
- id: `${id}-${opt}`,
283
- type: "button",
284
- role: "radio",
285
- "aria-checked": value === opt,
286
- disabled,
287
- onClick: () => onChange(opt),
288
- className: `min-w-14 rounded-md px-3 py-1 text-xs font-medium transition-colors ${value === opt ? "bg-primary text-primary-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
289
- children: opt === "sim" ? "Sim" : "N\xE3o"
290
- },
291
- opt
292
- )) });
293
- }
294
- function YesNoField({ field, value, onChange }) {
295
- const alert = answerAlerts(field, value);
296
- return /* @__PURE__ */ jsxs("div", { children: [
297
- /* @__PURE__ */ jsx(YesNoButtons, { id: field.id, value: yesNoAnswer(value), onChange }),
298
- alert && /* @__PURE__ */ jsx(AnswerAlertNote, { alert })
299
- ] });
300
- }
301
- function YesNoTextField({ field, value, onChange }) {
302
- const v = value && typeof value === "object" ? value : { answer: yesNoAnswer(value) };
303
- const showText = v.answer === "sim" || !!v.text?.trim();
304
- const alert = answerAlerts(field, v);
305
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
306
- /* @__PURE__ */ jsx(YesNoButtons, { id: field.id, value: v.answer, onChange: (a) => onChange({ ...v, answer: a }) }),
307
- alert && /* @__PURE__ */ jsx(AnswerAlertNote, { alert }),
308
- showText && /* @__PURE__ */ jsx(
309
- "textarea",
310
- {
311
- value: v.text ?? "",
312
- onChange: (e) => onChange({ ...v, text: e.target.value }),
313
- placeholder: field.placeholder || "Descreva\u2026",
314
- rows: 2,
315
- className: "flex w-full rounded-input border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
316
- }
317
- )
318
- ] });
319
- }
320
- function StencilPhotoField({ field, value, onChange, onOpenCamera, uploading }) {
321
- const cfg = stencilOf(field);
322
- const ratio = aspectRatioOf(cfg);
323
- const overlay = useMediaUrl(cfg.overlayPath);
324
- if (uploading) {
325
- return /* @__PURE__ */ jsx("div", { className: "w-full max-w-[320px] animate-pulse rounded-lg bg-muted", style: { aspectRatio: String(ratio) }, "aria-busy": "true" });
326
- }
327
- if (isMedia(value)) {
328
- return /* @__PURE__ */ jsxs("div", { className: "max-w-[320px] space-y-2", children: [
329
- /* @__PURE__ */ jsx(MediaImage, { value, alt: field.label, className: "w-full rounded-lg border object-cover", style: { aspectRatio: String(ratio) } }),
330
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
331
- /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: "outline", className: "h-8 gap-1 text-xs", onClick: onOpenCamera, children: [
332
- /* @__PURE__ */ jsx(Camera, { className: "h-3 w-3" }),
333
- " Refazer"
334
- ] }),
335
- /* @__PURE__ */ jsxs(
336
- Button,
337
- {
338
- type: "button",
339
- size: "sm",
340
- variant: "outline",
341
- className: "h-8 gap-1 text-xs text-destructive hover:text-destructive",
342
- onClick: () => onChange(null),
343
- children: [
344
- /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }),
345
- " Excluir"
346
- ]
347
- }
348
- )
349
- ] }),
350
- cfg.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: cfg.description })
351
- ] });
352
- }
353
- return /* @__PURE__ */ jsxs(
354
- Button,
355
- {
356
- variant: "bare",
357
- size: "bare",
358
- type: "button",
359
- onClick: onOpenCamera,
360
- className: "font-normal whitespace-normal flex w-full max-w-[320px] flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-input bg-muted/20 p-3 text-muted-foreground transition-colors hover:border-primary hover:bg-muted/40 hover:text-primary",
361
- style: { aspectRatio: String(ratio) },
362
- children: [
363
- overlay ? /* @__PURE__ */ jsx("img", { src: overlay, alt: "", className: "max-h-[70%] max-w-[70%] object-contain opacity-70" }) : /* @__PURE__ */ jsx(ScanFace, { className: "h-8 w-8" }),
364
- /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-center text-[11px] font-medium leading-tight", children: cfg.description || "Tocar para abrir c\xE2mera" })
365
- ]
366
- }
367
- );
368
- }
369
- function GalleryThumb({ item, onRemove }) {
370
- const url = useMediaUrl(item.path);
371
- return /* @__PURE__ */ jsxs("div", { className: "group relative aspect-square overflow-hidden rounded-lg border bg-muted", children: [
372
- !url ? /* @__PURE__ */ jsx("div", { className: "h-full w-full animate-pulse bg-muted" }) : item.type === "video" ? /* @__PURE__ */ jsx("video", { src: url, className: "h-full w-full object-cover", controls: true, preload: "metadata" }) : /* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsx("img", { src: url, alt: item.name ?? "", className: "h-full w-full object-cover" }) }),
373
- onRemove && /* @__PURE__ */ jsx(
374
- Button,
375
- {
376
- variant: "bare",
377
- size: "bare",
378
- type: "button",
379
- onClick: onRemove,
380
- "aria-label": "Remover",
381
- className: "font-normal absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-background/90 text-muted-foreground shadow hover:text-destructive",
382
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
383
- }
384
- )
385
- ] });
386
- }
387
- function GalleryView({ items }) {
388
- return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2 sm:grid-cols-4", children: items.map((it) => /* @__PURE__ */ jsx(GalleryThumb, { item: it }, it.path)) });
389
- }
390
- function GalleryField({ field, value, onChange }) {
391
- const cfg = galleryOf(field);
392
- const allowPhoto = cfg.allowPhoto ?? true;
393
- const allowVideo = cfg.allowVideo ?? true;
394
- const upload = useMediaUpload();
395
- const photoRef = React3.useRef(null);
396
- const videoRef = React3.useRef(null);
397
- const [pending, setPending] = React3.useState(0);
398
- const [erro, setErro] = React3.useState(null);
399
- const items = Array.isArray(value) ? value.filter(isMedia) : [];
400
- const latest = React3.useRef(items);
401
- latest.current = items;
402
- async function add(files, kind) {
403
- if (!files?.length || !upload) return;
404
- setErro(null);
405
- for (const file of Array.from(files)) {
406
- setPending((n) => n + 1);
407
- try {
408
- const blob = kind === "photo" ? await compressImage(file) : file;
409
- const { path } = await upload(blob, `uploads/${field.id}`, kind === "photo" ? "foto.jpg" : file.name);
410
- latest.current = [...latest.current, { path, type: kind, name: file.name }];
411
- onChange(latest.current);
412
- } catch (e) {
413
- setErro(`N\xE3o foi poss\xEDvel enviar ${file.name}: ${describeError(e, "erro")}`);
414
- } finally {
415
- setPending((n) => n - 1);
416
- }
417
- }
418
- }
419
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
420
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
421
- allowPhoto && /* @__PURE__ */ jsxs(
422
- Button,
423
- {
424
- type: "button",
425
- variant: "outline",
426
- size: "sm",
427
- className: "h-8 gap-1.5 text-xs",
428
- disabled: !upload,
429
- onClick: () => photoRef.current?.click(),
430
- children: [
431
- /* @__PURE__ */ jsx(ImagePlus, { className: "h-3.5 w-3.5" }),
432
- " Adicionar foto"
433
- ]
434
- }
435
- ),
436
- allowVideo && /* @__PURE__ */ jsxs(
437
- Button,
438
- {
439
- type: "button",
440
- variant: "outline",
441
- size: "sm",
442
- className: "h-8 gap-1.5 text-xs",
443
- disabled: !upload,
444
- onClick: () => videoRef.current?.click(),
445
- children: [
446
- /* @__PURE__ */ jsx(Video, { className: "h-3.5 w-3.5" }),
447
- " Adicionar v\xEDdeo"
448
- ]
449
- }
450
- )
451
- ] }),
452
- /* @__PURE__ */ jsx(
453
- "input",
454
- {
455
- ref: photoRef,
456
- type: "file",
457
- accept: "image/*",
458
- multiple: true,
459
- className: "hidden",
460
- onChange: (e) => {
461
- add(e.target.files, "photo");
462
- e.target.value = "";
463
- }
464
- }
465
- ),
466
- /* @__PURE__ */ jsx(
467
- "input",
468
- {
469
- ref: videoRef,
470
- type: "file",
471
- accept: "video/*",
472
- multiple: true,
473
- className: "hidden",
474
- onChange: (e) => {
475
- add(e.target.files, "video");
476
- e.target.value = "";
477
- }
478
- }
479
- ),
480
- (items.length > 0 || pending > 0) && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-2 sm:grid-cols-4", children: [
481
- items.map((it) => /* @__PURE__ */ jsx(GalleryThumb, { item: it, onRemove: () => onChange(items.filter((x) => x.path !== it.path)) }, it.path)),
482
- Array.from({ length: pending }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "aspect-square animate-pulse rounded-lg bg-muted", "aria-busy": "true" }, `p${i}`))
483
- ] }),
484
- erro && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: erro })
485
- ] });
486
- }
487
- function Stepper({ value, onChange }) {
488
- return /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center rounded-lg border", children: [
489
- /* @__PURE__ */ jsx(
490
- Button,
491
- {
492
- variant: "bare",
493
- size: "bare",
494
- type: "button",
495
- "aria-label": "Diminuir",
496
- disabled: value <= 1,
497
- onClick: () => onChange(value - 1),
498
- className: "font-normal flex h-7 w-7 items-center justify-center text-muted-foreground hover:text-foreground disabled:opacity-30",
499
- children: /* @__PURE__ */ jsx(Minus, { className: "h-3 w-3" })
500
- }
501
- ),
502
- /* @__PURE__ */ jsx("span", { className: "w-7 text-center text-sm tabular-nums", children: value }),
503
- /* @__PURE__ */ jsx(
504
- Button,
505
- {
506
- variant: "bare",
507
- size: "bare",
508
- type: "button",
509
- "aria-label": "Aumentar",
510
- onClick: () => onChange(value + 1),
511
- className: "font-normal flex h-7 w-7 items-center justify-center text-muted-foreground hover:text-foreground",
512
- children: /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" })
513
- }
514
- )
515
- ] });
516
- }
517
- function RecommendationField({ value, onChange, shortcuts = [] }) {
518
- const ctx = useOptionalCustomFormsContext();
519
- const items = Array.isArray(value) ? value : [];
520
- const [picking, setPicking] = React3.useState(null);
521
- const [catalog, setCatalog] = React3.useState(null);
522
- const [erro, setErro] = React3.useState(null);
523
- React3.useEffect(() => {
524
- if (!picking || catalog) return;
525
- let alive = true;
526
- Promise.resolve(ctx?.provider.searchCatalog?.("") ?? []).then((r) => {
527
- if (alive) setCatalog(r);
528
- }).catch((e) => {
529
- if (alive) {
530
- setCatalog([]);
531
- setErro(describeError(e, "N\xE3o foi poss\xEDvel ler o cat\xE1logo."));
532
- }
533
- });
534
- return () => {
535
- alive = false;
536
- };
537
- }, [picking, catalog, ctx]);
538
- const has = (serviceId) => items.some((i) => i.serviceId === serviceId && !i.packageId);
539
- const groups = React3.useMemo(() => {
540
- const opts = (catalog ?? []).filter((c) => c.kind === picking);
541
- if (picking === "package") {
542
- return [{ id: "pk", options: opts.map((c) => ({ id: c.id, label: c.name, hint: brl(c.price) })) }];
543
- }
544
- const by = /* @__PURE__ */ new Map();
545
- for (const c of opts) {
546
- const g = c.group ?? "Outros";
547
- if (!by.has(g)) by.set(g, []);
548
- by.get(g).push(c);
549
- }
550
- return [...by.entries()].sort(([a], [b]) => a.localeCompare(b, "pt-BR")).map(([g, cs]) => ({
551
- id: g,
552
- label: `${g} (${cs.length})`,
553
- options: cs.map((c) => ({ id: c.id, label: c.name, hint: brl(c.price), disabled: has(c.id) }))
554
- }));
555
- }, [catalog, picking, items]);
556
- async function pick(id) {
557
- const c = catalog?.find((x) => x.id === id);
558
- if (!c) return;
559
- if (c.kind === "service") {
560
- if (!has(c.id)) onChange([...items, { serviceId: c.id, name: c.name, unitPrice: c.price, quantity: 1 }]);
561
- setPicking(null);
562
- return;
563
- }
564
- try {
565
- const lines = await ctx?.provider.getPackageServices?.(c.id) ?? [];
566
- if (!lines.length) {
567
- setErro("Este pacote n\xE3o tem servi\xE7os cadastrados.");
568
- return;
569
- }
570
- onChange([...items.filter((i) => i.packageId !== c.id), ...lines.map((l) => ({ ...l, packageId: c.id, packageName: c.name }))]);
571
- setPicking(null);
572
- } catch (e) {
573
- setErro(describeError(e, "N\xE3o foi poss\xEDvel abrir o pacote."));
574
- }
575
- }
576
- const freeShortcuts = shortcuts.filter((s) => !has(s.serviceId));
577
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2.5", children: [
578
- freeShortcuts.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-lg border border-warning/40 bg-warning-soft/50 p-2.5", children: [
579
- /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 text-xs font-semibold text-warning-soft-foreground", children: [
580
- /* @__PURE__ */ jsx(ShoppingBag, { className: "h-3.5 w-3.5" }),
581
- " Compras antecipadas deste cliente",
582
- /* @__PURE__ */ jsx("span", { className: "text-[11px] font-normal opacity-80", children: "\xB7 toque para indicar o que vai executar" })
583
- ] }),
584
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: freeShortcuts.map((s) => /* @__PURE__ */ jsxs(
585
- Button,
586
- {
587
- variant: "bare",
588
- size: "bare",
589
- type: "button",
590
- onClick: () => onChange([...items, { serviceId: s.serviceId, name: s.name, unitPrice: s.unitPrice, quantity: 1 }]),
591
- className: "font-normal inline-flex items-center gap-1.5 rounded-full border bg-background px-2.5 py-1 text-xs hover:bg-muted",
592
- children: [
593
- /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }),
594
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: s.name }),
595
- s.hint && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-success", children: s.hint })
596
- ]
597
- },
598
- s.serviceId
599
- )) })
600
- ] }),
601
- items.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg border", children: [
602
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[1fr_auto_auto] gap-2 border-b bg-muted/50 px-3 py-2 text-xs font-semibold text-muted-foreground", children: [
603
- /* @__PURE__ */ jsx("span", { children: "Procedimento indicado" }),
604
- /* @__PURE__ */ jsx("span", { className: "w-[84px] text-center", children: "Qtd." }),
605
- /* @__PURE__ */ jsx("span", { className: "w-6" })
606
- ] }),
607
- items.map((it, idx) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[1fr_auto_auto] items-center gap-2 border-b px-3 py-2 last:border-0", children: [
608
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
609
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm", children: it.name }),
610
- /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-muted-foreground", children: [
611
- brl(it.unitPrice * it.quantity),
612
- it.packageName ? ` \xB7 pacote ${it.packageName}` : ""
613
- ] })
614
- ] }),
615
- /* @__PURE__ */ jsx(Stepper, { value: it.quantity, onChange: (q) => onChange(items.map((x, i) => i === idx ? { ...x, quantity: q } : x)) }),
616
- /* @__PURE__ */ jsx(
617
- Button,
618
- {
619
- variant: "bare",
620
- size: "bare",
621
- type: "button",
622
- "aria-label": `Remover ${it.name}`,
623
- onClick: () => onChange(items.filter((_, i) => i !== idx)),
624
- className: "font-normal flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-destructive",
625
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
626
- }
627
- )
628
- ] }, `${it.packageId ?? ""}:${it.serviceId}`))
629
- ] }) : !picking && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg border-2 border-dashed border-input p-3 text-sm text-muted-foreground", children: [
630
- /* @__PURE__ */ jsx(Receipt, { className: "h-5 w-5 shrink-0" }),
631
- 'Indique procedimentos para o cliente \u2014 eles v\xE3o para o CRM e para "O que o cliente deseja contratar?".'
632
- ] }),
633
- picking ? /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg border", children: [
634
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [
635
- /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold", children: picking === "package" ? "Indicar pacote" : "Adicionar servi\xE7os" }),
636
- /* @__PURE__ */ jsx(
637
- Button,
638
- {
639
- variant: "bare",
640
- size: "bare",
641
- type: "button",
642
- onClick: () => setPicking(null),
643
- "aria-label": "Fechar lista",
644
- className: "font-normal text-muted-foreground hover:text-foreground",
645
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
646
- }
647
- )
648
- ] }),
649
- catalog === null ? /* @__PURE__ */ jsx("div", { className: "space-y-1.5 p-3", "aria-busy": "true", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsx("div", { className: "h-8 animate-pulse rounded-md bg-muted" }, i)) }) : /* @__PURE__ */ jsx(
650
- PickerList,
651
- {
652
- groups,
653
- onSelect: pick,
654
- searchThreshold: 0,
655
- listClassName: "max-h-72",
656
- searchPlaceholder: picking === "package" ? "Buscar pacote\u2026" : "Buscar servi\xE7o\u2026",
657
- emptyLabel: picking === "package" ? "Nenhum pacote ativo" : "Nenhum servi\xE7o encontrado"
658
- }
659
- )
660
- ] }) : /* @__PURE__ */ jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [
661
- /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", className: "justify-center gap-2", onClick: () => setPicking("service"), children: [
662
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
663
- " Adicionar indica\xE7\xE3o"
664
- ] }),
665
- /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", className: "justify-center gap-2", onClick: () => setPicking("package"), children: [
666
- /* @__PURE__ */ jsx(Package, { className: "h-4 w-4" }),
667
- " Indicar pacote"
668
- ] })
669
- ] }),
670
- erro && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: erro })
671
- ] });
672
- }
673
- function RecommendationView({ items }) {
674
- if (!items.length) return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/50", children: "\u2014" });
675
- return /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: items.map((it) => /* @__PURE__ */ jsxs("li", { className: "flex items-baseline justify-between gap-3 text-sm", children: [
676
- /* @__PURE__ */ jsxs("span", { children: [
677
- it.quantity > 1 ? `${it.quantity}\xD7 ` : "",
678
- it.name
679
- ] }),
680
- /* @__PURE__ */ jsx("span", { className: "tabular-nums text-muted-foreground", children: brl(it.unitPrice * it.quantity) })
681
- ] }, `${it.packageId ?? ""}:${it.serviceId}`)) });
682
- }
683
- function SignatureField({ field, value, onChange }) {
684
- const canvasRef = React3.useRef(null);
685
- const drawing = React3.useRef(false);
686
- const dirty = React3.useRef(false);
687
- const upload = useMediaUpload();
688
- const [saving, setSaving] = React3.useState(false);
689
- const [erro, setErro] = React3.useState(null);
690
- React3.useEffect(() => {
691
- const c = canvasRef.current;
692
- if (!c) return;
693
- const ratio = window.devicePixelRatio || 1;
694
- c.width = c.offsetWidth * ratio;
695
- c.height = c.offsetHeight * ratio;
696
- const ctx = c.getContext("2d");
697
- ctx.scale(ratio, ratio);
698
- ctx.lineWidth = 2;
699
- ctx.lineCap = "round";
700
- ctx.strokeStyle = "#111827";
701
- }, [isMedia(value)]);
702
- if (isMedia(value)) {
703
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
704
- /* @__PURE__ */ jsx(MediaImage, { value, alt: "Assinatura", className: "max-h-32 rounded-md border bg-white" }),
705
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onChange(null), children: "Assinar novamente" })
706
- ] });
707
- }
708
- const pos = (e) => {
709
- const r = e.currentTarget.getBoundingClientRect();
710
- return [e.clientX - r.left, e.clientY - r.top];
711
- };
712
- async function confirm() {
713
- const c = canvasRef.current;
714
- if (!c || !dirty.current || !upload) return;
715
- setSaving(true);
716
- setErro(null);
717
- try {
718
- const blob = await new Promise((r) => c.toBlob(r, "image/png"));
719
- if (!blob) return;
720
- const { path } = await upload(blob, `uploads/${field.id}`, "assinatura.png");
721
- onChange({ path, type: "signature" });
722
- } catch (e) {
723
- setErro(describeError(e, "N\xE3o foi poss\xEDvel guardar a assinatura."));
724
- } finally {
725
- setSaving(false);
726
- }
727
- }
728
- function clear() {
729
- const c = canvasRef.current;
730
- if (!c) return;
731
- c.getContext("2d").clearRect(0, 0, c.width, c.height);
732
- dirty.current = false;
733
- }
734
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
735
- /* @__PURE__ */ jsx(
736
- "canvas",
737
- {
738
- ref: canvasRef,
739
- className: "h-36 w-full touch-none rounded-md border bg-white",
740
- onPointerDown: (e) => {
741
- drawing.current = true;
742
- e.currentTarget.setPointerCapture(e.pointerId);
743
- const ctx = e.currentTarget.getContext("2d");
744
- const [x, y] = pos(e);
745
- ctx.beginPath();
746
- ctx.moveTo(x, y);
747
- },
748
- onPointerMove: (e) => {
749
- if (!drawing.current) return;
750
- const ctx = e.currentTarget.getContext("2d");
751
- const [x, y] = pos(e);
752
- ctx.lineTo(x, y);
753
- ctx.stroke();
754
- dirty.current = true;
755
- },
756
- onPointerUp: () => {
757
- drawing.current = false;
758
- }
759
- }
760
- ),
761
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
762
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: clear, children: "Limpar" }),
763
- /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: confirm, disabled: saving || !upload, children: saving ? "Guardando\u2026" : "Confirmar assinatura" })
764
- ] }),
765
- erro && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: erro })
766
- ] });
767
- }
768
- var ratioOf = (a) => a === "1:1" ? 1 : a === "4:3" ? 4 / 3 : 3 / 4;
769
- function StencilCamera({
770
- title,
771
- overlayUrl,
772
- opacity,
773
- aspect,
774
- camera,
775
- burnOverlay,
776
- onCapture,
777
- onSkip,
778
- onClose
779
- }) {
780
- const videoRef = React3.useRef(null);
781
- const streamRef = React3.useRef(null);
782
- const fileRef = React3.useRef(null);
783
- const [facing, setFacing] = React3.useState(camera);
784
- const [preview, setPreview] = React3.useState(null);
785
- const [starting, setStarting] = React3.useState(true);
786
- const [noCamera, setNoCamera] = React3.useState(false);
787
- const [retake, setRetake] = React3.useState(0);
788
- const ratio = ratioOf(aspect);
789
- const stop = () => {
790
- streamRef.current?.getTracks().forEach((t) => t.stop());
791
- streamRef.current = null;
792
- };
793
- React3.useEffect(() => {
794
- if (preview) return;
795
- let alive = true;
796
- (async () => {
797
- setStarting(true);
798
- try {
799
- if (!navigator.mediaDevices?.getUserMedia) throw new Error("unsupported");
800
- stop();
801
- const stream = await navigator.mediaDevices.getUserMedia({
802
- video: { facingMode: { ideal: facing }, width: { ideal: 1920 }, height: { ideal: 1920 } },
803
- audio: false
804
- });
805
- if (!alive) {
806
- stream.getTracks().forEach((t) => t.stop());
807
- return;
808
- }
809
- streamRef.current = stream;
810
- if (videoRef.current) {
811
- videoRef.current.srcObject = stream;
812
- await videoRef.current.play().catch(() => {
813
- });
814
- }
815
- setNoCamera(false);
816
- } catch {
817
- if (alive) setNoCamera(true);
818
- } finally {
819
- if (alive) setStarting(false);
820
- }
821
- })();
822
- return () => {
823
- alive = false;
824
- };
825
- }, [facing, retake]);
826
- React3.useEffect(() => () => stop(), []);
827
- React3.useEffect(() => {
828
- const onKey = (e) => {
829
- if (e.key !== "Escape") return;
830
- e.stopPropagation();
831
- e.preventDefault();
832
- onClose();
833
- };
834
- window.addEventListener("keydown", onKey, true);
835
- return () => window.removeEventListener("keydown", onKey, true);
836
- }, [onClose]);
837
- async function drawOverlay(ctx, w, h) {
838
- if (!burnOverlay || !overlayUrl) return;
839
- await new Promise((resolve) => {
840
- const timer = setTimeout(resolve, 2500);
841
- const img = new Image();
842
- img.crossOrigin = "anonymous";
843
- img.onload = () => {
844
- try {
845
- const s = Math.min(w / img.width, h / img.height);
846
- ctx.globalAlpha = opacity;
847
- ctx.drawImage(img, (w - img.width * s) / 2, (h - img.height * s) / 2, img.width * s, img.height * s);
848
- ctx.globalAlpha = 1;
849
- } catch {
850
- }
851
- clearTimeout(timer);
852
- resolve();
853
- };
854
- img.onerror = () => {
855
- clearTimeout(timer);
856
- resolve();
857
- };
858
- img.src = overlayUrl;
859
- });
860
- }
861
- async function capture() {
862
- const video = videoRef.current;
863
- if (!video || !video.videoWidth) return;
864
- const vw = video.videoWidth, vh = video.videoHeight;
865
- let cw = vw, ch = vh;
866
- if (vw / vh > ratio) cw = vh * ratio;
867
- else ch = vw / ratio;
868
- const canvas = document.createElement("canvas");
869
- canvas.width = Math.round(cw);
870
- canvas.height = Math.round(ch);
871
- const ctx = canvas.getContext("2d");
872
- if (facing === "user") {
873
- ctx.translate(canvas.width, 0);
874
- ctx.scale(-1, 1);
875
- }
876
- ctx.drawImage(video, (vw - cw) / 2, (vh - ch) / 2, cw, ch, 0, 0, canvas.width, canvas.height);
877
- ctx.setTransform(1, 0, 0, 1, 0, 0);
878
- await drawOverlay(ctx, canvas.width, canvas.height);
879
- const blob = await new Promise((r) => {
880
- try {
881
- canvas.toBlob(r, "image/jpeg", 0.9);
882
- } catch {
883
- r(null);
884
- }
885
- });
886
- if (!blob) return;
887
- stop();
888
- setPreview({ blob, url: URL.createObjectURL(blob) });
889
- }
890
- function pickFile(file) {
891
- if (!file) return;
892
- setPreview({ blob: file, url: URL.createObjectURL(file) });
893
- }
894
- return createPortal(
895
- /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-[2147483647] flex select-none flex-col bg-black text-white", role: "dialog", "aria-label": title, children: [
896
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3", children: [
897
- /* @__PURE__ */ jsx(
898
- Button,
899
- {
900
- variant: "bare",
901
- size: "bare",
902
- type: "button",
903
- onClick: onClose,
904
- "aria-label": "Fechar",
905
- className: "font-normal flex h-10 w-10 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
906
- children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
907
- }
908
- ),
909
- /* @__PURE__ */ jsx("div", { className: "truncate px-2 text-sm font-medium", children: title }),
910
- !preview && !noCamera ? /* @__PURE__ */ jsx(
911
- Button,
912
- {
913
- variant: "bare",
914
- size: "bare",
915
- type: "button",
916
- onClick: () => setFacing((f) => f === "user" ? "environment" : "user"),
917
- "aria-label": "Trocar c\xE2mera",
918
- className: "font-normal flex h-10 w-10 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
919
- children: /* @__PURE__ */ jsx(SwitchCamera, { className: "h-5 w-5" })
920
- }
921
- ) : /* @__PURE__ */ jsx("div", { className: "w-10" })
922
- ] }),
923
- /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "relative h-full max-h-full max-w-full bg-black", style: { aspectRatio: String(ratio) }, children: preview ? /* @__PURE__ */ jsx("img", { src: preview.url, alt: "", className: "h-full w-full object-contain" }) : noCamera ? /* @__PURE__ */ jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-3 px-6 text-center text-sm text-white/80", children: [
924
- overlayUrl && /* @__PURE__ */ jsx("img", { src: overlayUrl, alt: "", className: "max-h-[50%] opacity-60" }),
925
- /* @__PURE__ */ jsx("p", { children: "N\xE3o foi poss\xEDvel abrir a c\xE2mera neste aparelho. Escolha uma foto do arquivo." })
926
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
927
- /* @__PURE__ */ jsx(
928
- "video",
929
- {
930
- ref: videoRef,
931
- playsInline: true,
932
- muted: true,
933
- className: "h-full w-full object-cover",
934
- style: { transform: facing === "user" ? "scaleX(-1)" : void 0 }
935
- }
936
- ),
937
- overlayUrl && /* @__PURE__ */ jsx(
938
- "img",
939
- {
940
- src: overlayUrl,
941
- alt: "",
942
- className: "pointer-events-none absolute inset-0 h-full w-full object-contain",
943
- style: { opacity }
944
- }
945
- ),
946
- starting && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-white/80", children: "Iniciando c\xE2mera\u2026" })
947
- ] }) }) }),
948
- /* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => pickFile(e.target.files?.[0]) }),
949
- /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center gap-6 p-4 pb-[calc(2rem+env(safe-area-inset-bottom))]", children: [
950
- preview ? /* @__PURE__ */ jsxs(Fragment, { children: [
951
- /* @__PURE__ */ jsxs(
952
- Button,
953
- {
954
- type: "button",
955
- variant: "secondary",
956
- size: "lg",
957
- className: "gap-2",
958
- onClick: () => {
959
- URL.revokeObjectURL(preview.url);
960
- setPreview(null);
961
- setRetake((n) => n + 1);
962
- },
963
- children: [
964
- /* @__PURE__ */ jsx(RotateCcw, { className: "h-4 w-4" }),
965
- " Tirar novamente"
966
- ]
967
- }
968
- ),
969
- /* @__PURE__ */ jsxs(Button, { type: "button", size: "lg", className: "gap-2", onClick: () => onCapture(preview.blob), children: [
970
- /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
971
- " Usar foto"
972
- ] })
973
- ] }) : noCamera ? /* @__PURE__ */ jsxs(Button, { type: "button", size: "lg", className: "gap-2", onClick: () => fileRef.current?.click(), children: [
974
- /* @__PURE__ */ jsx(ImageUp, { className: "h-4 w-4" }),
975
- " Escolher foto"
976
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
977
- /* @__PURE__ */ jsx(
978
- Button,
979
- {
980
- variant: "bare",
981
- size: "bare",
982
- type: "button",
983
- disabled: starting,
984
- onClick: capture,
985
- "aria-label": "Capturar",
986
- className: "font-normal flex h-16 w-16 items-center justify-center rounded-full border-4 border-white/40 bg-white transition-transform active:scale-95 disabled:opacity-60",
987
- children: /* @__PURE__ */ jsx(Camera, { className: "h-6 w-6 text-black" })
988
- }
989
- ),
990
- /* @__PURE__ */ jsx(
991
- Button,
992
- {
993
- variant: "bare",
994
- size: "bare",
995
- type: "button",
996
- onClick: () => fileRef.current?.click(),
997
- "aria-label": "Escolher do arquivo",
998
- className: "font-normal absolute left-6 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
999
- children: /* @__PURE__ */ jsx(ImageUp, { className: "h-5 w-5" })
1000
- }
1001
- )
1002
- ] }),
1003
- !preview && onSkip && /* @__PURE__ */ jsx(
1004
- Button,
1005
- {
1006
- variant: "bare",
1007
- size: "bare",
1008
- type: "button",
1009
- onClick: onSkip,
1010
- "aria-label": "Pular esta foto",
1011
- className: "font-normal absolute right-6 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
1012
- children: /* @__PURE__ */ jsx(SkipForward, { className: "h-5 w-5" })
1013
- }
1014
- )
1015
- ] })
1016
- ] }),
1017
- document.body
1018
- );
1019
- }
1020
- var RICH_TEXT_CLASS = [
1021
- "space-y-2",
1022
- "[&_p]:leading-relaxed",
1023
- "[&_b]:font-semibold [&_strong]:font-semibold",
1024
- "[&_i]:italic [&_em]:italic",
1025
- "[&_u]:underline",
1026
- "[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:leading-relaxed",
1027
- "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground",
1028
- "[&_a]:text-primary [&_a]:underline",
1029
- "[&_h1]:font-semibold [&_h2]:font-semibold [&_h3]:font-semibold",
1030
- "[&_table]:w-full [&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1",
1031
- "[&_th]:border [&_th]:border-border [&_th]:px-2 [&_th]:py-1 [&_th]:font-medium"
1032
- ].join(" ");
1033
- function isFieldEmpty(field, value) {
1034
- if (field.type === "title") return false;
1035
- if (value == null || value === "" || value === false) return true;
1036
- if (Array.isArray(value)) return value.length === 0;
1037
- if (field.type === "yes_no" || field.type === "yes_no_text") return yesNoAnswer(value) === "";
1038
- return false;
1039
- }
1040
- function ReadField({ label, children }) {
1041
- return /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
1042
- label && /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: label }),
1043
- /* @__PURE__ */ jsx("div", { className: "text-sm text-foreground", children })
1044
- ] });
1045
- }
1046
- function EmptyValue() {
1047
- return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/50", children: "\u2014" });
1048
- }
1049
- function optionLabel(field, value) {
1050
- if (value == null || value === "") return null;
1051
- const match = (field.options ?? []).find((o) => o.value === value);
1052
- return match?.label ?? String(value);
1053
- }
1054
- function ReadValue({ field, value }) {
1055
- switch (field.type) {
1056
- case "memo":
1057
- case "richtext": {
1058
- const rich = fieldRichValue(value);
1059
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: "html" in rich ? /* @__PURE__ */ jsx("div", { className: RICH_TEXT_CLASS, dangerouslySetInnerHTML: { __html: rich.html } }) : rich.text ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap", children: rich.text }) : /* @__PURE__ */ jsx(EmptyValue, {}) });
1060
- }
1061
- case "date": {
1062
- const raw = typeof value === "string" ? value : "";
1063
- const date = raw ? new Date(raw) : null;
1064
- const text = date && !Number.isNaN(date.getTime()) ? raw.slice(0, 10).split("-").reverse().join("/") : raw;
1065
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: text || /* @__PURE__ */ jsx(EmptyValue, {}) });
1066
- }
1067
- case "select":
1068
- case "radio": {
1069
- const label = optionLabel(field, value);
1070
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: label ?? /* @__PURE__ */ jsx(EmptyValue, {}) });
1071
- }
1072
- case "tags": {
1073
- const selected = Array.isArray(value) ? value : [];
1074
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: selected.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: selected.map((v) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: optionLabel(field, v) ?? v }, v)) }) : /* @__PURE__ */ jsx(EmptyValue, {}) });
1075
- }
1076
- case "checkbox":
1077
- return /* @__PURE__ */ jsxs(ReadField, { children: [
1078
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: field.label }),
1079
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: " \xB7 " }),
1080
- /* @__PURE__ */ jsx("span", { children: value ? "Sim" : "N\xE3o" })
1081
- ] });
1082
- case "yes_no":
1083
- case "yes_no_text": {
1084
- const answer = yesNoAnswer(value);
1085
- const text = value && typeof value === "object" ? value.text?.trim() : "";
1086
- const alert = answerAlerts(field, value);
1087
- return /* @__PURE__ */ jsxs(ReadField, { label: field.label, children: [
1088
- answer ? /* @__PURE__ */ jsx("span", { className: "font-medium", children: answer === "sim" ? "Sim" : "N\xE3o" }) : /* @__PURE__ */ jsx(EmptyValue, {}),
1089
- text ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
1090
- " \u2014 ",
1091
- text
1092
- ] }) : null,
1093
- alert && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(AnswerAlertNote, { alert }) })
1094
- ] });
1095
- }
1096
- case "stencil_photo":
1097
- case "signature":
1098
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: isMedia(value) ? /* @__PURE__ */ jsx(
1099
- MediaImage,
1100
- {
1101
- value,
1102
- alt: field.label,
1103
- className: field.type === "signature" ? "max-h-32 rounded-md border bg-white" : "w-full max-w-xs rounded-lg border object-cover",
1104
- style: field.type === "signature" ? void 0 : { aspectRatio: String(aspectRatioOf(stencilOf(field))) }
1105
- }
1106
- ) : /* @__PURE__ */ jsx(EmptyValue, {}) });
1107
- case "gallery": {
1108
- const items = Array.isArray(value) ? value.filter(isMedia) : [];
1109
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: items.length ? /* @__PURE__ */ jsx(GalleryView, { items }) : /* @__PURE__ */ jsx(EmptyValue, {}) });
1110
- }
1111
- case "recommendation":
1112
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: /* @__PURE__ */ jsx(RecommendationView, { items: Array.isArray(value) ? value : [] }) });
1113
- default: {
1114
- const text = value == null || value === "" ? null : String(value);
1115
- return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: text ?? /* @__PURE__ */ jsx(EmptyValue, {}) });
1116
- }
1117
- }
1118
- }
1119
- function FieldLabel({ field }) {
1120
- return /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1121
- field.label,
1122
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1123
- ] });
1124
- }
1125
- function FieldRenderer({
1126
- field,
1127
- value,
1128
- onChange,
1129
- readOnly,
1130
- onOpenCamera,
1131
- uploading,
1132
- shortcuts
1133
- }) {
1134
- useTranslation();
1135
- if (readOnly && field.type !== "title" && field.type !== "image" && field.type !== "budget") {
1136
- return /* @__PURE__ */ jsx(ReadValue, { field, value });
1137
- }
1138
- switch (field.type) {
1139
- case "title":
1140
- return /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold pt-2 pb-1 border-b", children: field.label });
1141
- case "text":
1142
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1143
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1144
- field.label,
1145
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1146
- ] }),
1147
- /* @__PURE__ */ jsx(
1148
- Input,
1149
- {
1150
- value: value ?? "",
1151
- onChange: (e) => onChange(e.target.value),
1152
- placeholder: htmlToPlainText2(field.placeholder),
1153
- className: "h-9 text-sm",
1154
- readOnly
1155
- }
1156
- )
1157
- ] });
1158
- case "memo":
1159
- case "richtext":
1160
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1161
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1162
- field.label,
1163
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1164
- ] }),
1165
- /* @__PURE__ */ jsx(
1166
- Textarea,
1167
- {
1168
- value: value ?? "",
1169
- onChange: (e) => onChange(e.target.value),
1170
- placeholder: htmlToPlainText2(field.placeholder),
1171
- className: "min-h-[100px] bg-background shadow-none",
1172
- readOnly
1173
- }
1174
- )
1175
- ] });
1176
- case "date":
1177
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1178
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1179
- field.label,
1180
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1181
- ] }),
1182
- /* @__PURE__ */ jsx(
1183
- Input,
1184
- {
1185
- type: "date",
1186
- value: value ?? "",
1187
- onChange: (e) => onChange(e.target.value),
1188
- className: "h-9 text-sm",
1189
- readOnly
1190
- }
1191
- )
1192
- ] });
1193
- case "select":
1194
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1195
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1196
- field.label,
1197
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1198
- ] }),
1199
- readOnly ? /* @__PURE__ */ jsx("p", { className: "text-sm py-1", children: value ?? "-" }) : /* @__PURE__ */ jsxs(Select, { value: value ?? "", onValueChange: onChange, children: [
1200
- /* @__PURE__ */ jsx(SelectTrigger, { className: "h-9 text-sm", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: htmlToPlainText2(field.placeholder) ?? "..." }) }),
1201
- /* @__PURE__ */ jsx(SelectContent, { children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })
1202
- ] })
1203
- ] });
1204
- case "radio":
1205
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1206
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1207
- field.label,
1208
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1209
- ] }),
1210
- /* @__PURE__ */ jsx("div", { className: "space-y-1", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
1211
- /* @__PURE__ */ jsx(
1212
- "input",
1213
- {
1214
- type: "radio",
1215
- name: field.id,
1216
- value: opt.value,
1217
- checked: value === opt.value,
1218
- onChange: () => onChange(opt.value),
1219
- disabled: readOnly,
1220
- className: "text-primary"
1221
- }
1222
- ),
1223
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: opt.label })
1224
- ] }, opt.value)) })
1225
- ] });
1226
- case "tags": {
1227
- const selectedTags = Array.isArray(value) ? value : [];
1228
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1229
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1230
- field.label,
1231
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1232
- ] }),
1233
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: (field.options ?? []).map((opt) => {
1234
- const active = selectedTags.includes(opt.value);
1235
- return /* @__PURE__ */ jsx(
1236
- Badge,
1237
- {
1238
- variant: active ? "default" : "outline",
1239
- className: `cursor-pointer text-xs ${readOnly ? "pointer-events-none" : ""}`,
1240
- onClick: () => {
1241
- if (readOnly) return;
1242
- onChange(
1243
- active ? selectedTags.filter((t2) => t2 !== opt.value) : [...selectedTags, opt.value]
1244
- );
1245
- },
1246
- children: opt.label
1247
- },
1248
- opt.value
1249
- );
1250
- }) })
1251
- ] });
1252
- }
1253
- case "checkbox":
1254
- return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer py-1", children: [
1255
- /* @__PURE__ */ jsx(Checkbox, { checked: !!value, onChange, disabled: readOnly }),
1256
- /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium", children: [
1257
- field.label,
1258
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1259
- ] })
1260
- ] });
1261
- case "yes_no":
1262
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1263
- /* @__PURE__ */ jsx(FieldLabel, { field }),
1264
- /* @__PURE__ */ jsx(YesNoField, { field, value, onChange })
1265
- ] });
1266
- case "yes_no_text":
1267
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1268
- /* @__PURE__ */ jsx(FieldLabel, { field }),
1269
- /* @__PURE__ */ jsx(YesNoTextField, { field, value, onChange })
1270
- ] });
1271
- case "stencil_photo":
1272
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1273
- /* @__PURE__ */ jsx(FieldLabel, { field }),
1274
- /* @__PURE__ */ jsx(
1275
- StencilPhotoField,
1276
- {
1277
- field,
1278
- value,
1279
- onChange,
1280
- uploading,
1281
- onOpenCamera: () => onOpenCamera?.()
1282
- }
1283
- )
1284
- ] });
1285
- case "gallery":
1286
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1287
- /* @__PURE__ */ jsx(FieldLabel, { field }),
1288
- /* @__PURE__ */ jsx(GalleryField, { field, value, onChange })
1289
- ] });
1290
- case "recommendation":
1291
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1292
- /* @__PURE__ */ jsx(FieldLabel, { field }),
1293
- /* @__PURE__ */ jsx(RecommendationField, { value, onChange, shortcuts })
1294
- ] });
1295
- case "signature":
1296
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1297
- /* @__PURE__ */ jsx(FieldLabel, { field }),
1298
- /* @__PURE__ */ jsx(SignatureField, { field, value, onChange })
1299
- ] });
1300
- case "image":
1301
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1302
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1303
- field.label,
1304
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1305
- ] }),
1306
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border-2 border-dashed py-8 text-sm text-muted-foreground", children: "Image upload \u2014 v2" })
1307
- ] });
1308
- case "budget":
1309
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1310
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
1311
- field.label,
1312
- field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
1313
- ] }),
1314
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border-2 border-dashed py-8 text-sm text-muted-foreground", children: "Budget / line items \u2014 v2" })
1315
- ] });
1316
- default:
1317
- return null;
1318
- }
1319
- }
1320
- function FormRenderer({ schema, data, onChange, readOnly, missing, recommendationShortcuts }) {
1321
- const sortedFields = [...schema.fields].sort((a, b) => a.row - b.row || a.col - b.col);
1322
- const upload = useMediaUpload();
1323
- const [camera, setCamera] = React3.useState(null);
1324
- const [uploading, setUploading] = React3.useState(null);
1325
- const [cameraError, setCameraError] = React3.useState(null);
1326
- const dataRef = React3.useRef(data);
1327
- dataRef.current = data;
1328
- const handleFieldChange = useCallback(
1329
- (fieldId, value) => {
1330
- onChange({ ...dataRef.current, [fieldId]: value });
1331
- },
1332
- [onChange]
1333
- );
1334
- const nextStencil = (afterId) => {
1335
- const i = sortedFields.findIndex((f) => f.id === afterId);
1336
- return sortedFields.slice(i + 1).find((f) => f.type === "stencil_photo" && !isMedia(dataRef.current[f.id])) ?? null;
1337
- };
1338
- async function onCapture(photo) {
1339
- const field = camera;
1340
- if (!field || !upload) return;
1341
- setCamera(nextStencil(field.id));
1342
- setUploading(field.id);
1343
- setCameraError(null);
1344
- try {
1345
- const { path } = await upload(photo, `uploads/${field.id}`, "foto.jpg");
1346
- handleFieldChange(field.id, { path, type: "photo" });
1347
- } catch (e) {
1348
- setCameraError(`${field.label}: ${describeError(e, "n\xE3o foi poss\xEDvel enviar a foto")}`);
1349
- } finally {
1350
- setUploading(null);
1351
- }
1352
- }
1353
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1354
- cameraError && /* @__PURE__ */ jsx("p", { className: "mb-3 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: cameraError }),
1355
- /* @__PURE__ */ jsx(
1356
- "div",
1357
- {
1358
- className: "grid gap-x-4 gap-y-3",
1359
- style: { gridTemplateColumns: "repeat(12, 1fr)" },
1360
- children: sortedFields.map((field) => /* @__PURE__ */ jsx(
1361
- "div",
1362
- {
1363
- id: `ff-field-${field.id}`,
1364
- style: { gridColumn: `span ${field.colSpan}` },
1365
- className: missing?.has(field.id) ? "rounded-lg ring-2 ring-destructive/60 ring-offset-4 ring-offset-background" : void 0,
1366
- children: /* @__PURE__ */ jsx(
1367
- FieldRenderer,
1368
- {
1369
- field,
1370
- value: data[field.id],
1371
- onChange: (v) => handleFieldChange(field.id, v),
1372
- readOnly,
1373
- uploading: uploading === field.id,
1374
- onOpenCamera: () => setCamera(field),
1375
- shortcuts: recommendationShortcuts
1376
- }
1377
- )
1378
- },
1379
- field.id
1380
- ))
1381
- }
1382
- ),
1383
- camera && /* @__PURE__ */ jsx(CameraHost, { field: camera, onCapture, onSkip: () => setCamera(nextStencil(camera.id)), onClose: () => setCamera(null) })
1384
- ] });
1385
- }
1386
- function CameraHost({ field, onCapture, onSkip, onClose }) {
1387
- const cfg = stencilOf(field);
1388
- const overlay = useMediaUrl(cfg.overlayPath);
1389
- return /* @__PURE__ */ jsx(
1390
- StencilCamera,
1391
- {
1392
- title: field.label,
1393
- overlayUrl: overlay,
1394
- opacity: cfg.opacity ?? 0.6,
1395
- aspect: cfg.aspect ?? "3:4",
1396
- camera: cfg.camera ?? "environment",
1397
- burnOverlay: !!cfg.burnOverlay,
1398
- onCapture,
1399
- onSkip,
1400
- onClose
1401
- },
1402
- field.id
1403
- );
1404
- }
1405
-
1406
- // src/lib/print.ts
1407
- function esc(value) {
1408
- return String(value ?? "").replace(
1409
- /[&<>"']/g,
1410
- (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
1411
- );
1412
- }
1413
- function nl2br(value) {
1414
- return esc(value).replace(/\r?\n/g, "<br/>");
1415
- }
1416
- function formatDate(value) {
1417
- const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
1418
- if (!m) return esc(value);
1419
- return `${m[3]}/${m[2]}/${m[1]}`;
1420
- }
1421
- function optionLabel2(field, value) {
1422
- const opt = (field.options ?? []).find((o) => o.value === value);
1423
- return esc(opt ? opt.label : value);
1424
- }
1425
- function renderValue(field, value) {
1426
- const empty = '<span class="empty">\u2014</span>';
1427
- switch (field.type) {
1428
- case "memo":
1429
- case "richtext": {
1430
- const rich = fieldRichValue(value);
1431
- if ("html" in rich) return `<div class="long rich">${rich.html}</div>`;
1432
- return rich.text ? `<div class="long">${nl2br(rich.text)}</div>` : empty;
1433
- }
1434
- case "date":
1435
- return value ? formatDate(String(value)) : empty;
1436
- case "select":
1437
- case "radio":
1438
- return value ? optionLabel2(field, value) : empty;
1439
- case "tags": {
1440
- const arr = Array.isArray(value) ? value : [];
1441
- return arr.length ? arr.map((v) => optionLabel2(field, v)).join(", ") : empty;
1442
- }
1443
- case "checkbox":
1444
- return value ? "Sim" : "N\xE3o";
1445
- case "yes_no":
1446
- case "yes_no_text": {
1447
- const v = value;
1448
- const a = typeof v === "string" ? v : v?.answer;
1449
- const text = typeof v === "object" && v?.text?.trim() ? ` \u2014 ${esc(v.text.trim())}` : "";
1450
- return a === "sim" ? `Sim${text}` : a === "nao" ? `N\xE3o${text}` : empty;
1451
- }
1452
- case "recommendation": {
1453
- const lines = Array.isArray(value) ? value : [];
1454
- return lines.length ? lines.map((l) => esc(`${l.quantity > 1 ? `${l.quantity}\xD7 ` : ""}${l.name}`)).join("<br>") : empty;
1455
- }
1456
- case "image":
1457
- case "gallery":
1458
- case "budget":
1459
- case "stencil_photo":
1460
- case "signature":
1461
- return "";
1462
- // not meaningful on a printed contract
1463
- default:
1464
- return value ? esc(value) : empty;
1465
- }
1466
- }
1467
- function buildPrintHtml(template, doc, opts = {}) {
1468
- const heading = opts.heading ?? doc.title ?? template.name;
1469
- const subParts = [opts.subheading ?? doc.personName, new Date(doc.createdAt).toLocaleDateString()].filter(Boolean).map((s) => esc(s)).join(" &middot; ");
1470
- const fields = [...template.schema.fields].sort((a, b) => a.row - b.row || a.col - b.col);
1471
- const clampSpan = (n) => Math.max(1, Math.min(12, Number(n) || 12));
1472
- const body = fields.map((field) => {
1473
- if (field.type === "title") {
1474
- return `<h2 class="section" style="grid-column: 1 / -1">${esc(field.label)}</h2>`;
1475
- }
1476
- const rendered = renderValue(field, doc.data[field.id]);
1477
- if (rendered === "") return "";
1478
- const isLong = field.type === "memo" || field.type === "richtext";
1479
- return `<div class="field${isLong ? " field--long" : ""}" style="grid-column: span ${clampSpan(field.colSpan)}">
1480
- <div class="label">${esc(field.label)}</div>
1481
- <div class="value">${rendered}</div>
1482
- </div>`;
1483
- }).join("\n");
1484
- return `<!doctype html>
1485
- <html lang="pt-BR">
1486
- <head>
1487
- <meta charset="utf-8"/>
1488
- <title>${esc(heading)}</title>
1489
- <style>
1490
- @page { size: A4; margin: 20mm; }
1491
- * { box-sizing: border-box; }
1492
- html, body { margin: 0; padding: 0; }
1493
- body {
1494
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
1495
- color: #111; font-size: 12pt; line-height: 1.5;
1496
- -webkit-print-color-adjust: exact; print-color-adjust: exact;
1497
- }
1498
- .page { max-width: 170mm; margin: 0 auto; padding: 12mm 0; }
1499
- header { border-bottom: 2px solid #111; padding-bottom: 8px; margin-bottom: 20px; }
1500
- header h1 { font-size: 18pt; margin: 0 0 2px; }
1501
- header .sub { font-size: 10pt; color: #555; }
1502
- /* 12-column grid \u2014 mirrors the builder Preview (FormRenderer) so the print
1503
- keeps the authored column layout. */
1504
- .grid { display: grid; grid-template-columns: repeat(12, 1fr); column-gap: 16px; row-gap: 12px; align-items: start; }
1505
- h2.section { font-size: 13pt; margin: 22px 0 4px; padding-bottom: 3px; border-bottom: 1px solid #ccc; }
1506
- .grid > h2.section:first-child { margin-top: 4px; }
1507
- .field { margin: 0; min-width: 0; }
1508
- .field .label { font-size: 9pt; text-transform: uppercase; letter-spacing: .04em; color: #666; margin-bottom: 2px; }
1509
- .field .value { font-size: 12pt; }
1510
- .field--long .value .long { white-space: normal; text-align: justify; }
1511
- /* Conteudo formatado (sanitizado em lib/html-text.ts). O HTML chega sem
1512
- class nenhuma, entao o estilo vem por tag. orphans/widows evitam que um
1513
- paragrafo deixe uma linha solta virando a pagina. */
1514
- .long.rich p { margin: 0 0 8px; orphans: 2; widows: 2; }
1515
- .long.rich p:last-child { margin-bottom: 0; }
1516
- .long.rich ul, .long.rich ol { margin: 0 0 8px; padding-left: 20px; }
1517
- .long.rich li { margin: 0 0 3px; }
1518
- .long.rich b, .long.rich strong { font-weight: 600; }
1519
- .long.rich blockquote { margin: 0 0 8px; padding-left: 10px; border-left: 2px solid #ccc; color: #444; }
1520
- .long.rich a { color: #111; text-decoration: underline; }
1521
- .long.rich table { width: 100%; border-collapse: collapse; margin: 0 0 8px; }
1522
- .long.rich td, .long.rich th { border: 1px solid #ccc; padding: 4px 6px; text-align: left; }
1523
- .long.rich h1, .long.rich h2, .long.rich h3,
1524
- .long.rich h4, .long.rich h5, .long.rich h6 { font-size: 12pt; font-weight: 600; margin: 10px 0 4px; }
1525
- .empty { color: #bbb; }
1526
- footer { margin-top: 40px; font-size: 9pt; color: #888; text-align: center; }
1527
- @media screen { body { background: #f3f3f3; } .page { background: #fff; max-width: 210mm; min-height: 297mm; padding: 20mm; box-shadow: 0 1px 8px rgba(0,0,0,.15); } }
1528
- </style>
1529
- </head>
1530
- <body>
1531
- <div class="page">
1532
- <header>
1533
- <h1>${esc(heading)}</h1>
1534
- ${subParts ? `<div class="sub">${subParts}</div>` : ""}
1535
- </header>
1536
- <div class="grid">
1537
- ${body}
1538
- </div>
1539
- </div>
1540
- </body>
1541
- </html>`;
1542
- }
1543
- function printHtml(html) {
1544
- if (typeof document === "undefined") return;
1545
- const iframe = document.createElement("iframe");
1546
- iframe.setAttribute("aria-hidden", "true");
1547
- iframe.style.position = "fixed";
1548
- iframe.style.right = "0";
1549
- iframe.style.bottom = "0";
1550
- iframe.style.width = "0";
1551
- iframe.style.height = "0";
1552
- iframe.style.border = "0";
1553
- document.body.appendChild(iframe);
1554
- const cleanup = () => {
1555
- setTimeout(() => {
1556
- if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
1557
- }, 1e3);
1558
- };
1559
- const win = iframe.contentWindow;
1560
- const idoc = iframe.contentDocument || win?.document;
1561
- if (!win || !idoc) {
1562
- cleanup();
1563
- return;
1564
- }
1565
- idoc.open();
1566
- idoc.write(html);
1567
- idoc.close();
1568
- const doPrint = () => {
1569
- try {
1570
- win.focus();
1571
- win.print();
1572
- } finally {
1573
- cleanup();
1574
- }
1575
- };
1576
- if (idoc.readyState === "complete") {
1577
- setTimeout(doPrint, 50);
1578
- } else {
1579
- iframe.addEventListener("load", () => setTimeout(doPrint, 50), { once: true });
1580
- }
1581
- }
1582
- function printDocument(template, doc, opts) {
1583
- printHtml(buildPrintHtml(template, doc, opts));
1584
- }
1585
-
1586
- // src/lib/agent-tools.ts
1587
- var CATEGORY_ENUM = ["anamnesis", "evolution", "report", "contract", "general"];
1588
- var FIELD_SHAPE = "A field object: { type, label, required?, placeholder?, options?[] }. type \u2208 title|text|memo|richtext|date|select|radio|tags|checkbox|image|gallery|budget (title = section heading, no input). options are plain strings, only for select/radio/tags.";
1589
- var createOrUpdateFormTool = {
1590
- id: "custom_forms.createOrUpdateForm",
1591
- name: "createOrUpdateForm",
1592
- description: "Creates or edits a form template (anamnese, contrato, ficha\u2026). Persisted server-side, so it works from ANY page \u2014 the user does NOT need the form builder open. Omit templateId to CREATE (name required); pass templateId to EDIT an existing form. Send the FULL desired field list in `fields` (replaces all fields), or `append_fields` to add to what already exists. Layout is automatic \u2014 never send positions. Understand the intent and propose a sensible field set in one call rather than asking field by field. The user confirms before it is saved.",
1593
- icon: "FileText",
1594
- mode: "persist",
1595
- limitKey: "form_templates",
1596
- permission: { feature: "custom_forms", action: "create" },
1597
- execution: { plane: "server", kind: "rpc", rpc: "agent_forms_upsert_template" },
1598
- category: "Forms",
1599
- suggestions: [
1600
- { label: "Monte um formul\xE1rio de anamnese" },
1601
- { label: "Adicione um campo de alergias neste formul\xE1rio" }
1602
- ],
1603
- parameters: {
1604
- type: "object",
1605
- properties: {
1606
- templateId: {
1607
- type: "string",
1608
- description: "Existing form id to EDIT (from context or listFormTemplates). Omit to create a new form."
1609
- },
1610
- name: { type: "string", description: "Form name \u2014 required when creating." },
1611
- category: {
1612
- type: "string",
1613
- enum: [...CATEGORY_ENUM],
1614
- description: "anamnesis | evolution | report | contract | general"
1615
- },
1616
- description: { type: "string", description: "Optional short description of the form." },
1617
- fields: {
1618
- type: "array",
1619
- description: `The FULL desired field list \u2014 REPLACES every existing field. Each item: ${FIELD_SHAPE}`,
1620
- items: { type: "object", description: FIELD_SHAPE }
1621
- },
1622
- append_fields: {
1623
- type: "array",
1624
- description: `Fields to ADD to the existing form, keeping current ones. Use this for incremental edits instead of \`fields\`. Each item: ${FIELD_SHAPE}`,
1625
- items: { type: "object", description: FIELD_SHAPE }
1626
- }
1627
- }
1628
- }
1629
- };
1630
- var FORM_BUILDER_INSTRUCTIONS = [
1631
- "Voc\xEA est\xE1 na tela do CONSTRUTOR DE FORMUL\xC1RIOS. Para montar ou editar formul\xE1rios use a tool createOrUpdateForm (ela persiste no backend e funciona mesmo fora desta tela).",
1632
- "O formul\xE1rio aberto agora \u2014 id, nome, categoria e campos com seus ids \u2014 est\xE1 no JSON da tela em contexto.",
1633
- "Para EDITAR este formul\xE1rio, passe o templateId do contexto. Para adicionar campos mantendo os atuais, use append_fields; para redefinir tudo, use fields com a lista completa.",
1634
- "Ao criar do zero: entenda a inten\xE7\xE3o e proponha um conjunto de campos enxuto numa \xFAnica chamada (n\xE3o pergunte campo a campo). Depois confirme em uma frase o que foi montado.",
1635
- "Tipos de campo: title (cabe\xE7alho de se\xE7\xE3o, sem input), text, memo (multilinha), richtext, date, select, radio, tags, checkbox, image, gallery, budget. Layout \xE9 autom\xE1tico."
1636
- ].join(" ");
1637
-
1638
- // src/lib/person-fields.ts
1639
- var PERSON_FIELD_OPTIONS = [
1640
- { value: "name", labelKey: "customForms.personField.name" },
1641
- { value: "documentNumber", labelKey: "customForms.personField.documentNumber" },
1642
- { value: "email", labelKey: "customForms.personField.email" },
1643
- { value: "phone", labelKey: "customForms.personField.phone" },
1644
- { value: "dateOfBirth", labelKey: "customForms.personField.dateOfBirth" },
1645
- { value: "guardianName", labelKey: "customForms.personField.guardianName" },
1646
- { value: "address", labelKey: "customForms.personField.address" },
1647
- { value: "city", labelKey: "customForms.personField.city" },
1648
- { value: "state", labelKey: "customForms.personField.state" }
1649
- ];
1650
- var toSnake = (k) => k.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase());
1651
- var toCamel = (k) => k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
1652
- function resolvePersonValue(person, mapKey) {
1653
- if (!person || !mapKey) return void 0;
1654
- for (const key of [mapKey, toSnake(mapKey), toCamel(mapKey)]) {
1655
- const v = person[key];
1656
- if (v != null && v !== "") return v;
1657
- }
1658
- return void 0;
1659
- }
1660
- function prefillFromPerson(schema, person, existing = {}) {
1661
- const data = { ...existing };
1662
- if (!schema) return data;
1663
- for (const field of schema.fields) {
1664
- const current = data[field.id];
1665
- if (current != null && current !== "") continue;
1666
- const mapped = field.map ? resolvePersonValue(person, field.map) : void 0;
1667
- if (mapped != null) {
1668
- data[field.id] = mapped;
1669
- } else if (field.defaultValue != null) {
1670
- data[field.id] = field.defaultValue;
1671
- }
1672
- }
1673
- return data;
1674
- }
1675
-
1676
- export { CustomFormsProvider, FORM_BUILDER_INSTRUCTIONS, FormRenderer, MediaImage, PERSON_FIELD_OPTIONS, alertOf, answerAlerts, buildPrintHtml, createOrUpdateFormTool, htmlToPlainText2 as htmlToPlainText, isFieldEmpty, isMedia, prefillFromPerson, printDocument, sanitizeHtml, useMediaUpload, yesNoAnswer };
1677
- //# sourceMappingURL=chunk-JRR7A5HN.js.map
1678
- //# sourceMappingURL=chunk-JRR7A5HN.js.map