@fayz-ai/plugin-forms 0.14.8 → 0.15.0

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.
Files changed (86) hide show
  1. package/dist/{FormBuilder-EI7ILXQW.js → FormBuilder-Q2Q42HAT.js} +44 -35
  2. package/dist/FormBuilder-Q2Q42HAT.js.map +1 -0
  3. package/dist/{chunk-ANOY2YYX.js → chunk-JRR7A5HN.js} +59 -42
  4. package/dist/chunk-JRR7A5HN.js.map +1 -0
  5. package/dist/components/BuilderCanvas.d.ts +1 -1
  6. package/dist/components/BuilderCanvas.d.ts.map +1 -1
  7. package/dist/components/BuilderField.d.ts +1 -1
  8. package/dist/components/BuilderField.d.ts.map +1 -1
  9. package/dist/components/DocumentFormDialog.d.ts +6 -5
  10. package/dist/components/DocumentFormDialog.d.ts.map +1 -1
  11. package/dist/components/DocumentList.d.ts +5 -5
  12. package/dist/components/DocumentList.d.ts.map +1 -1
  13. package/dist/components/DocumentPreview.d.ts +3 -3
  14. package/dist/components/DocumentPreview.d.ts.map +1 -1
  15. package/dist/components/FieldConfigDialog.d.ts +1 -1
  16. package/dist/components/FieldConfigDialog.d.ts.map +1 -1
  17. package/dist/components/FieldPalette.d.ts +1 -1
  18. package/dist/components/FieldPalette.d.ts.map +1 -1
  19. package/dist/components/FormBuilder.d.ts +4 -4
  20. package/dist/components/FormBuilder.d.ts.map +1 -1
  21. package/dist/components/FormFillDialog.d.ts +9 -32
  22. package/dist/components/FormFillDialog.d.ts.map +1 -1
  23. package/dist/components/FormRenderer.d.ts +1 -1
  24. package/dist/components/FormRenderer.d.ts.map +1 -1
  25. package/dist/components/FormViewer.d.ts +3 -3
  26. package/dist/components/FormViewer.d.ts.map +1 -1
  27. package/dist/components/PersonAnswerAlerts.d.ts +2 -2
  28. package/dist/components/PersonAnswerAlerts.d.ts.map +1 -1
  29. package/dist/components/PersonDocumentsWidget.d.ts +4 -4
  30. package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
  31. package/dist/components/PersonFormsAlert.d.ts +7 -41
  32. package/dist/components/PersonFormsAlert.d.ts.map +1 -1
  33. package/dist/components/TemplateSelector.d.ts +3 -3
  34. package/dist/components/TemplateSelector.d.ts.map +1 -1
  35. package/dist/components/fields/FieldTypeSettings.d.ts +1 -1
  36. package/dist/components/fields/FieldTypeSettings.d.ts.map +1 -1
  37. package/dist/components/fields/StencilCamera.d.ts.map +1 -1
  38. package/dist/components/fields/fields.d.ts +3 -7
  39. package/dist/components/fields/fields.d.ts.map +1 -1
  40. package/dist/components/fields/media.d.ts +1 -1
  41. package/dist/components/fields/media.d.ts.map +1 -1
  42. package/dist/components/formsZoneWidgets.d.ts +5 -0
  43. package/dist/components/formsZoneWidgets.d.ts.map +1 -0
  44. package/dist/config.d.ts +2 -2
  45. package/dist/config.d.ts.map +1 -1
  46. package/dist/context.d.ts +4 -4
  47. package/dist/context.d.ts.map +1 -1
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +382 -1099
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/attendance-contracts.d.ts.map +1 -1
  53. package/dist/lib/categories.d.ts +2 -2
  54. package/dist/lib/categories.d.ts.map +1 -1
  55. package/dist/lib/contract-render.d.ts.map +1 -1
  56. package/dist/lib/person-fields.d.ts +1 -1
  57. package/dist/lib/person-fields.d.ts.map +1 -1
  58. package/dist/lib/print.d.ts +2 -1
  59. package/dist/lib/print.d.ts.map +1 -1
  60. package/dist/settings/v1-settings.d.ts +2 -0
  61. package/dist/settings/v1-settings.d.ts.map +1 -0
  62. package/dist/views/CustomFormsSettingsTab.d.ts +4 -4
  63. package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
  64. package/dist/views/TemplateListView.d.ts +3 -3
  65. package/dist/views/TemplateListView.d.ts.map +1 -1
  66. package/package.json +5 -4
  67. package/dist/FormBuilder-EI7ILXQW.js.map +0 -1
  68. package/dist/chunk-ANOY2YYX.js.map +0 -1
  69. package/dist/data/index.d.ts +0 -4
  70. package/dist/data/index.d.ts.map +0 -1
  71. package/dist/data/mock.d.ts +0 -3
  72. package/dist/data/mock.d.ts.map +0 -1
  73. package/dist/data/supabase.d.ts +0 -3
  74. package/dist/data/supabase.d.ts.map +0 -1
  75. package/dist/data/tables.d.ts +0 -7
  76. package/dist/data/tables.d.ts.map +0 -1
  77. package/dist/data/types.d.ts +0 -57
  78. package/dist/data/types.d.ts.map +0 -1
  79. package/dist/lib/person-requirements.d.ts +0 -32
  80. package/dist/lib/person-requirements.d.ts.map +0 -1
  81. package/dist/lib/tenant.d.ts +0 -3
  82. package/dist/lib/tenant.d.ts.map +0 -1
  83. package/dist/store.d.ts +0 -40
  84. package/dist/store.d.ts.map +0 -1
  85. package/dist/types.d.ts +0 -248
  86. package/dist/types.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,772 +1,284 @@
1
- import { isFieldEmpty, CustomFormsProvider, FormRenderer, sanitizeHtml, createOrUpdateFormTool, alertOf, answerAlerts, yesNoAnswer, prefillFromPerson, printDocument, htmlToPlainText, isMedia } from './chunk-ANOY2YYX.js';
2
- import React12, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
3
- import { tl, createSafeDataProvider, describeError, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, getActiveTenantId, useTranslation, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
4
- import { getFayzSupabaseClient, useOrganizationStore, PluginSettingsPanel, registerTimelineSource, registerTabCountProvider, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions, invalidateTabCounts, TimelineList } from '@fayz-ai/admin';
5
- import { create } from 'zustand';
1
+ import { isFieldEmpty, CustomFormsProvider, FormRenderer, sanitizeHtml, createOrUpdateFormTool, alertOf, answerAlerts, yesNoAnswer, prefillFromPerson, printDocument, htmlToPlainText, isMedia } from './chunk-JRR7A5HN.js';
2
+ import React13, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
3
+ import { tl, describeError, useActiveTenantId, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, getActiveTenantId, useTranslation, formatCurrency, useDataVersion } from '@fayz-ai/core';
4
+ import { T } from '@fayz-ai/forms/data/tables';
5
+ import { createSafeFormsProvider, createFormsStore, FORM_TEMPLATE_EVENT } from '@fayz-ai/forms/store';
6
+ import { defineSettingsTab, PluginSettingsPanel, registerTimelineSource, registerTabCountProvider, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions, invalidateTabCounts, TimelineList } from '@fayz-ai/admin';
7
+ import { FORMS_ZONES } from '@fayz-ai/forms/views/zones';
8
+ import { Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Skeleton, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, Input, Badge, Switch, RowActions, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, ConfirmDialog } from '@fayz-ai/ui';
9
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
10
  import { ClipboardList, QrCode, Printer, Check, CheckCircle2, Clock, Circle, X, Camera, FileText, FileSignature, FileCheck2, Activity, Stethoscope, Image, Paperclip, AlertTriangle, ChevronUp, ChevronDown, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft, EyeOff, Eye, Upload } from 'lucide-react';
7
- import { Skeleton, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Button, Input, Badge, Switch, RowActions, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, ConfirmDialog } from '@fayz-ai/ui';
8
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
+ import { shownInChecklist, isOutstanding } from '@fayz-ai/forms/domain/requirement';
12
+ export { isOutstanding } from '@fayz-ai/forms/domain/requirement';
13
+ export { getPersonFormRequirements } from '@fayz-ai/forms/data/supabase';
9
14
 
10
- // src/data/tables.ts
11
- var T = {
12
- documents: "plg_forms_documents",
13
- templates: "plg_forms_templates",
14
- documentFiles: "plg_forms_document_files",
15
- categories: "plg_forms_categories"
16
- };
17
-
18
- // src/data/mock.ts
19
- function uuid() {
20
- return crypto.randomUUID();
21
- }
22
- function now() {
23
- return (/* @__PURE__ */ new Date()).toISOString();
24
- }
25
- function createMockFormsProvider() {
26
- const templates = [];
27
- const documents = [];
28
- const files = [];
15
+ var defaultLabels = () => ({
16
+ pageTitle: tl("Forms", "Formul\xE1rios"),
17
+ settingsLabel: tl("Forms", "Formul\xE1rios"),
18
+ settingsSubtitle: tl("Create and manage custom forms for your business", "Crie e organize os formul\xE1rios da sua empresa"),
19
+ templates: tl("Templates", "Modelos"),
20
+ documents: tl("Documents", "Documentos"),
21
+ newTemplate: tl("New Template", "Novo modelo"),
22
+ addDocument: tl("Add Document", "Adicionar documento")
23
+ });
24
+ function resolveConfig(options) {
29
25
  return {
30
- // ── Templates ──────────────────────────────────────────
31
- async getTemplates(query) {
32
- let filtered = templates.filter((t) => !t.isDeleted && t.isCurrent);
33
- if (query.category) {
34
- filtered = filtered.filter((t) => t.category === query.category);
35
- }
36
- if (query.search) {
37
- const s = query.search.toLowerCase();
38
- filtered = filtered.filter((t) => t.name.toLowerCase().includes(s));
39
- }
40
- const page = query.page ?? 1;
41
- const pageSize = query.pageSize ?? 25;
42
- const start = (page - 1) * pageSize;
43
- return { data: filtered.slice(start, start + pageSize), total: filtered.length };
44
- },
45
- async getTemplateById(id) {
46
- return templates.find((t) => t.id === id && !t.isDeleted) ?? null;
47
- },
48
- async createTemplate(input) {
49
- const template = {
50
- id: uuid(),
51
- tenantId: "mock-tenant",
52
- name: input.name,
53
- description: input.description,
54
- category: input.category,
55
- version: 1,
56
- isCurrent: true,
57
- schema: input.schema,
58
- specialty: input.specialty,
59
- tags: input.tags ?? [],
60
- metadata: input.metadata ?? {},
61
- isActive: true,
62
- isDeleted: false,
63
- requiredOnAttendance: false,
64
- alertWhenMissing: false,
65
- validityDays: null,
66
- createdAt: now(),
67
- updatedAt: now()
68
- };
69
- templates.push(template);
70
- return template;
71
- },
72
- async updateTemplate(id, input) {
73
- const idx = templates.findIndex((t) => t.id === id);
74
- if (idx === -1) throw new Error("Template not found");
75
- const existing = templates[idx];
76
- const updated = {
77
- ...existing,
78
- ...input,
79
- tags: input.tags ?? existing.tags,
80
- metadata: input.metadata ?? existing.metadata,
81
- schema: input.schema ?? existing.schema,
82
- updatedAt: now()
83
- };
84
- templates[idx] = updated;
85
- return updated;
86
- },
87
- async deleteTemplate(id) {
88
- const idx = templates.findIndex((t) => t.id === id);
89
- if (idx !== -1) templates[idx] = { ...templates[idx], isDeleted: true };
90
- },
91
- async getTemplateUsage() {
92
- const usage = {};
93
- for (const doc of documents) {
94
- if (doc.isDeleted || !doc.templateId) continue;
95
- const current = usage[doc.templateId] ?? { documentCount: 0, lastDocumentAt: null };
96
- current.documentCount += 1;
97
- if (!current.lastDocumentAt || doc.createdAt > current.lastDocumentAt) {
98
- current.lastDocumentAt = doc.createdAt;
99
- }
100
- usage[doc.templateId] = current;
101
- }
102
- return usage;
103
- },
104
- // ── Documents ──────────────────────────────────────────
105
- async getDocuments(query) {
106
- let filtered = documents.filter((d) => !d.isDeleted);
107
- if (query.personId) {
108
- filtered = filtered.filter((d) => d.personId === query.personId);
109
- }
110
- if (query.templateId) {
111
- filtered = filtered.filter((d) => d.templateId === query.templateId);
112
- }
113
- if (query.status) {
114
- const statuses = Array.isArray(query.status) ? query.status : [query.status];
115
- filtered = filtered.filter((d) => statuses.includes(d.status));
116
- }
117
- if (query.search) {
118
- const s = query.search.toLowerCase();
119
- filtered = filtered.filter(
120
- (d) => d.title?.toLowerCase().includes(s) || d.templateName?.toLowerCase().includes(s)
121
- );
122
- }
123
- if (query.kinds?.length) {
124
- filtered = filtered.filter((d) => query.kinds.includes(d.kind ?? "form"));
125
- }
126
- if (query.templateCategory) {
127
- filtered = filtered.filter((d) => (d.templateCategory ?? "general") === query.templateCategory);
128
- }
129
- filtered.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
130
- const page = query.page ?? 1;
131
- const pageSize = query.pageSize ?? 25;
132
- const start = (page - 1) * pageSize;
133
- return { data: filtered.slice(start, start + pageSize), total: filtered.length };
134
- },
135
- async getDocumentCounts(query) {
136
- const counts = {};
137
- for (const d of documents) {
138
- if (d.isDeleted || d.status === "archived") continue;
139
- if (d.personId !== query.personId) continue;
140
- if (query.kinds?.length && !query.kinds.includes(d.kind ?? "form")) continue;
141
- const bucket = d.kind === "image" || d.kind === "attachment" ? "file" : d.templateCategory ?? "general";
142
- counts[bucket] = (counts[bucket] ?? 0) + 1;
143
- }
144
- return counts;
145
- },
146
- async getDocumentById(id) {
147
- return documents.find((d) => d.id === id && !d.isDeleted) ?? null;
148
- },
149
- async createDocument(input) {
150
- const template = templates.find((t) => t.id === input.templateId);
151
- const doc = {
152
- id: uuid(),
153
- tenantId: "mock-tenant",
154
- templateId: input.templateId ?? "",
155
- templateName: template?.name,
156
- templateCategory: template?.category,
157
- personId: input.personId,
158
- title: input.title,
159
- data: input.data ?? {},
160
- kind: input.kind ?? (input.templateId ? "form" : "attachment"),
161
- status: input.status ?? "draft",
162
- metadata: {},
163
- isDeleted: false,
164
- createdAt: now(),
165
- updatedAt: now()
166
- };
167
- documents.push(doc);
168
- return doc;
169
- },
170
- async updateDocument(id, input) {
171
- const idx = documents.findIndex((d) => d.id === id);
172
- if (idx === -1) throw new Error("Document not found");
173
- const existing = documents[idx];
174
- const updated = {
175
- ...existing,
176
- ...input,
177
- data: input.data ?? existing.data,
178
- updatedAt: now()
179
- };
180
- documents[idx] = updated;
181
- return updated;
182
- },
183
- async deleteDocument(id) {
184
- const idx = documents.findIndex((d) => d.id === id);
185
- if (idx !== -1) documents[idx] = { ...documents[idx], isDeleted: true };
186
- },
187
- // ── Files ──────────────────────────────────────────────
188
- async getDocumentFiles(documentId) {
189
- return files.filter((f) => f.documentId === documentId);
190
- },
191
- async uploadFile(documentId, fieldKey, file) {
192
- const docFile = {
193
- id: uuid(),
194
- tenantId: "mock-tenant",
195
- documentId,
196
- fieldKey,
197
- fileUrl: URL.createObjectURL(file),
198
- fileName: file.name,
199
- fileSize: file.size,
200
- mimeType: file.type,
201
- sortOrder: files.filter((f) => f.documentId === documentId && f.fieldKey === fieldKey).length,
202
- metadata: {},
203
- createdAt: now()
204
- };
205
- files.push(docFile);
206
- return docFile;
207
- },
208
- async uploadMedia(blob) {
209
- return { path: URL.createObjectURL(blob) };
210
- },
211
- async mediaUrl(path) {
212
- return path;
213
- },
214
- async searchCatalog() {
215
- return [];
216
- },
217
- async deleteFile(fileId) {
218
- const idx = files.findIndex((f) => f.id === fileId);
219
- if (idx !== -1) files.splice(idx, 1);
220
- }
26
+ labels: { ...defaultLabels(), ...options?.labels }
221
27
  };
222
28
  }
223
- function getFormsTenantId() {
224
- return getActiveTenantId();
225
- }
226
- function getTenantId() {
227
- return getFormsTenantId() ?? getActiveTenantId() ?? "";
228
- }
229
- function getClient() {
230
- const supabase = getSupabaseClientOptional();
231
- if (!supabase) throw new Error("Supabase not initialized");
232
- return supabase;
233
- }
234
- var MEDIA_BUCKET = "attachments";
235
- var SIGNED_TTL_S = 3600;
236
- var signedCache = /* @__PURE__ */ new Map();
237
- var USAGE_FALLBACK_LIMIT = 5e3;
238
- async function countTemplateUsageClientSide(db2, tenantId2) {
239
- try {
240
- const { data, error, count } = await db2.from("v_documents").select("template_id, created_at", { count: "exact" }).eq("tenant_id", tenantId2).eq("is_active", true).not("template_id", "is", null).limit(USAGE_FALLBACK_LIMIT);
241
- if (error) return {};
242
- if (typeof count === "number" && count > USAGE_FALLBACK_LIMIT) return {};
243
- const usage = {};
244
- for (const row of data ?? []) {
245
- const current = usage[row.template_id] ?? { documentCount: 0, lastDocumentAt: null };
246
- current.documentCount += 1;
247
- if (!current.lastDocumentAt || row.created_at > current.lastDocumentAt) {
248
- current.lastDocumentAt = row.created_at;
249
- }
250
- usage[row.template_id] = current;
29
+ function recommendationsOf(template, data) {
30
+ const by = /* @__PURE__ */ new Map();
31
+ for (const f of template.schema.fields) {
32
+ if (f.type !== "recommendation" || !Array.isArray(data[f.id])) continue;
33
+ for (const l of data[f.id]) {
34
+ const cur = by.get(l.serviceId);
35
+ by.set(l.serviceId, cur ? { ...cur, quantity: cur.quantity + l.quantity } : { ...l });
251
36
  }
252
- return usage;
253
- } catch {
254
- return {};
255
37
  }
38
+ return [...by.values()];
256
39
  }
257
- function mapTemplate(r) {
258
- return {
259
- requiredOnAttendance: r.required_on_attendance ?? false,
260
- validityDays: r.validity_days ?? null,
261
- alertWhenMissing: r.alert_when_missing ?? false,
262
- id: r.id,
263
- tenantId: r.tenant_id,
264
- name: r.name,
265
- description: r.description,
266
- category: r.category,
267
- version: r.version,
268
- isCurrent: r.is_current,
269
- parentId: r.parent_id,
270
- schema: r.schema,
271
- specialty: r.specialty,
272
- tags: r.tags ?? [],
273
- metadata: r.metadata ?? {},
274
- isActive: r.is_active,
275
- isDeleted: r.is_deleted,
276
- createdBy: r.created_by,
277
- updatedBy: r.updated_by,
278
- createdAt: r.created_at,
279
- updatedAt: r.updated_at
280
- };
281
- }
282
- function mapDocument(r) {
283
- return {
284
- id: r.id,
285
- tenantId: r.tenant_id,
286
- templateId: r.template_id ?? "",
287
- templateName: r.template_name,
288
- templateCategory: r.template_category,
289
- personId: r.person_id,
290
- personName: r.person_name,
291
- title: r.title,
292
- data: r.form_data ?? {},
293
- status: r.status,
294
- signedAt: r.signed_at,
295
- signedBy: r.signed_by,
296
- notes: r.notes,
297
- metadata: r.metadata ?? {},
298
- isDeleted: false,
299
- createdBy: r.created_by,
300
- updatedBy: r.updated_by,
301
- createdAt: r.created_at,
302
- updatedAt: r.updated_at,
303
- // Extra fields from core document
304
- kind: r.kind,
305
- fileUrl: r.file_url,
306
- fileName: r.file_name,
307
- fileSize: r.file_size,
308
- mimeType: r.mime_type
309
- };
310
- }
311
- function mapFile(r) {
312
- return {
313
- id: r.id,
314
- tenantId: r.tenant_id,
315
- documentId: r.document_id,
316
- fieldKey: r.field_key,
317
- fileUrl: r.file_url,
318
- fileName: r.file_name,
319
- fileSize: r.file_size,
320
- mimeType: r.mime_type,
321
- sortOrder: r.sort_order,
322
- metadata: r.metadata ?? {},
323
- createdAt: r.created_at
324
- };
325
- }
326
- function createSupabaseFormsProvider() {
327
- return {
328
- // ── Templates ──────────────────────────────────────────
329
- async getTemplates(query) {
330
- const db2 = getClient();
331
- let qb = db2.from(T.templates).select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_current", true).eq("is_deleted", false).eq("is_active", true);
332
- if (query.category) qb = qb.eq("category", query.category);
333
- if (query.search) qb = qb.ilike("name", `%${query.search}%`);
334
- const page = query.page ?? 1;
335
- const pageSize = query.pageSize ?? 25;
336
- qb = qb.order("updated_at", { ascending: false }).range((page - 1) * pageSize, page * pageSize - 1);
337
- const { data, count, error } = await qb;
338
- if (error) throw new Error(error.message);
339
- return { data: (data ?? []).map(mapTemplate), total: count ?? 0 };
340
- },
341
- async getTemplateById(id) {
342
- const db2 = getClient();
343
- const { data, error } = await db2.from(T.templates).select("*").eq("id", id).eq("is_deleted", false).single();
344
- if (error || !data) return null;
345
- return mapTemplate(data);
346
- },
347
- async createTemplate(input) {
348
- const db2 = getClient();
349
- const tenantId2 = getTenantId();
350
- const { data, error } = await db2.from(T.templates).insert({
351
- tenant_id: tenantId2,
352
- name: input.name,
353
- description: input.description,
354
- category: input.category,
355
- schema: input.schema,
356
- specialty: input.specialty,
357
- tags: input.tags ?? [],
358
- metadata: input.metadata ?? {}
359
- }).select().single();
360
- if (error) throw new Error(error.message);
361
- return mapTemplate(data);
362
- },
363
- async updateTemplate(id, input) {
364
- const db2 = getClient();
365
- const tenantId2 = getTenantId();
366
- const { count } = await db2.from(T.documents).select("document_id", { count: "exact", head: true }).eq("template_id", id);
367
- const hasDocuments = (count ?? 0) > 0;
368
- if (hasDocuments && input.schema) {
369
- const existing = await this.getTemplateById(id);
370
- if (!existing) throw new Error("Template not found");
371
- const { data: newRow, error: insertError } = await db2.from(T.templates).insert({
372
- tenant_id: tenantId2,
373
- name: input.name ?? existing.name,
374
- description: input.description ?? existing.description,
375
- category: input.category ?? existing.category,
376
- schema: input.schema,
377
- specialty: input.specialty ?? existing.specialty,
378
- tags: input.tags ?? existing.tags,
379
- metadata: input.metadata ?? existing.metadata,
380
- is_active: input.isActive ?? existing.isActive,
381
- version: existing.version + 1,
382
- is_current: true,
383
- parent_id: existing.parentId ?? existing.id,
384
- // A EXIGÊNCIA É DO MODELO, NÃO DA VERSÃO. Elas eram gravadas na
385
- // linha que estava sendo ARQUIVADA: editar os campos de uma
386
- // anamnese exigida devolvia uma versão nova que não exigia nada,
387
- // e o aviso sumia sem ninguém ter desligado nada.
388
- required_on_attendance: input.requiredOnAttendance ?? existing.requiredOnAttendance,
389
- validity_days: input.validityDays !== void 0 ? input.validityDays : existing.validityDays,
390
- alert_when_missing: input.alertWhenMissing ?? existing.alertWhenMissing
391
- }).select().single();
392
- if (insertError) throw new Error(insertError.message);
393
- await db2.from(T.templates).update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
394
- return mapTemplate(newRow);
395
- }
396
- const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
397
- if (input.name !== void 0) updates.name = input.name;
398
- if (input.description !== void 0) updates.description = input.description;
399
- if (input.category !== void 0) updates.category = input.category;
400
- if (input.schema !== void 0) updates.schema = input.schema;
401
- if (input.specialty !== void 0) updates.specialty = input.specialty;
402
- if (input.tags !== void 0) updates.tags = input.tags;
403
- if (input.metadata !== void 0) updates.metadata = input.metadata;
404
- if (input.isActive !== void 0) updates.is_active = input.isActive;
405
- if (input.requiredOnAttendance !== void 0) updates.required_on_attendance = input.requiredOnAttendance;
406
- if (input.validityDays !== void 0) updates.validity_days = input.validityDays;
407
- if (input.alertWhenMissing !== void 0) updates.alert_when_missing = input.alertWhenMissing;
408
- const { data, error } = await db2.from(T.templates).update(updates).eq("id", id).select().single();
409
- if (error) throw new Error(error.message);
410
- return mapTemplate(data);
411
- },
412
- async deleteTemplate(id) {
413
- const db2 = getClient();
414
- const { error } = await db2.from(T.templates).update({ is_deleted: true, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
415
- if (error) throw new Error(error.message);
416
- },
417
- // Uso por modelo. Caminho principal: `v_forms_template_usage`, que agrega no
418
- // banco (004_template_usage.sql) e devolve uma linha por modelo.
419
- //
420
- // O fallback existe porque um pool provisionado antes daquela migração não
421
- // tem a view, e a tela não pode ficar sem o número por causa disso: ele
422
- // relê as colunas mínimas dos documentos e agrega aqui. É limitado de
423
- // propósito — acima do teto o número seria mentira, e mentir sobre uma
424
- // contagem é pior do que omiti-la, então ele desiste em silêncio.
425
- async getTemplateUsage() {
426
- const db2 = getClient();
427
- const tenantId2 = getTenantId();
428
- if (!tenantId2) return {};
429
- const { data, error } = await db2.from("v_forms_template_usage").select("template_id, document_count, last_document_at").eq("tenant_id", tenantId2);
430
- if (!error) {
431
- const usage = {};
432
- for (const row of data ?? []) {
433
- usage[row.template_id] = {
434
- documentCount: Number(row.document_count ?? 0),
435
- lastDocumentAt: row.last_document_at ?? null
436
- };
437
- }
438
- return usage;
439
- }
440
- return countTemplateUsageClientSide(db2, tenantId2);
441
- },
442
- // ── Documents (public.documents + plg_forms_documents extension) ──
443
- async getDocuments(query) {
444
- const db2 = getClient();
445
- let qb = db2.from("v_documents").select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_active", true);
446
- if (query.personId) qb = qb.eq("person_id", query.personId);
447
- if (query.templateId) qb = qb.eq("template_id", query.templateId);
448
- if (query.status) {
449
- const statuses = Array.isArray(query.status) ? query.status : [query.status];
450
- qb = qb.in("status", statuses);
451
- }
452
- if (query.search) qb = qb.or(`title.ilike.%${query.search}%,template_name.ilike.%${query.search}%`);
453
- if (query.kinds?.length) qb = qb.in("kind", query.kinds);
454
- if (query.templateCategory) qb = qb.eq("template_category", query.templateCategory);
455
- const page = query.page ?? 1;
456
- const pageSize = query.pageSize ?? 50;
457
- qb = qb.order("created_at", { ascending: false }).range((page - 1) * pageSize, page * pageSize - 1);
458
- const { data, count, error } = await qb;
459
- if (error) throw new Error(error.message);
460
- return { data: (data ?? []).map(mapDocument), total: count ?? 0 };
461
- },
462
- /**
463
- * Os totais por balde, sem trazer documento nenhum.
464
- *
465
- * Duas colunas e nada mais: o corpo do formulário (`form_data`) é o campo
466
- * pesado da view, e contar não precisa dele. Agrupa no cliente porque
467
- * PostgREST não faz `group by` — e a alternativa, um `count` por balde,
468
- * seriam seis idas para responder uma pergunta.
469
- */
470
- async getDocumentCounts(query) {
471
- const db2 = getClient();
472
- let qb = db2.from("v_documents").select("kind, template_category").eq("tenant_id", getTenantId()).eq("person_id", query.personId).eq("is_active", true).neq("status", "archived");
473
- if (query.kinds?.length) qb = qb.in("kind", query.kinds);
474
- const { data, error } = await qb;
475
- if (error) return {};
476
- const counts = {};
477
- for (const row of data ?? []) {
478
- const bucket = row.kind === "image" || row.kind === "attachment" ? "file" : row.template_category || "general";
479
- counts[bucket] = (counts[bucket] ?? 0) + 1;
480
- }
481
- return counts;
482
- },
483
- async getDocumentById(id) {
484
- const db2 = getClient();
485
- const { data, error } = await db2.from("v_documents").select("*").eq("id", id).single();
486
- if (error || !data) return null;
487
- return mapDocument(data);
488
- },
489
- async createDocument(input) {
490
- const tenantId2 = getTenantId();
491
- const isForm = !!input.templateId;
492
- const { data: coreDoc, error: coreError } = await getClient().from("documents").insert({
493
- tenant_id: tenantId2,
494
- kind: input.kind ?? (isForm ? "form" : "attachment"),
495
- person_id: input.personId,
496
- title: input.title,
497
- status: input.status ?? "draft",
498
- file_url: input.fileUrl,
499
- file_name: input.fileName,
500
- file_size: input.fileSize,
501
- mime_type: input.mimeType,
502
- subject_type: input.subjectType,
503
- subject_id: input.subjectId,
504
- metadata: {}
505
- }).select().single();
506
- if (coreError) throw new Error(coreError.message);
507
- if (isForm && input.templateId) {
508
- const db2 = getClient();
509
- const { error: extError } = await db2.from(T.documents).insert({
510
- document_id: coreDoc.id,
511
- tenant_id: tenantId2,
512
- template_id: input.templateId,
513
- data: input.data ?? {},
514
- product_id: input.productId ?? null,
515
- professional_id: input.professionalId ?? null
516
- });
517
- if (extError) {
518
- await getClient().from("documents").delete().eq("id", coreDoc.id);
519
- throw new Error(extError.message);
520
- }
521
- }
522
- const doc = await this.getDocumentById(coreDoc.id);
523
- return doc;
524
- },
525
- async updateDocument(id, input) {
526
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
527
- const coreUpdates = { updated_at: now2 };
528
- if (input.status !== void 0) coreUpdates.status = input.status;
529
- if (input.title !== void 0) coreUpdates.title = input.title;
530
- if (input.notes !== void 0) coreUpdates.notes = input.notes;
531
- const { error: coreError } = await getClient().from("documents").update(coreUpdates).eq("id", id);
532
- if (coreError) throw new Error(coreError.message);
533
- if (input.data !== void 0) {
534
- const db2 = getClient();
535
- await db2.from(T.documents).update({ data: input.data, updated_at: now2 }).eq("document_id", id);
536
- }
537
- const doc = await this.getDocumentById(id);
538
- return doc;
539
- },
540
- async deleteDocument(id) {
541
- const { error } = await getClient().from("documents").update({ is_active: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
542
- if (error) throw new Error(error.message);
543
- },
544
- // ── Files ──────────────────────────────────────────────
545
- async getDocumentFiles(documentId) {
546
- const db2 = getClient();
547
- const { data, error } = await db2.from(T.documentFiles).select("*").eq("document_id", documentId).order("sort_order");
548
- if (error) throw new Error(error.message);
549
- return (data ?? []).map(mapFile);
550
- },
551
- async uploadFile(documentId, fieldKey, file) {
552
- const db2 = getClient();
553
- const tenantId2 = getTenantId();
554
- const ext = file.name.split(".").pop() ?? "bin";
555
- const storagePath = `${tenantId2}/forms/${documentId}/${fieldKey}/${crypto.randomUUID()}.${ext}`;
556
- const { error: uploadError } = await db2.storage.from("documents").upload(storagePath, file, { contentType: file.type });
557
- if (uploadError) throw new Error(uploadError.message);
558
- const { data: urlData } = db2.storage.from("documents").getPublicUrl(storagePath);
559
- const { data, error } = await db2.from(T.documentFiles).insert({
560
- tenant_id: tenantId2,
561
- document_id: documentId,
562
- field_key: fieldKey,
563
- file_url: urlData.publicUrl,
564
- file_name: file.name,
565
- file_size: file.size,
566
- mime_type: file.type
567
- }).select().single();
568
- if (error) throw new Error(error.message);
569
- return mapFile(data);
570
- },
571
- async uploadMedia(blob, folder, fileName) {
572
- const tenantId2 = getTenantId();
573
- const ext = (fileName.split(".").pop() || "bin").toLowerCase();
574
- const path = `${tenantId2}/forms/${folder}/${crypto.randomUUID()}.${ext}`;
575
- const { error } = await getClient().storage.from(MEDIA_BUCKET).upload(path, blob, { contentType: blob.type || void 0, upsert: false });
576
- if (error) throw new Error(error.message);
577
- return { path };
578
- },
579
- async mediaUrl(path) {
580
- const cached = signedCache.get(path);
581
- if (cached && cached.until > Date.now()) return cached.url;
582
- const { data, error } = await getClient().storage.from(MEDIA_BUCKET).createSignedUrl(path, SIGNED_TTL_S);
583
- if (error || !data?.signedUrl) return null;
584
- signedCache.set(path, { url: data.signedUrl, until: Date.now() + (SIGNED_TTL_S - 60) * 1e3 });
585
- return data.signedUrl;
586
- },
587
- async searchCatalog(term) {
588
- const db2 = getClient();
589
- const tenantId2 = getTenantId();
590
- const like = term.trim() ? `%${term.trim()}%` : null;
591
- let sq = db2.from("products").select("id, name, price, category:categories(name)").eq("tenant_id", tenantId2).eq("kind", "service").eq("is_active", true).order("name").limit(500);
592
- if (like) sq = sq.ilike("name", like);
593
- let pq = db2.from("service_packages").select("id, name, price").eq("tenant_id", tenantId2).eq("is_active", true).order("name").limit(100);
594
- if (like) pq = pq.ilike("name", like);
595
- const [sv, pk] = await Promise.all([sq, pq]);
596
- if (sv.error) throw new Error(sv.error.message);
597
- return [
598
- ...(sv.data ?? []).map((r) => ({
599
- kind: "service",
600
- id: r.id,
601
- name: r.name,
602
- price: Number(r.price) || 0,
603
- group: r.category?.name ?? null
604
- })),
605
- ...(pk.data ?? []).map((r) => ({
606
- kind: "package",
607
- id: r.id,
608
- name: r.name,
609
- price: Number(r.price) || 0,
610
- group: null
611
- }))
612
- ];
613
- },
614
- async getLatestAnswers(personId, templateIds) {
615
- if (!templateIds.length) return [];
616
- const { data, error } = await getClient().from(T.documents).select("template_id, data, document:documents!inner(person_id, created_at, updated_at)").in("template_id", templateIds).eq("document.person_id", personId).order("created_at", { ascending: false, referencedTable: "documents" }).limit(50);
617
- if (error) throw new Error(error.message);
618
- const latest = /* @__PURE__ */ new Map();
619
- for (const r of data ?? []) {
620
- const at = r.document?.updated_at ?? r.document?.created_at ?? "";
621
- const cur = latest.get(r.template_id);
622
- if (!cur || at > cur.at) latest.set(r.template_id, { templateId: r.template_id, data: r.data ?? {}, at });
623
- }
624
- return [...latest.values()];
625
- },
626
- async getPackageServices(packageId) {
627
- const { data, error } = await getClient().from("service_package_items").select("product_id, quantity, unit_price, product:products(name, price, kind)").eq("package_id", packageId).order("sort_order");
628
- if (error) throw new Error(error.message);
629
- return (data ?? []).filter((r) => r.product_id && (r.product?.kind ?? "service") === "service").map((r) => ({
630
- serviceId: r.product_id,
631
- name: r.product?.name ?? "",
632
- unitPrice: Number(r.unit_price ?? r.product?.price) || 0,
633
- quantity: Math.max(1, Number(r.quantity) || 1)
634
- }));
635
- },
636
- async deleteFile(fileId) {
637
- const db2 = getClient();
638
- const { error } = await db2.from(T.documentFiles).delete().eq("id", fileId);
639
- if (error) throw new Error(error.message);
40
+ function FormFillDialog({
41
+ templateId,
42
+ personId,
43
+ personName,
44
+ onSaved,
45
+ onClose,
46
+ frame = "modal",
47
+ subject,
48
+ productId,
49
+ professionalId,
50
+ recommendationShortcuts,
51
+ provider: given
52
+ }) {
53
+ const provider = React13.useMemo(() => given ?? createSafeFormsProvider(), [given]);
54
+ const store = React13.useMemo(() => createFormsStore(provider), [provider]);
55
+ const config = React13.useMemo(() => resolveConfig(), []);
56
+ const [template, setTemplate] = React13.useState(null);
57
+ const [erro, setErro] = React13.useState(null);
58
+ const [data, setData] = React13.useState({});
59
+ const [salvando, setSalvando] = React13.useState(false);
60
+ const [missing, setMissing] = React13.useState(/* @__PURE__ */ new Set());
61
+ React13.useEffect(() => {
62
+ if (!template || missing.size === 0) return;
63
+ const still = new Set([...missing].filter((id) => {
64
+ const f = template.schema.fields.find((x) => x.id === id);
65
+ return f ? isFieldEmpty(f, data[id]) : false;
66
+ }));
67
+ if (still.size !== missing.size) setMissing(still);
68
+ if (still.size === 0) setErro(null);
69
+ }, [data, template, missing]);
70
+ async function salvar() {
71
+ if (!template) return;
72
+ const faltando = template.schema.fields.filter((f) => f.required && isFieldEmpty(f, data[f.id]));
73
+ if (faltando.length) {
74
+ setMissing(new Set(faltando.map((f) => f.id)));
75
+ const primeiro = [...faltando].sort((a, b) => a.row - b.row || a.col - b.col)[0];
76
+ requestAnimationFrame(() => document.getElementById(`ff-field-${primeiro.id}`)?.scrollIntoView({ block: "center" }));
77
+ setErro(faltando.length === 1 ? `Preencha o campo obrigat\xF3rio destacado: ${faltando[0].label}` : `Preencha os ${faltando.length} campos obrigat\xF3rios destacados.`);
78
+ return;
640
79
  }
641
- };
642
- }
643
- var FORM_TEMPLATE_EVENT = "forms.template.";
644
- function emitTemplateEvent(action, id, name) {
645
- emitDomainEvent(
646
- `${FORM_TEMPLATE_EVENT}${action}`,
647
- { record_id: id, record_name: name ?? null },
648
- { tenantId: getActiveTenantId() ?? null, subject: { type: "form_template", id } }
649
- );
650
- }
651
- function createCustomFormsStore(provider) {
652
- return create((set) => ({
653
- templates: [],
654
- templatesTotal: 0,
655
- templatesLoading: false,
656
- templatesError: null,
657
- templateUsage: {},
658
- documents: [],
659
- documentsTotal: 0,
660
- documentsLoading: false,
661
- documentsError: null,
662
- activeTemplate: null,
663
- activeTemplateLoading: false,
664
- activeDocument: null,
665
- activeDocumentLoading: false,
666
- async fetchTemplates(query) {
667
- set({ templatesLoading: true, templatesError: null });
668
- try {
669
- const result = await provider.getTemplates(query ?? {});
670
- set({ templates: result.data, templatesTotal: result.total });
671
- } catch (e) {
672
- set({ templatesError: e.message ?? "Failed to fetch templates" });
673
- } finally {
674
- set({ templatesLoading: false });
675
- }
676
- },
677
- // Separado de `fetchTemplates` porque é outra pergunta e outro custo: a
678
- // lista precisa dos modelos para desenhar, e o uso é um enfeite que chega
679
- // depois. Junto, a tela inteira esperaria a agregação; e uma falha na
680
- // contagem levaria embora a lista, que não tem nada a ver com isso — daí
681
- // ele engolir o erro em vez de escrevê-lo em `templatesError`.
682
- async fetchTemplateUsage() {
683
- try {
684
- set({ templateUsage: await provider.getTemplateUsage() });
685
- } catch {
686
- set({ templateUsage: {} });
687
- }
688
- },
689
- async fetchTemplateById(id) {
690
- set({ activeTemplateLoading: true });
691
- try {
692
- const template = await provider.getTemplateById(id);
693
- set({ activeTemplate: template });
694
- return template;
695
- } catch {
696
- set({ activeTemplate: null });
697
- return null;
698
- } finally {
699
- set({ activeTemplateLoading: false });
700
- }
701
- },
702
- async createTemplate(input) {
703
- const template = await provider.createTemplate(input);
704
- set((s) => ({ templates: [template, ...s.templates], templatesTotal: s.templatesTotal + 1 }));
705
- emitTemplateEvent("created", template.id, template.name);
706
- return template;
707
- },
708
- async updateTemplate(id, input) {
709
- const template = await provider.updateTemplate(id, input);
710
- set((s) => ({
711
- templates: s.templates.map((t) => t.id === id ? template : t),
712
- activeTemplate: s.activeTemplate?.id === id ? template : s.activeTemplate
713
- }));
714
- emitTemplateEvent("updated", template.id, template.name);
715
- return template;
716
- },
717
- async deleteTemplate(id) {
718
- await provider.deleteTemplate(id);
719
- emitTemplateEvent("deleted", id);
720
- set((s) => ({
721
- templates: s.templates.filter((t) => t.id !== id),
722
- templatesTotal: s.templatesTotal - 1
723
- }));
724
- },
725
- async fetchDocuments(query) {
726
- set({ documentsLoading: true, documentsError: null });
80
+ setErro(null);
81
+ setSalvando(true);
82
+ try {
83
+ const doc = await store.getState().createDocument({
84
+ templateId: template.id,
85
+ personId,
86
+ title: personName ? `${template.name} \u2014 ${personName}` : template.name,
87
+ data,
88
+ status: "completed",
89
+ subjectType: subject?.type,
90
+ subjectId: subject?.id,
91
+ productId: productId ?? void 0,
92
+ professionalId: professionalId ?? void 0
93
+ });
94
+ await onSaved?.({ documentId: doc.id, template, data, recommendations: recommendationsOf(template, data) });
95
+ onClose();
96
+ } catch (e) {
97
+ setErro(describeError(e, "N\xE3o foi poss\xEDvel salvar o formul\xE1rio."));
98
+ } finally {
99
+ setSalvando(false);
100
+ }
101
+ }
102
+ React13.useEffect(() => {
103
+ let vivo = true;
104
+ (async () => {
727
105
  try {
728
- const result = await provider.getDocuments(query ?? {});
729
- set({ documents: result.data, documentsTotal: result.total });
106
+ const t = await store.getState().fetchTemplateById(templateId);
107
+ if (vivo) setTemplate(t);
730
108
  } catch (e) {
731
- set({ documentsError: e.message ?? "Failed to fetch documents" });
732
- } finally {
733
- set({ documentsLoading: false });
109
+ if (vivo) setErro(describeError(e, "N\xE3o foi poss\xEDvel abrir o formul\xE1rio."));
734
110
  }
735
- },
736
- async fetchDocumentById(id) {
737
- set({ activeDocumentLoading: true });
738
- try {
739
- const doc = await provider.getDocumentById(id);
740
- set({ activeDocument: doc });
741
- return doc;
742
- } catch {
743
- set({ activeDocument: null });
744
- return null;
745
- } finally {
746
- set({ activeDocumentLoading: false });
111
+ })();
112
+ return () => {
113
+ vivo = false;
114
+ };
115
+ }, [templateId, store]);
116
+ const corpo = /* @__PURE__ */ jsxs(Fragment, { children: [
117
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: [
118
+ erro && /* @__PURE__ */ jsx("p", { role: "alert", className: "mb-3 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: erro }),
119
+ !template ? erro ? null : (
120
+ /* Esqueleto com a FORMA do formulário: três campos e um rótulo. */
121
+ /* @__PURE__ */ jsx("div", { className: "space-y-4", "aria-busy": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
122
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-24 animate-pulse rounded bg-muted/70" }),
123
+ /* @__PURE__ */ jsx("div", { className: "h-9 w-full animate-pulse rounded-lg bg-muted" })
124
+ ] }, i)) })
125
+ ) : /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
126
+ FormRenderer,
127
+ {
128
+ schema: template.schema,
129
+ data,
130
+ onChange: setData,
131
+ missing,
132
+ recommendationShortcuts
133
+ }
134
+ ) })
135
+ ] }),
136
+ /* @__PURE__ */ jsx("div", { className: "border-t fayz-glass-divider px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5 shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
137
+ /* @__PURE__ */ jsx(
138
+ Button,
139
+ {
140
+ variant: "bare",
141
+ size: "bare",
142
+ type: "button",
143
+ onClick: onClose,
144
+ className: "rounded-button px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground",
145
+ children: "Agora n\xE3o"
146
+ }
147
+ ),
148
+ /* @__PURE__ */ jsx(
149
+ Button,
150
+ {
151
+ variant: "bare",
152
+ size: "bare",
153
+ type: "button",
154
+ onClick: salvar,
155
+ disabled: !template || salvando,
156
+ className: "rounded-button bg-foreground px-3 py-1.5 text-xs font-semibold text-background disabled:opacity-40",
157
+ children: salvando ? "Salvando\u2026" : "Concluir"
158
+ }
159
+ )
160
+ ] }) })
161
+ ] });
162
+ if (frame === "inline") return corpo;
163
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (o) => {
164
+ if (!o) onClose();
165
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "2xl", noPadding: true, className: "gap-0", children: [
166
+ /* @__PURE__ */ jsx(ModalHeader, { className: "border-b px-5 py-3.5", children: /* @__PURE__ */ jsx(ModalTitle, { className: "truncate text-base", children: template?.name ?? "Formul\xE1rio" }) }),
167
+ /* @__PURE__ */ jsx(ModalBody, { className: "p-0", children: corpo })
168
+ ] }) });
169
+ }
170
+ var DATE = (iso) => (/* @__PURE__ */ new Date(iso + "T00:00:00")).toLocaleDateString("pt-BR");
171
+ function Linha({ item }) {
172
+ const Icone = item.state === "done" ? CheckCircle2 : item.state === "late" ? Clock : Circle;
173
+ const cor = item.state === "done" ? "text-success" : item.state === "late" ? "text-warning" : "text-muted-foreground";
174
+ return /* @__PURE__ */ jsxs("li", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 px-3 py-1.5 text-xs", children: [
175
+ /* @__PURE__ */ jsx(Icone, { className: `h-3.5 w-3.5 shrink-0 ${cor}`, "aria-hidden": true }),
176
+ /* @__PURE__ */ jsx("span", { className: item.state === "done" ? "text-foreground" : "font-medium text-foreground", children: item.label }),
177
+ item.detail && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
178
+ "\u2014 ",
179
+ item.detail
180
+ ] }),
181
+ item.action && // O VERBO NÃO PODE PARECER DESLIGADO. Em `text-primary` sobre o âmbar
182
+ // da faixa ele saía lavado e, quando quebrava a linha, parecia um
183
+ // rótulo cinza no canto — a recepção lia "pendente" e não via que
184
+ // havia o que clicar.
185
+ /* @__PURE__ */ jsx(
186
+ Button,
187
+ {
188
+ variant: "bare",
189
+ size: "bare",
190
+ type: "button",
191
+ onClick: item.action.onClick,
192
+ className: "ml-auto shrink-0 rounded-button bg-foreground px-2 py-0.5 text-[11px] font-semibold text-background hover:opacity-90",
193
+ children: item.action.label
747
194
  }
195
+ )
196
+ ] });
197
+ }
198
+ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, sendLink, className, provider: given }) {
199
+ const provider = React13.useMemo(() => given ?? createSafeFormsProvider(), [given]);
200
+ const tenantId2 = useActiveTenantId();
201
+ const [rows, setRows] = React13.useState(null);
202
+ React13.useEffect(() => {
203
+ if (!personId || !tenantId2) {
204
+ setRows([]);
205
+ return;
206
+ }
207
+ let cancelled = false;
208
+ setRows(null);
209
+ provider.getPersonRequirements(personId).then((r) => {
210
+ if (!cancelled) setRows(r);
211
+ }).catch(() => {
212
+ if (!cancelled) setRows([]);
213
+ });
214
+ return () => {
215
+ cancelled = true;
216
+ };
217
+ }, [personId, tenantId2, version, provider]);
218
+ const itens = React13.useMemo(() => [
219
+ ...(rows ?? []).filter(shownInChecklist).map((r) => {
220
+ const pendente = isOutstanding(r);
221
+ return {
222
+ id: r.templateId,
223
+ label: r.templateName,
224
+ state: !pendente ? "done" : r.state === "expired" ? "late" : "todo",
225
+ detail: !pendente ? r.validUntil ? `em dia, v\xE1lido at\xE9 ${DATE(r.validUntil)}` : "em dia" : r.state === "expired" && r.validUntil ? `venceu em ${DATE(r.validUntil)}` : "nunca preenchido",
226
+ action: pendente && onFill ? { label: "Preencher agora", onClick: () => onFill(r.templateId) } : void 0
227
+ };
228
+ }),
229
+ ...extras ?? []
230
+ ], [rows, extras, onFill]);
231
+ const peso = (i) => i.state === "late" ? 0 : i.state === "todo" ? 1 : 2;
232
+ itens.sort((a, b) => peso(a) - peso(b));
233
+ const pendentes = itens.filter((i) => i.state !== "done");
234
+ const faltando = pendentes.length;
235
+ const chave = pendentes.map((i) => i.label).join("|");
236
+ React13.useEffect(() => {
237
+ onPending?.(chave ? chave.split("|") : []);
238
+ }, [chave]);
239
+ if (rows === null) return /* @__PURE__ */ jsx(Skeleton, { className: `h-12 w-full ${className ?? ""}` });
240
+ if (itens.length === 0) return null;
241
+ return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-lg border ${faltando ? "border-warning/40 bg-warning/5" : "border-success/30 bg-success/5"} ${className ?? ""}`, children: [
242
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-inherit px-3 py-1.5 text-[11px] font-medium text-foreground", children: [
243
+ /* @__PURE__ */ jsx(ClipboardList, { className: `h-3.5 w-3.5 ${faltando ? "text-warning" : "text-success"}`, "aria-hidden": true }),
244
+ faltando === 0 ? "Checagem do paciente \u2014 tudo em dia" : faltando === 1 ? "1 item pendente com o paciente" : `${faltando} itens pendentes com o paciente`,
245
+ sendLink && /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
246
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
247
+ Button,
248
+ {
249
+ variant: "bare",
250
+ size: "bare",
251
+ type: "button",
252
+ "aria-label": "Enviar link ao paciente pelo WhatsApp",
253
+ "aria-disabled": sendLink === "soon",
254
+ onClick: sendLink === "soon" ? void 0 : sendLink,
255
+ className: `font-normal ${`ml-auto inline-flex h-6 w-6 items-center justify-center rounded-md transition-colors ${sendLink === "soon" ? "cursor-not-allowed text-muted-foreground/60" : "text-foreground hover:bg-muted"}`}`,
256
+ children: /* @__PURE__ */ jsx(QrCode, { className: "h-3.5 w-3.5", "aria-hidden": true })
257
+ }
258
+ ) }),
259
+ /* @__PURE__ */ jsx(TooltipContent, { side: "left", children: sendLink === "soon" ? "Enviar pelo WhatsApp (QR Code) \u2014 em breve" : "Enviar pelo WhatsApp (QR Code)" })
260
+ ] }) })
261
+ ] }),
262
+ /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/60", children: itens.map((i) => /* @__PURE__ */ jsx(Linha, { item: i }, i.id)) })
263
+ ] });
264
+ }
265
+
266
+ // src/components/formsZoneWidgets.ts
267
+ function formsZoneWidgets(provider) {
268
+ const PersonChecklist = (props) => React13.createElement(PersonFormsAlert, { ...props, provider });
269
+ const FormFill = (props) => React13.createElement(FormFillDialog, { ...props, provider });
270
+ return [
271
+ {
272
+ id: "forms-person-checklist",
273
+ zone: FORMS_ZONES.personChecklist,
274
+ component: PersonChecklist
748
275
  },
749
- async createDocument(input) {
750
- const doc = await provider.createDocument(input);
751
- set((s) => ({ documents: [doc, ...s.documents], documentsTotal: s.documentsTotal + 1 }));
752
- return doc;
753
- },
754
- async updateDocument(id, input) {
755
- const doc = await provider.updateDocument(id, input);
756
- set((s) => ({
757
- documents: s.documents.map((d) => d.id === id ? doc : d),
758
- activeDocument: s.activeDocument?.id === id ? doc : s.activeDocument
759
- }));
760
- return doc;
761
- },
762
- async deleteDocument(id) {
763
- await provider.deleteDocument(id);
764
- set((s) => ({
765
- documents: s.documents.filter((d) => d.id !== id),
766
- documentsTotal: s.documentsTotal - 1
767
- }));
276
+ {
277
+ id: "forms-form-fill",
278
+ zone: FORMS_ZONES.formFill,
279
+ component: FormFill
768
280
  }
769
- }));
281
+ ];
770
282
  }
771
283
 
772
284
  // src/locales/en.ts
@@ -1053,20 +565,6 @@ var customFormsLocales = {
1053
565
  en,
1054
566
  "pt-BR": ptBR
1055
567
  };
1056
- var defaultLabels = () => ({
1057
- pageTitle: tl("Forms", "Formul\xE1rios"),
1058
- settingsLabel: tl("Forms & Documents", "Formul\xE1rios e documentos"),
1059
- settingsSubtitle: tl("Create and manage custom forms for your business", "Crie e organize os formul\xE1rios da sua empresa"),
1060
- templates: tl("Templates", "Modelos"),
1061
- documents: tl("Documents", "Documentos"),
1062
- newTemplate: tl("New Template", "Novo modelo"),
1063
- addDocument: tl("Add Document", "Adicionar documento")
1064
- });
1065
- function resolveConfig(options) {
1066
- return {
1067
- labels: { ...defaultLabels(), ...options?.labels }
1068
- };
1069
- }
1070
568
  var CATEGORY_COLORS = {
1071
569
  anamnesis: "bg-info-soft text-info-soft-foreground",
1072
570
  evaluation: "bg-info-soft text-info-soft-foreground",
@@ -1082,7 +580,7 @@ function TemplateListView({ store, config, onEdit, onNew }) {
1082
580
  const templates = store((s) => s.templates);
1083
581
  const loading = store((s) => s.templatesLoading);
1084
582
  const usage = store((s) => s.templateUsage);
1085
- const orgId = getFormsTenantId();
583
+ const orgId = getActiveTenantId();
1086
584
  const createActions = useMemo(() => {
1087
585
  const permission = { feature: "custom_forms", action: "create" };
1088
586
  return [
@@ -1275,13 +773,12 @@ function TemplateListView({ store, config, onEdit, onNew }) {
1275
773
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "relative min-w-[10rem] flex-1", children: [
1276
774
  /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
1277
775
  /* @__PURE__ */ jsx(
1278
- "input",
776
+ Input,
1279
777
  {
1280
- type: "text",
1281
778
  value: search,
1282
779
  onChange: (e) => setSearch(e.target.value),
1283
780
  placeholder: t("common.search"),
1284
- className: "w-full bg-transparent py-1.5 pl-8 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none"
781
+ className: "h-auto rounded-none border-0 bg-transparent py-1.5 pl-8 pr-3 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
1285
782
  }
1286
783
  )
1287
784
  ] }) }),
@@ -1319,7 +816,7 @@ function CustomFormsSettingsTab({ config, provider, store }) {
1319
816
  window.location.hash = `#${ROUTE_BASE}/forms`;
1320
817
  store.getState().fetchTemplates();
1321
818
  }, [store]);
1322
- React12.useEffect(() => {
819
+ React13.useEffect(() => {
1323
820
  const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
1324
821
  window.addEventListener("hashchange", sync);
1325
822
  return () => window.removeEventListener("hashchange", sync);
@@ -1350,11 +847,11 @@ function BuilderWrapper({
1350
847
  config,
1351
848
  onBack
1352
849
  }) {
1353
- const FormBuilderLazy = React12.lazy(
1354
- () => import('./FormBuilder-EI7ILXQW.js').then((m) => ({ default: m.FormBuilder }))
850
+ const FormBuilderLazy = React13.lazy(
851
+ () => import('./FormBuilder-Q2Q42HAT.js').then((m) => ({ default: m.FormBuilder }))
1355
852
  );
1356
853
  return /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
1357
- React12.Suspense,
854
+ React13.Suspense,
1358
855
  {
1359
856
  fallback: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx("div", { className: "h-6 w-6 border-2 border-primary border-t-transparent animate-spin rounded-full" }) }),
1360
857
  children: /* @__PURE__ */ jsx(
@@ -1481,8 +978,10 @@ function DocumentPreview({
1481
978
  ] }, field.id);
1482
979
  }) }),
1483
980
  hidden > 0 && /* @__PURE__ */ jsx(
1484
- "button",
981
+ Button,
1485
982
  {
983
+ variant: "bare",
984
+ size: "bare",
1486
985
  type: "button",
1487
986
  onClick: onOpen,
1488
987
  className: "text-xs font-medium text-primary hover:underline",
@@ -1755,11 +1254,13 @@ function DocumentList({ personId, provider, store, onView, onFileDrop, kinds })
1755
1254
  buckets.length > 1 && [{ id: void 0, label: t("customForms.bucket.all"), count: totalAll }, ...buckets].map((chip) => {
1756
1255
  const active = chip.id === bucket;
1757
1256
  return /* @__PURE__ */ jsxs(
1758
- "button",
1257
+ Button,
1759
1258
  {
1259
+ variant: "bare",
1260
+ size: "bare",
1760
1261
  type: "button",
1761
1262
  onClick: () => setBucket(chip.id),
1762
- className: `rounded-full border px-2.5 py-1 text-xs transition-colors ${active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"}`,
1263
+ className: `gap-0 font-normal rounded-full border px-2.5 py-1 text-xs transition-colors ${active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"}`,
1763
1264
  children: [
1764
1265
  chip.label,
1765
1266
  /* @__PURE__ */ jsx("span", { className: "ml-1.5 tabular-nums opacity-60", children: chip.count })
@@ -1769,13 +1270,15 @@ function DocumentList({ personId, provider, store, onView, onFileDrop, kinds })
1769
1270
  );
1770
1271
  }),
1771
1272
  /* @__PURE__ */ jsxs(
1772
- "button",
1273
+ Button,
1773
1274
  {
1275
+ variant: "bare",
1276
+ size: "bare",
1774
1277
  type: "button",
1775
1278
  onClick: toggleExpandAll,
1776
1279
  "aria-pressed": expandAll,
1777
1280
  title: t(expandAll ? "customForms.hideContent" : "customForms.showContent"),
1778
- className: `ml-auto flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors ${expandAll ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"}`,
1281
+ className: `font-normal ml-auto flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors ${expandAll ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"}`,
1779
1282
  children: [
1780
1283
  expandAll ? /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" }),
1781
1284
  t(expandAll ? "customForms.hideContent" : "customForms.showContent")
@@ -1828,8 +1331,10 @@ function DocumentList({ personId, provider, store, onView, onFileDrop, kinds })
1828
1331
  if (!doc) return null;
1829
1332
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1830
1333
  /* @__PURE__ */ jsx(
1831
- "button",
1334
+ Button,
1832
1335
  {
1336
+ variant: "bare",
1337
+ size: "bare",
1833
1338
  type: "button",
1834
1339
  onClick: (e) => {
1835
1340
  e.stopPropagation();
@@ -1842,8 +1347,10 @@ function DocumentList({ personId, provider, store, onView, onFileDrop, kinds })
1842
1347
  }
1843
1348
  ),
1844
1349
  /* @__PURE__ */ jsx(
1845
- "button",
1350
+ Button,
1846
1351
  {
1352
+ variant: "bare",
1353
+ size: "bare",
1847
1354
  type: "button",
1848
1355
  onClick: (e) => {
1849
1356
  e.stopPropagation();
@@ -1921,7 +1428,7 @@ function DocumentFormDialog({
1921
1428
  provider.getTemplateById(existingDocument.templateId).then(setTemplate);
1922
1429
  }
1923
1430
  }, [existingDocument, template, provider]);
1924
- const prefilled = React12.useRef(!!initialTemplate || !!existingDocument);
1431
+ const prefilled = React13.useRef(!!initialTemplate || !!existingDocument);
1925
1432
  useEffect(() => {
1926
1433
  if (prefilled.current || !template || existingDocument) return;
1927
1434
  prefilled.current = true;
@@ -1953,7 +1460,7 @@ function DocumentFormDialog({
1953
1460
  },
1954
1461
  [template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
1955
1462
  );
1956
- const snapshot = React12.useRef(null);
1463
+ const snapshot = React13.useRef(null);
1957
1464
  useEffect(() => {
1958
1465
  if (template && snapshot.current === null) {
1959
1466
  snapshot.current = JSON.stringify(existingDocument?.data ?? {});
@@ -1973,8 +1480,10 @@ function DocumentFormDialog({
1973
1480
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1974
1481
  existingDocument?.title ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1975
1482
  /* @__PURE__ */ jsx(
1976
- "button",
1483
+ Button,
1977
1484
  {
1485
+ variant: "bare",
1486
+ size: "bare",
1978
1487
  onClick: onBack,
1979
1488
  className: "flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground",
1980
1489
  children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
@@ -1996,8 +1505,10 @@ function DocumentFormDialog({
1996
1505
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1997
1506
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1998
1507
  /* @__PURE__ */ jsx(
1999
- "button",
1508
+ Button,
2000
1509
  {
1510
+ variant: "bare",
1511
+ size: "bare",
2001
1512
  onClick: onBack,
2002
1513
  className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors",
2003
1514
  children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
@@ -2043,8 +1554,10 @@ function FormViewer({ document: doc, provider, onBack, onEdit }) {
2043
1554
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2044
1555
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2045
1556
  /* @__PURE__ */ jsx(
2046
- "button",
1557
+ Button,
2047
1558
  {
1559
+ variant: "bare",
1560
+ size: "bare",
2048
1561
  onClick: onBack,
2049
1562
  className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors",
2050
1563
  children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
@@ -2188,7 +1701,7 @@ function AddDocumentDropdown({ personId, onSelect, optionFilter, label }) {
2188
1701
  showSearch && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-2.5 py-2", children: [
2189
1702
  /* @__PURE__ */ jsx(Search, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }),
2190
1703
  /* @__PURE__ */ jsx(
2191
- "input",
1704
+ Input,
2192
1705
  {
2193
1706
  ref: inputRef,
2194
1707
  value: query,
@@ -2198,12 +1711,12 @@ function AddDocumentDropdown({ personId, onSelect, optionFilter, label }) {
2198
1711
  },
2199
1712
  onKeyDown: handleInputKeyDown,
2200
1713
  placeholder: t("customForms.searchDocumentTypes"),
2201
- className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
1714
+ className: "h-auto rounded-none border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
2202
1715
  }
2203
1716
  )
2204
1717
  ] }),
2205
1718
  /* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-[min(60vh,20rem)] overflow-y-auto overscroll-contain p-1", children: [
2206
- groups.map((group, gi) => /* @__PURE__ */ jsxs(React12.Fragment, { children: [
1719
+ groups.map((group, gi) => /* @__PURE__ */ jsxs(React13.Fragment, { children: [
2207
1720
  gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
2208
1721
  /* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
2209
1722
  group.items.map((opt) => {
@@ -2211,13 +1724,15 @@ function AddDocumentDropdown({ personId, onSelect, optionFilter, label }) {
2211
1724
  flatIndex += 1;
2212
1725
  const index = flatIndex;
2213
1726
  return /* @__PURE__ */ jsxs(
2214
- "button",
1727
+ Button,
2215
1728
  {
1729
+ variant: "bare",
1730
+ size: "bare",
2216
1731
  type: "button",
2217
1732
  "data-index": index,
2218
1733
  onClick: () => choose(opt),
2219
1734
  onMouseEnter: () => setActiveIndex(index),
2220
- className: `relative flex w-full cursor-default select-none items-center gap-2 rounded-button px-2 py-1.5 text-left text-sm outline-none transition-colors ${index === activeIndex ? "bg-muted" : ""}`,
1735
+ className: `font-normal justify-start relative flex w-full cursor-default select-none items-center gap-2 rounded-button px-2 py-1.5 text-left text-sm outline-none transition-colors ${index === activeIndex ? "bg-muted" : ""}`,
2221
1736
  children: [
2222
1737
  /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2223
1738
  /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
@@ -2395,8 +1910,10 @@ function FileDocumentViewer({ document: doc, onBack }) {
2395
1910
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2396
1911
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2397
1912
  /* @__PURE__ */ jsx(
2398
- "button",
1913
+ Button,
2399
1914
  {
1915
+ variant: "bare",
1916
+ size: "bare",
2400
1917
  onClick: onBack,
2401
1918
  className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors",
2402
1919
  children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
@@ -2425,9 +1942,9 @@ var TONE = {
2425
1942
  info: { wrap: "border-info/40 bg-info-soft", chip: "bg-info text-white", dot: "bg-info" }
2426
1943
  };
2427
1944
  function PersonAnswerAlerts({ personId, provider }) {
2428
- const [alerts, setAlerts] = React12.useState([]);
2429
- const [open, setOpen] = React12.useState(false);
2430
- React12.useEffect(() => {
1945
+ const [alerts, setAlerts] = React13.useState([]);
1946
+ const [open, setOpen] = React13.useState(false);
1947
+ React13.useEffect(() => {
2431
1948
  if (!personId || !provider.getLatestAnswers) return;
2432
1949
  let alive = true;
2433
1950
  (async () => {
@@ -2465,12 +1982,14 @@ function PersonAnswerAlerts({ personId, provider }) {
2465
1982
  const tone = TONE[alerts[0].severity];
2466
1983
  return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-lg border-2 ${tone.wrap}`, children: [
2467
1984
  /* @__PURE__ */ jsxs(
2468
- "button",
1985
+ Button,
2469
1986
  {
1987
+ variant: "bare",
1988
+ size: "bare",
2470
1989
  type: "button",
2471
1990
  onClick: () => setOpen((o) => !o),
2472
1991
  "aria-expanded": open,
2473
- className: "flex w-full items-center gap-2 px-3 py-2 text-left",
1992
+ className: "font-normal whitespace-normal justify-start flex w-full items-center gap-2 px-3 py-2 text-left",
2474
1993
  children: [
2475
1994
  /* @__PURE__ */ jsx("span", { className: `flex h-7 w-7 shrink-0 items-center justify-center rounded-full ${tone.chip}`, children: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4" }) }),
2476
1995
  /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
@@ -2632,267 +2151,8 @@ function buildFormsAnalyticsCards() {
2632
2151
  }
2633
2152
  ];
2634
2153
  }
2635
- async function getPersonFormRequirements(tenantId2, personId) {
2636
- const client = getFayzSupabaseClient();
2637
- const { data, error } = await client.rpc("forms_person_requirements", {
2638
- p_tenant_id: tenantId2,
2639
- p_person_id: personId
2640
- });
2641
- if (error) throw error;
2642
- return (data ?? []).map((r) => ({
2643
- templateId: r.template_id,
2644
- templateName: r.template_name,
2645
- category: r.category,
2646
- validityDays: r.validity_days ?? null,
2647
- alertWhenMissing: r.alert_when_missing ?? false,
2648
- lastFilledAt: r.last_filled_at ?? null,
2649
- validUntil: r.valid_until ?? null,
2650
- state: r.state
2651
- }));
2652
- }
2653
- var isOutstanding = (r) => r.state !== "valid";
2654
- var DATE = (iso) => (/* @__PURE__ */ new Date(iso + "T00:00:00")).toLocaleDateString("pt-BR");
2655
- function Linha({ item }) {
2656
- const Icone = item.state === "done" ? CheckCircle2 : item.state === "late" ? Clock : Circle;
2657
- const cor = item.state === "done" ? "text-success" : item.state === "late" ? "text-warning" : "text-muted-foreground";
2658
- return /* @__PURE__ */ jsxs("li", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 px-3 py-1.5 text-xs", children: [
2659
- /* @__PURE__ */ jsx(Icone, { className: `h-3.5 w-3.5 shrink-0 ${cor}`, "aria-hidden": true }),
2660
- /* @__PURE__ */ jsx("span", { className: item.state === "done" ? "text-foreground" : "font-medium text-foreground", children: item.label }),
2661
- item.detail && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
2662
- "\u2014 ",
2663
- item.detail
2664
- ] }),
2665
- item.action && // O VERBO NÃO PODE PARECER DESLIGADO. Em `text-primary` sobre o âmbar
2666
- // da faixa ele saía lavado e, quando quebrava a linha, parecia um
2667
- // rótulo cinza no canto — a recepção lia "pendente" e não via que
2668
- // havia o que clicar.
2669
- /* @__PURE__ */ jsx(
2670
- "button",
2671
- {
2672
- type: "button",
2673
- onClick: item.action.onClick,
2674
- className: "ml-auto shrink-0 rounded-button bg-foreground px-2 py-0.5 text-[11px] font-semibold text-background hover:opacity-90",
2675
- children: item.action.label
2676
- }
2677
- )
2678
- ] });
2679
- }
2680
- function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, sendLink, className }) {
2681
- const tenantId2 = useOrganizationStore((s) => s.currentOrg?.id);
2682
- const [rows, setRows] = React12.useState(null);
2683
- React12.useEffect(() => {
2684
- if (!personId || !tenantId2) {
2685
- setRows([]);
2686
- return;
2687
- }
2688
- let cancelled = false;
2689
- setRows(null);
2690
- getPersonFormRequirements(tenantId2, personId).then((r) => {
2691
- if (!cancelled) setRows(r);
2692
- }).catch(() => {
2693
- if (!cancelled) setRows([]);
2694
- });
2695
- return () => {
2696
- cancelled = true;
2697
- };
2698
- }, [personId, tenantId2, version]);
2699
- const itens = React12.useMemo(() => [
2700
- // O QUE NÃO ALERTA NÃO INTERROMPE. Exigir um formulário põe a bola no
2701
- // checklist do atendimento; ALERTAR é a segunda decisão do V1
2702
- // (`alert_when_missing`, desligada por padrão) e é ela que autoriza a
2703
- // faixa âmbar na frente de quem só queria marcar um horário. Sem este
2704
- // recorte, uma anamnese importada com a segunda chave desligada — como a
2705
- // casa já a tinha no V1 — abria "1 item pendente" em todo agendamento.
2706
- //
2707
- // Em dia continua aparecendo de qualquer forma: a linha verde é
2708
- // confirmação, não interrupção, e é ela que faz a âmbar ter crédito.
2709
- ...(rows ?? []).filter((r) => r.alertWhenMissing || !isOutstanding(r)).map((r) => {
2710
- const pendente = isOutstanding(r);
2711
- return {
2712
- id: r.templateId,
2713
- label: r.templateName,
2714
- state: !pendente ? "done" : r.state === "expired" ? "late" : "todo",
2715
- detail: !pendente ? r.validUntil ? `em dia, v\xE1lido at\xE9 ${DATE(r.validUntil)}` : "em dia" : r.state === "expired" && r.validUntil ? `venceu em ${DATE(r.validUntil)}` : "nunca preenchido",
2716
- action: pendente && onFill ? { label: "Preencher agora", onClick: () => onFill(r.templateId) } : void 0
2717
- };
2718
- }),
2719
- ...extras ?? []
2720
- ], [rows, extras, onFill]);
2721
- const peso = (i) => i.state === "late" ? 0 : i.state === "todo" ? 1 : 2;
2722
- itens.sort((a, b) => peso(a) - peso(b));
2723
- const pendentes = itens.filter((i) => i.state !== "done");
2724
- const faltando = pendentes.length;
2725
- const chave = pendentes.map((i) => i.label).join("|");
2726
- React12.useEffect(() => {
2727
- onPending?.(chave ? chave.split("|") : []);
2728
- }, [chave]);
2729
- if (rows === null) return /* @__PURE__ */ jsx(Skeleton, { className: `h-12 w-full ${className ?? ""}` });
2730
- if (itens.length === 0) return null;
2731
- return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-lg border ${faltando ? "border-warning/40 bg-warning/5" : "border-success/30 bg-success/5"} ${className ?? ""}`, children: [
2732
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-inherit px-3 py-1.5 text-[11px] font-medium text-foreground", children: [
2733
- /* @__PURE__ */ jsx(ClipboardList, { className: `h-3.5 w-3.5 ${faltando ? "text-warning" : "text-success"}`, "aria-hidden": true }),
2734
- faltando === 0 ? "Checagem do paciente \u2014 tudo em dia" : faltando === 1 ? "1 item pendente com o paciente" : `${faltando} itens pendentes com o paciente`,
2735
- sendLink && /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
2736
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
2737
- "button",
2738
- {
2739
- type: "button",
2740
- "aria-label": "Enviar link ao paciente pelo WhatsApp",
2741
- "aria-disabled": sendLink === "soon",
2742
- onClick: sendLink === "soon" ? void 0 : sendLink,
2743
- className: `ml-auto inline-flex h-6 w-6 items-center justify-center rounded-md transition-colors ${sendLink === "soon" ? "cursor-not-allowed text-muted-foreground/60" : "text-foreground hover:bg-muted"}`,
2744
- children: /* @__PURE__ */ jsx(QrCode, { className: "h-3.5 w-3.5", "aria-hidden": true })
2745
- }
2746
- ) }),
2747
- /* @__PURE__ */ jsx(TooltipContent, { side: "left", children: sendLink === "soon" ? "Enviar pelo WhatsApp (QR Code) \u2014 em breve" : "Enviar pelo WhatsApp (QR Code)" })
2748
- ] }) })
2749
- ] }),
2750
- /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/60", children: itens.map((i) => /* @__PURE__ */ jsx(Linha, { item: i }, i.id)) })
2751
- ] });
2752
- }
2753
- function recommendationsOf(template, data) {
2754
- const by = /* @__PURE__ */ new Map();
2755
- for (const f of template.schema.fields) {
2756
- if (f.type !== "recommendation" || !Array.isArray(data[f.id])) continue;
2757
- for (const l of data[f.id]) {
2758
- const cur = by.get(l.serviceId);
2759
- by.set(l.serviceId, cur ? { ...cur, quantity: cur.quantity + l.quantity } : { ...l });
2760
- }
2761
- }
2762
- return [...by.values()];
2763
- }
2764
- function FormFillDialog({
2765
- templateId,
2766
- personId,
2767
- personName,
2768
- onSaved,
2769
- onClose,
2770
- frame = "modal",
2771
- subject,
2772
- productId,
2773
- professionalId,
2774
- recommendationShortcuts
2775
- }) {
2776
- const provider = React12.useMemo(() => createSafeDataProvider(
2777
- () => createSupabaseFormsProvider(),
2778
- () => createMockFormsProvider()
2779
- ), []);
2780
- const store = React12.useMemo(() => createCustomFormsStore(provider), [provider]);
2781
- const config = React12.useMemo(() => resolveConfig(), []);
2782
- const [template, setTemplate] = React12.useState(null);
2783
- const [erro, setErro] = React12.useState(null);
2784
- const [data, setData] = React12.useState({});
2785
- const [salvando, setSalvando] = React12.useState(false);
2786
- const [missing, setMissing] = React12.useState(/* @__PURE__ */ new Set());
2787
- React12.useEffect(() => {
2788
- if (!template || missing.size === 0) return;
2789
- const still = new Set([...missing].filter((id) => {
2790
- const f = template.schema.fields.find((x) => x.id === id);
2791
- return f ? isFieldEmpty(f, data[id]) : false;
2792
- }));
2793
- if (still.size !== missing.size) setMissing(still);
2794
- if (still.size === 0) setErro(null);
2795
- }, [data, template, missing]);
2796
- async function salvar() {
2797
- if (!template) return;
2798
- const faltando = template.schema.fields.filter((f) => f.required && isFieldEmpty(f, data[f.id]));
2799
- if (faltando.length) {
2800
- setMissing(new Set(faltando.map((f) => f.id)));
2801
- const primeiro = [...faltando].sort((a, b) => a.row - b.row || a.col - b.col)[0];
2802
- requestAnimationFrame(() => document.getElementById(`ff-field-${primeiro.id}`)?.scrollIntoView({ block: "center" }));
2803
- setErro(faltando.length === 1 ? `Preencha o campo obrigat\xF3rio destacado: ${faltando[0].label}` : `Preencha os ${faltando.length} campos obrigat\xF3rios destacados.`);
2804
- return;
2805
- }
2806
- setErro(null);
2807
- setSalvando(true);
2808
- try {
2809
- const doc = await store.getState().createDocument({
2810
- templateId: template.id,
2811
- personId,
2812
- title: personName ? `${template.name} \u2014 ${personName}` : template.name,
2813
- data,
2814
- status: "completed",
2815
- subjectType: subject?.type,
2816
- subjectId: subject?.id,
2817
- productId: productId ?? void 0,
2818
- professionalId: professionalId ?? void 0
2819
- });
2820
- await onSaved?.({ documentId: doc.id, template, data, recommendations: recommendationsOf(template, data) });
2821
- onClose();
2822
- } catch (e) {
2823
- setErro(describeError(e, "N\xE3o foi poss\xEDvel salvar o formul\xE1rio."));
2824
- } finally {
2825
- setSalvando(false);
2826
- }
2827
- }
2828
- React12.useEffect(() => {
2829
- let vivo = true;
2830
- (async () => {
2831
- try {
2832
- const t = await store.getState().fetchTemplateById(templateId);
2833
- if (vivo) setTemplate(t);
2834
- } catch (e) {
2835
- if (vivo) setErro(describeError(e, "N\xE3o foi poss\xEDvel abrir o formul\xE1rio."));
2836
- }
2837
- })();
2838
- return () => {
2839
- vivo = false;
2840
- };
2841
- }, [templateId, store]);
2842
- const corpo = /* @__PURE__ */ jsxs(Fragment, { children: [
2843
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: [
2844
- erro && /* @__PURE__ */ jsx("p", { role: "alert", className: "mb-3 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: erro }),
2845
- !template ? erro ? null : (
2846
- /* Esqueleto com a FORMA do formulário: três campos e um rótulo. */
2847
- /* @__PURE__ */ jsx("div", { className: "space-y-4", "aria-busy": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2848
- /* @__PURE__ */ jsx("div", { className: "h-3 w-24 animate-pulse rounded bg-muted/70" }),
2849
- /* @__PURE__ */ jsx("div", { className: "h-9 w-full animate-pulse rounded-lg bg-muted" })
2850
- ] }, i)) })
2851
- ) : /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
2852
- FormRenderer,
2853
- {
2854
- schema: template.schema,
2855
- data,
2856
- onChange: setData,
2857
- missing,
2858
- recommendationShortcuts
2859
- }
2860
- ) })
2861
- ] }),
2862
- /* @__PURE__ */ jsx("div", { className: "border-t fayz-glass-divider px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5 shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
2863
- /* @__PURE__ */ jsx(
2864
- "button",
2865
- {
2866
- type: "button",
2867
- onClick: onClose,
2868
- className: "rounded-button px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground",
2869
- children: "Agora n\xE3o"
2870
- }
2871
- ),
2872
- /* @__PURE__ */ jsx(
2873
- "button",
2874
- {
2875
- type: "button",
2876
- onClick: salvar,
2877
- disabled: !template || salvando,
2878
- className: "rounded-button bg-foreground px-3 py-1.5 text-xs font-semibold text-background disabled:opacity-40",
2879
- children: salvando ? "Salvando\u2026" : "Concluir"
2880
- }
2881
- )
2882
- ] }) })
2883
- ] });
2884
- if (frame === "inline") return corpo;
2885
- return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (o) => {
2886
- if (!o) onClose();
2887
- }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "2xl", noPadding: true, className: "gap-0", children: [
2888
- /* @__PURE__ */ jsx(ModalHeader, { className: "border-b px-5 py-3.5", children: /* @__PURE__ */ jsx(ModalTitle, { className: "truncate text-base", children: template?.name ?? "Formul\xE1rio" }) }),
2889
- /* @__PURE__ */ jsx(ModalBody, { className: "p-0", children: corpo })
2890
- ] }) });
2891
- }
2892
-
2893
- // src/lib/contract-render.ts
2894
2154
  var MONTHS = ["janeiro", "fevereiro", "mar\xE7o", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
2895
- var brl = (n) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(n);
2155
+ var brl = (n) => formatCurrency(n);
2896
2156
  var pad = (n) => String(n).padStart(2, "0");
2897
2157
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2898
2158
  function dateBR(value) {
@@ -2913,7 +2173,7 @@ function itemsTable(items, withDiscount) {
2913
2173
  return `<table><thead><tr>${head.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
2914
2174
  }
2915
2175
  function renderContractTags(templateHtml, ctx) {
2916
- const now2 = ctx.now ?? /* @__PURE__ */ new Date();
2176
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
2917
2177
  const c = ctx.client ?? {};
2918
2178
  const u = ctx.unit ?? {};
2919
2179
  const services = ctx.services ?? [];
@@ -2957,9 +2217,9 @@ function renderContractTags(templateHtml, ctx) {
2957
2217
  "Orcamento.ItensAprovados": itemsTable(ctx.approvedItems, true),
2958
2218
  "Indicacoes.Itens": itemsTable(ctx.recommendedItems, false),
2959
2219
  "Indicacoes.Lista": (ctx.recommendedItems ?? []).map((i) => esc(i.description)).join(", "),
2960
- "Sistema.Data": `${pad(now2.getDate())}/${pad(now2.getMonth() + 1)}/${now2.getFullYear()}`,
2961
- "Sistema.DataExtenso": `${pad(now2.getDate())} de ${MONTHS[now2.getMonth()]} de ${now2.getFullYear()}`,
2962
- "Sistema.Hora": `${pad(now2.getHours())}:${pad(now2.getMinutes())}`
2220
+ "Sistema.Data": `${pad(now.getDate())}/${pad(now.getMonth() + 1)}/${now.getFullYear()}`,
2221
+ "Sistema.DataExtenso": `${pad(now.getDate())} de ${MONTHS[now.getMonth()]} de ${now.getFullYear()}`,
2222
+ "Sistema.Hora": `${pad(now.getHours())}:${pad(now.getMinutes())}`
2963
2223
  };
2964
2224
  const built = /* @__PURE__ */ new Set([
2965
2225
  "Orcamento.Itens",
@@ -2983,7 +2243,7 @@ var db = () => {
2983
2243
  if (!c) throw new Error("Supabase not initialized");
2984
2244
  return c;
2985
2245
  };
2986
- var tenantId = () => getFormsTenantId() ?? getActiveTenantId() ?? "";
2246
+ var tenantId = () => getActiveTenantId() ?? getActiveTenantId() ?? "";
2987
2247
  var BUCKET = "attachments";
2988
2248
  function toLine(s) {
2989
2249
  const total = s.unitPrice * s.quantity - (s.discount ?? 0);
@@ -3188,8 +2448,8 @@ var CONTRACT_TEXT_CLASS = [
3188
2448
  "[&_th]:border [&_th]:border-border [&_th]:bg-muted/50 [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:font-medium"
3189
2449
  ].join(" ");
3190
2450
  function useSigned(path) {
3191
- const [url, setUrl] = React12.useState(null);
3192
- React12.useEffect(() => {
2451
+ const [url, setUrl] = React13.useState(null);
2452
+ React13.useEffect(() => {
3193
2453
  let alive = true;
3194
2454
  contractMediaUrl(path).then((u) => {
3195
2455
  if (alive) setUrl(u);
@@ -3202,9 +2462,9 @@ function useSigned(path) {
3202
2462
  return url;
3203
2463
  }
3204
2464
  function SignaturePad({ onChange }) {
3205
- const ref = React12.useRef(null);
3206
- const drawing = React12.useRef(false);
3207
- React12.useEffect(() => {
2465
+ const ref = React13.useRef(null);
2466
+ const drawing = React13.useRef(false);
2467
+ React13.useEffect(() => {
3208
2468
  const c = ref.current;
3209
2469
  if (!c) return;
3210
2470
  const r = window.devicePixelRatio || 1;
@@ -3249,10 +2509,12 @@ function SignaturePad({ onChange }) {
3249
2509
  }
3250
2510
  ),
3251
2511
  /* @__PURE__ */ jsx(
3252
- "button",
2512
+ Button,
3253
2513
  {
2514
+ variant: "bare",
2515
+ size: "bare",
3254
2516
  type: "button",
3255
- className: "text-xs text-muted-foreground hover:text-foreground",
2517
+ className: "font-normal text-xs text-muted-foreground hover:text-foreground",
3256
2518
  onClick: () => {
3257
2519
  const c = ref.current;
3258
2520
  if (c) {
@@ -3266,17 +2528,17 @@ function SignaturePad({ onChange }) {
3266
2528
  ] });
3267
2529
  }
3268
2530
  function SelfieCapture({ value, onChange }) {
3269
- const video = React12.useRef(null);
3270
- const stream = React12.useRef(null);
3271
- const [on, setOn] = React12.useState(false);
3272
- const [erro, setErro] = React12.useState(null);
3273
- const preview = React12.useMemo(() => value ? URL.createObjectURL(value) : null, [value]);
2531
+ const video = React13.useRef(null);
2532
+ const stream = React13.useRef(null);
2533
+ const [on, setOn] = React13.useState(false);
2534
+ const [erro, setErro] = React13.useState(null);
2535
+ const preview = React13.useMemo(() => value ? URL.createObjectURL(value) : null, [value]);
3274
2536
  const stop = () => {
3275
2537
  stream.current?.getTracks().forEach((t) => t.stop());
3276
2538
  stream.current = null;
3277
2539
  setOn(false);
3278
2540
  };
3279
- React12.useEffect(() => () => stop(), []);
2541
+ React13.useEffect(() => () => stop(), []);
3280
2542
  async function start() {
3281
2543
  setErro(null);
3282
2544
  try {
@@ -3310,12 +2572,14 @@ function SelfieCapture({ value, onChange }) {
3310
2572
  preview ? /* @__PURE__ */ jsxs("div", { className: "relative w-40", children: [
3311
2573
  /* @__PURE__ */ jsx("img", { src: preview, alt: "Selfie", className: "rounded-lg border" }),
3312
2574
  /* @__PURE__ */ jsx(
3313
- "button",
2575
+ Button,
3314
2576
  {
2577
+ variant: "bare",
2578
+ size: "bare",
3315
2579
  type: "button",
3316
2580
  "aria-label": "Remover selfie",
3317
2581
  onClick: () => onChange(null),
3318
- className: "absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-background/90",
2582
+ className: "font-normal absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-background/90",
3319
2583
  children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
3320
2584
  }
3321
2585
  )
@@ -3349,16 +2613,16 @@ function printContract(contract, signatureUrl) {
3349
2613
  setTimeout(() => w.print(), 300);
3350
2614
  }
3351
2615
  function ContractSignPane({ contract, defaultSignerName, onSigned, onClose }) {
3352
- const [signer, setSigner] = React12.useState(defaultSignerName ?? "");
3353
- const [signature, setSignature] = React12.useState(null);
3354
- const [selfie, setSelfie] = React12.useState(null);
3355
- const [saving, setSaving] = React12.useState(false);
3356
- const [erro, setErro] = React12.useState(null);
2616
+ const [signer, setSigner] = React13.useState(defaultSignerName ?? "");
2617
+ const [signature, setSignature] = React13.useState(null);
2618
+ const [selfie, setSelfie] = React13.useState(null);
2619
+ const [saving, setSaving] = React13.useState(false);
2620
+ const [erro, setErro] = React13.useState(null);
3357
2621
  const signatureUrl = useSigned(contract.signaturePath);
3358
2622
  const selfieUrl = useSigned(contract.selfiePath);
3359
2623
  const signed = contract.status === "signed";
3360
- const html = React12.useMemo(() => sanitizeHtml(contract.html), [contract.html]);
3361
- React12.useEffect(() => {
2624
+ const html = React13.useMemo(() => sanitizeHtml(contract.html), [contract.html]);
2625
+ React13.useEffect(() => {
3362
2626
  setErro(null);
3363
2627
  }, [signature, selfie, signer]);
3364
2628
  const missing = contract.requireSignature && !signature ? "Desenhe a assinatura para confirmar." : contract.requireSelfie && !selfie ? "Tire a selfie para confirmar." : !signer.trim() ? "Informe o nome de quem assina." : null;
@@ -3406,8 +2670,10 @@ function ContractSignPane({ contract, defaultSignerName, onSigned, onClose }) {
3406
2670
  ] }),
3407
2671
  /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t fayz-glass-divider px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
3408
2672
  /* @__PURE__ */ jsx(
3409
- "button",
2673
+ Button,
3410
2674
  {
2675
+ variant: "bare",
2676
+ size: "bare",
3411
2677
  type: "button",
3412
2678
  onClick: onClose,
3413
2679
  className: "rounded-button px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground",
@@ -3415,8 +2681,10 @@ function ContractSignPane({ contract, defaultSignerName, onSigned, onClose }) {
3415
2681
  }
3416
2682
  ),
3417
2683
  !signed && /* @__PURE__ */ jsx(
3418
- "button",
2684
+ Button,
3419
2685
  {
2686
+ variant: "bare",
2687
+ size: "bare",
3420
2688
  type: "button",
3421
2689
  onClick: sign,
3422
2690
  disabled: saving,
@@ -3427,6 +2695,22 @@ function ContractSignPane({ contract, defaultSignerName, onSigned, onClose }) {
3427
2695
  ] }) })
3428
2696
  ] });
3429
2697
  }
2698
+ var formsV1SettingsTabs = [
2699
+ defineSettingsTab({
2700
+ id: "forms-contract-rules",
2701
+ label: tl("Contract rules", "Regras de contrato"),
2702
+ icon: "FileCheck2",
2703
+ section: "registry",
2704
+ pending: true,
2705
+ defaults: { "contracts.execution_requires_signed_contract": false, "contracts.prepaid_purchase_flow": false },
2706
+ sections: [{
2707
+ fields: [
2708
+ { key: "contracts.execution_requires_signed_contract", type: "boolean", label: tl("A service runs only with the contract signed", "Executar servi\xE7o s\xF3 com contrato assinado") },
2709
+ { key: "contracts.prepaid_purchase_flow", type: "boolean", label: tl("Prepaid purchase flow", "Fluxo de compra pr\xE9-paga") }
2710
+ ]
2711
+ }]
2712
+ })
2713
+ ];
3430
2714
 
3431
2715
  // src/index.ts
3432
2716
  function registerBuiltInProviders(formsProvider) {
@@ -3466,11 +2750,8 @@ function registerBuiltInProviders(formsProvider) {
3466
2750
  function createCustomFormsPlugin(options) {
3467
2751
  registerTranslations(customFormsLocales);
3468
2752
  const config = resolveConfig(options);
3469
- const provider = options?.dataProvider ?? createSafeDataProvider(
3470
- () => createSupabaseFormsProvider(),
3471
- () => createMockFormsProvider()
3472
- );
3473
- const store = createCustomFormsStore(provider);
2753
+ const provider = options?.dataProvider ?? createSafeFormsProvider();
2754
+ const store = createFormsStore(provider);
3474
2755
  registerBuiltInProviders(provider);
3475
2756
  registerFormsTimelineSource();
3476
2757
  registerFormsTabCounts();
@@ -3584,14 +2865,14 @@ function createCustomFormsPlugin(options) {
3584
2865
  }
3585
2866
  ];
3586
2867
  const registries = [...formRegistries, ...options?.settingsRegistries ?? []];
3587
- const SettingsComponent = () => React12.createElement(PluginSettingsPanel, {
2868
+ const SettingsComponent = () => React13.createElement(PluginSettingsPanel, {
3588
2869
  title: config.labels.settingsLabel,
3589
2870
  customTabs: [
3590
2871
  {
3591
2872
  id: "forms",
3592
2873
  label: config.labels.pageTitle,
3593
2874
  icon: "FileText",
3594
- content: React12.createElement(CustomFormsSettingsTab, { config, provider, store })
2875
+ content: React13.createElement(CustomFormsSettingsTab, { config, provider, store })
3595
2876
  }
3596
2877
  ],
3597
2878
  registries,
@@ -3599,11 +2880,11 @@ function createCustomFormsPlugin(options) {
3599
2880
  hostPluginId: "custom_forms"
3600
2881
  });
3601
2882
  SettingsComponent.displayName = "CustomFormsSettingsPanel";
3602
- const PersonDocumentsWidget2 = (props) => React12.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "records" });
2883
+ const PersonDocumentsWidget2 = (props) => React13.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "records" });
3603
2884
  PersonDocumentsWidget2.displayName = "PersonDocumentsWidget";
3604
- const PersonFilesWidget = (props) => React12.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "files" });
2885
+ const PersonFilesWidget = (props) => React13.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "files" });
3605
2886
  PersonFilesWidget.displayName = "PersonFilesWidget";
3606
- const PersonAnswerAlertsWidget = (props) => React12.createElement(PersonAnswerAlerts, { personId: props.item?.id, provider });
2887
+ const PersonAnswerAlertsWidget = (props) => React13.createElement(PersonAnswerAlerts, { personId: props.item?.id, provider });
3607
2888
  PersonAnswerAlertsWidget.displayName = "PersonAnswerAlertsWidget";
3608
2889
  return {
3609
2890
  id: "custom_forms",
@@ -3629,6 +2910,7 @@ function createCustomFormsPlugin(options) {
3629
2910
  navigation: [],
3630
2911
  routes: [],
3631
2912
  settings: [
2913
+ ...formsV1SettingsTabs,
3632
2914
  {
3633
2915
  id: "custom_forms",
3634
2916
  label: config.labels.settingsLabel,
@@ -3650,6 +2932,7 @@ function createCustomFormsPlugin(options) {
3650
2932
  component: PersonFilesWidget,
3651
2933
  order: 0
3652
2934
  },
2935
+ ...formsZoneWidgets(provider),
3653
2936
  {
3654
2937
  // Renders nothing unless a form marks an answer as an alert (V1 alert_config).
3655
2938
  id: "person-answer-alerts",
@@ -3722,6 +3005,6 @@ function createCustomFormsPlugin(options) {
3722
3005
  };
3723
3006
  }
3724
3007
 
3725
- export { ContractSignPane, FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, ensureAttendanceContracts, getPersonFormRequirements, isOutstanding, listAttendanceContracts, recommendationsOf, registerDocumentTypeProvider };
3008
+ export { ContractSignPane, FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, ensureAttendanceContracts, listAttendanceContracts, recommendationsOf, registerDocumentTypeProvider };
3726
3009
  //# sourceMappingURL=index.js.map
3727
3010
  //# sourceMappingURL=index.js.map