@fayz-ai/plugin-forms 0.13.5 → 0.14.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 (53) hide show
  1. package/dist/{FormBuilder-AO6V2YOT.js → FormBuilder-N4R5SENC.js} +161 -15
  2. package/dist/FormBuilder-N4R5SENC.js.map +1 -0
  3. package/dist/chunk-2DFNG5UT.js +1661 -0
  4. package/dist/chunk-2DFNG5UT.js.map +1 -0
  5. package/dist/components/ContractSignPane.d.ts +12 -0
  6. package/dist/components/ContractSignPane.d.ts.map +1 -0
  7. package/dist/components/DocumentPreview.d.ts.map +1 -1
  8. package/dist/components/FieldConfigDialog.d.ts.map +1 -1
  9. package/dist/components/FieldPalette.d.ts.map +1 -1
  10. package/dist/components/FormFillDialog.d.ts +22 -2
  11. package/dist/components/FormFillDialog.d.ts.map +1 -1
  12. package/dist/components/FormRenderer.d.ts +9 -2
  13. package/dist/components/FormRenderer.d.ts.map +1 -1
  14. package/dist/components/PersonAnswerAlerts.d.ts +12 -0
  15. package/dist/components/PersonAnswerAlerts.d.ts.map +1 -0
  16. package/dist/components/PersonFormsAlert.d.ts +6 -1
  17. package/dist/components/PersonFormsAlert.d.ts.map +1 -1
  18. package/dist/components/fields/FieldTypeSettings.d.ts +11 -0
  19. package/dist/components/fields/FieldTypeSettings.d.ts.map +1 -0
  20. package/dist/components/fields/StencilCamera.d.ts +19 -0
  21. package/dist/components/fields/StencilCamera.d.ts.map +1 -0
  22. package/dist/components/fields/fields.d.ts +59 -0
  23. package/dist/components/fields/fields.d.ts.map +1 -0
  24. package/dist/components/fields/media.d.ts +18 -0
  25. package/dist/components/fields/media.d.ts.map +1 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/context.d.ts +2 -0
  28. package/dist/context.d.ts.map +1 -1
  29. package/dist/data/mock.d.ts.map +1 -1
  30. package/dist/data/supabase.d.ts.map +1 -1
  31. package/dist/data/types.d.ts +27 -0
  32. package/dist/data/types.d.ts.map +1 -1
  33. package/dist/index.d.ts +5 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +969 -134
  36. package/dist/index.js.map +1 -1
  37. package/dist/lib/attendance-contracts.d.ts +61 -0
  38. package/dist/lib/attendance-contracts.d.ts.map +1 -0
  39. package/dist/lib/categories.d.ts +1 -1
  40. package/dist/lib/categories.d.ts.map +1 -1
  41. package/dist/lib/contract-render.d.ts +57 -0
  42. package/dist/lib/contract-render.d.ts.map +1 -0
  43. package/dist/lib/print.d.ts.map +1 -1
  44. package/dist/locales/en.d.ts.map +1 -1
  45. package/dist/locales/pt-BR.d.ts.map +1 -1
  46. package/dist/types.d.ts +52 -3
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
  49. package/dist/views/TemplateListView.d.ts.map +1 -1
  50. package/package.json +4 -4
  51. package/dist/FormBuilder-AO6V2YOT.js.map +0 -1
  52. package/dist/chunk-2AIKIPGU.js +0 -695
  53. package/dist/chunk-2AIKIPGU.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { FormRenderer, createOrUpdateFormTool, prefillFromPerson, printDocument, htmlToPlainText } from './chunk-2AIKIPGU.js';
2
- import React11, { createContext, useState, useCallback, useRef, useMemo, useEffect } from 'react';
3
- import { tl, createSafeDataProvider, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, useTranslation, getActiveTenantId, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
1
+ import { isFieldEmpty, CustomFormsProvider, FormRenderer, sanitizeHtml, createOrUpdateFormTool, alertOf, answerAlerts, yesNoAnswer, prefillFromPerson, printDocument, htmlToPlainText, isMedia } from './chunk-2DFNG5UT.js';
2
+ import React12, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
3
+ import { tl, createSafeDataProvider, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, getActiveTenantId, useTranslation, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
4
4
  import { getFayzSupabaseClient, useOrganizationStore, PluginSettingsPanel, registerTimelineSource, registerTabCountProvider, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions, invalidateTabCounts, TimelineList } from '@fayz-ai/admin';
5
5
  import { create } from 'zustand';
6
- import { ClipboardList, CheckCircle2, Clock, Circle, FileText, FileSignature, FileCheck2, Activity, Image, Paperclip, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft, Printer, ChevronDown, EyeOff, Eye, Upload } from 'lucide-react';
7
- import { Skeleton, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Badge, Switch, RowActions, Button, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, ConfirmDialog } from '@fayz-ai/ui';
6
+ 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
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
 
10
10
  // src/data/tables.ts
@@ -205,6 +205,15 @@ function createMockFormsProvider() {
205
205
  files.push(docFile);
206
206
  return docFile;
207
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
+ },
208
217
  async deleteFile(fileId) {
209
218
  const idx = files.findIndex((f) => f.id === fileId);
210
219
  if (idx !== -1) files.splice(idx, 1);
@@ -222,10 +231,13 @@ function getClient() {
222
231
  if (!supabase) throw new Error("Supabase not initialized");
223
232
  return supabase;
224
233
  }
234
+ var MEDIA_BUCKET = "attachments";
235
+ var SIGNED_TTL_S = 3600;
236
+ var signedCache = /* @__PURE__ */ new Map();
225
237
  var USAGE_FALLBACK_LIMIT = 5e3;
226
- async function countTemplateUsageClientSide(db, tenantId) {
238
+ async function countTemplateUsageClientSide(db2, tenantId2) {
227
239
  try {
228
- const { data, error, count } = await db.from("v_documents").select("template_id, created_at", { count: "exact" }).eq("tenant_id", tenantId).eq("is_active", true).not("template_id", "is", null).limit(USAGE_FALLBACK_LIMIT);
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);
229
241
  if (error) return {};
230
242
  if (typeof count === "number" && count > USAGE_FALLBACK_LIMIT) return {};
231
243
  const usage = {};
@@ -315,8 +327,8 @@ function createSupabaseFormsProvider() {
315
327
  return {
316
328
  // ── Templates ──────────────────────────────────────────
317
329
  async getTemplates(query) {
318
- const db = getClient();
319
- let qb = db.from(T.templates).select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_current", true).eq("is_deleted", false).eq("is_active", true);
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);
320
332
  if (query.category) qb = qb.eq("category", query.category);
321
333
  if (query.search) qb = qb.ilike("name", `%${query.search}%`);
322
334
  const page = query.page ?? 1;
@@ -327,16 +339,16 @@ function createSupabaseFormsProvider() {
327
339
  return { data: (data ?? []).map(mapTemplate), total: count ?? 0 };
328
340
  },
329
341
  async getTemplateById(id) {
330
- const db = getClient();
331
- const { data, error } = await db.from(T.templates).select("*").eq("id", id).eq("is_deleted", false).single();
342
+ const db2 = getClient();
343
+ const { data, error } = await db2.from(T.templates).select("*").eq("id", id).eq("is_deleted", false).single();
332
344
  if (error || !data) return null;
333
345
  return mapTemplate(data);
334
346
  },
335
347
  async createTemplate(input) {
336
- const db = getClient();
337
- const tenantId = getTenantId();
338
- const { data, error } = await db.from(T.templates).insert({
339
- tenant_id: tenantId,
348
+ const db2 = getClient();
349
+ const tenantId2 = getTenantId();
350
+ const { data, error } = await db2.from(T.templates).insert({
351
+ tenant_id: tenantId2,
340
352
  name: input.name,
341
353
  description: input.description,
342
354
  category: input.category,
@@ -349,15 +361,15 @@ function createSupabaseFormsProvider() {
349
361
  return mapTemplate(data);
350
362
  },
351
363
  async updateTemplate(id, input) {
352
- const db = getClient();
353
- const tenantId = getTenantId();
354
- const { count } = await db.from(T.documents).select("document_id", { count: "exact", head: true }).eq("template_id", id);
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);
355
367
  const hasDocuments = (count ?? 0) > 0;
356
368
  if (hasDocuments && input.schema) {
357
369
  const existing = await this.getTemplateById(id);
358
370
  if (!existing) throw new Error("Template not found");
359
- const { data: newRow, error: insertError } = await db.from(T.templates).insert({
360
- tenant_id: tenantId,
371
+ const { data: newRow, error: insertError } = await db2.from(T.templates).insert({
372
+ tenant_id: tenantId2,
361
373
  name: input.name ?? existing.name,
362
374
  description: input.description ?? existing.description,
363
375
  category: input.category ?? existing.category,
@@ -378,7 +390,7 @@ function createSupabaseFormsProvider() {
378
390
  alert_when_missing: input.alertWhenMissing ?? existing.alertWhenMissing
379
391
  }).select().single();
380
392
  if (insertError) throw new Error(insertError.message);
381
- await db.from(T.templates).update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
393
+ await db2.from(T.templates).update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
382
394
  return mapTemplate(newRow);
383
395
  }
384
396
  const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
@@ -393,13 +405,13 @@ function createSupabaseFormsProvider() {
393
405
  if (input.requiredOnAttendance !== void 0) updates.required_on_attendance = input.requiredOnAttendance;
394
406
  if (input.validityDays !== void 0) updates.validity_days = input.validityDays;
395
407
  if (input.alertWhenMissing !== void 0) updates.alert_when_missing = input.alertWhenMissing;
396
- const { data, error } = await db.from(T.templates).update(updates).eq("id", id).select().single();
408
+ const { data, error } = await db2.from(T.templates).update(updates).eq("id", id).select().single();
397
409
  if (error) throw new Error(error.message);
398
410
  return mapTemplate(data);
399
411
  },
400
412
  async deleteTemplate(id) {
401
- const db = getClient();
402
- const { error } = await db.from(T.templates).update({ is_deleted: true, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", 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);
403
415
  if (error) throw new Error(error.message);
404
416
  },
405
417
  // Uso por modelo. Caminho principal: `v_forms_template_usage`, que agrega no
@@ -411,10 +423,10 @@ function createSupabaseFormsProvider() {
411
423
  // propósito — acima do teto o número seria mentira, e mentir sobre uma
412
424
  // contagem é pior do que omiti-la, então ele desiste em silêncio.
413
425
  async getTemplateUsage() {
414
- const db = getClient();
415
- const tenantId = getTenantId();
416
- if (!tenantId) return {};
417
- const { data, error } = await db.from("v_forms_template_usage").select("template_id, document_count, last_document_at").eq("tenant_id", tenantId);
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);
418
430
  if (!error) {
419
431
  const usage = {};
420
432
  for (const row of data ?? []) {
@@ -425,12 +437,12 @@ function createSupabaseFormsProvider() {
425
437
  }
426
438
  return usage;
427
439
  }
428
- return countTemplateUsageClientSide(db, tenantId);
440
+ return countTemplateUsageClientSide(db2, tenantId2);
429
441
  },
430
442
  // ── Documents (public.documents + plg_forms_documents extension) ──
431
443
  async getDocuments(query) {
432
- const db = getClient();
433
- let qb = db.from("v_documents").select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_active", true);
444
+ const db2 = getClient();
445
+ let qb = db2.from("v_documents").select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_active", true);
434
446
  if (query.personId) qb = qb.eq("person_id", query.personId);
435
447
  if (query.templateId) qb = qb.eq("template_id", query.templateId);
436
448
  if (query.status) {
@@ -456,8 +468,8 @@ function createSupabaseFormsProvider() {
456
468
  * seriam seis idas para responder uma pergunta.
457
469
  */
458
470
  async getDocumentCounts(query) {
459
- const db = getClient();
460
- let qb = db.from("v_documents").select("kind, template_category").eq("tenant_id", getTenantId()).eq("person_id", query.personId).eq("is_active", true).neq("status", "archived");
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");
461
473
  if (query.kinds?.length) qb = qb.in("kind", query.kinds);
462
474
  const { data, error } = await qb;
463
475
  if (error) return {};
@@ -469,16 +481,16 @@ function createSupabaseFormsProvider() {
469
481
  return counts;
470
482
  },
471
483
  async getDocumentById(id) {
472
- const db = getClient();
473
- const { data, error } = await db.from("v_documents").select("*").eq("id", id).single();
484
+ const db2 = getClient();
485
+ const { data, error } = await db2.from("v_documents").select("*").eq("id", id).single();
474
486
  if (error || !data) return null;
475
487
  return mapDocument(data);
476
488
  },
477
489
  async createDocument(input) {
478
- const tenantId = getTenantId();
490
+ const tenantId2 = getTenantId();
479
491
  const isForm = !!input.templateId;
480
492
  const { data: coreDoc, error: coreError } = await getClient().from("documents").insert({
481
- tenant_id: tenantId,
493
+ tenant_id: tenantId2,
482
494
  kind: input.kind ?? (isForm ? "form" : "attachment"),
483
495
  person_id: input.personId,
484
496
  title: input.title,
@@ -487,16 +499,20 @@ function createSupabaseFormsProvider() {
487
499
  file_name: input.fileName,
488
500
  file_size: input.fileSize,
489
501
  mime_type: input.mimeType,
502
+ subject_type: input.subjectType,
503
+ subject_id: input.subjectId,
490
504
  metadata: {}
491
505
  }).select().single();
492
506
  if (coreError) throw new Error(coreError.message);
493
507
  if (isForm && input.templateId) {
494
- const db = getClient();
495
- const { error: extError } = await db.from(T.documents).insert({
508
+ const db2 = getClient();
509
+ const { error: extError } = await db2.from(T.documents).insert({
496
510
  document_id: coreDoc.id,
497
- tenant_id: tenantId,
511
+ tenant_id: tenantId2,
498
512
  template_id: input.templateId,
499
- data: input.data ?? {}
513
+ data: input.data ?? {},
514
+ product_id: input.productId ?? null,
515
+ professional_id: input.professionalId ?? null
500
516
  });
501
517
  if (extError) {
502
518
  await getClient().from("documents").delete().eq("id", coreDoc.id);
@@ -515,8 +531,8 @@ function createSupabaseFormsProvider() {
515
531
  const { error: coreError } = await getClient().from("documents").update(coreUpdates).eq("id", id);
516
532
  if (coreError) throw new Error(coreError.message);
517
533
  if (input.data !== void 0) {
518
- const db = getClient();
519
- await db.from(T.documents).update({ data: input.data, updated_at: now2 }).eq("document_id", id);
534
+ const db2 = getClient();
535
+ await db2.from(T.documents).update({ data: input.data, updated_at: now2 }).eq("document_id", id);
520
536
  }
521
537
  const doc = await this.getDocumentById(id);
522
538
  return doc;
@@ -527,21 +543,21 @@ function createSupabaseFormsProvider() {
527
543
  },
528
544
  // ── Files ──────────────────────────────────────────────
529
545
  async getDocumentFiles(documentId) {
530
- const db = getClient();
531
- const { data, error } = await db.from(T.documentFiles).select("*").eq("document_id", documentId).order("sort_order");
546
+ const db2 = getClient();
547
+ const { data, error } = await db2.from(T.documentFiles).select("*").eq("document_id", documentId).order("sort_order");
532
548
  if (error) throw new Error(error.message);
533
549
  return (data ?? []).map(mapFile);
534
550
  },
535
551
  async uploadFile(documentId, fieldKey, file) {
536
- const db = getClient();
537
- const tenantId = getTenantId();
552
+ const db2 = getClient();
553
+ const tenantId2 = getTenantId();
538
554
  const ext = file.name.split(".").pop() ?? "bin";
539
- const storagePath = `${tenantId}/forms/${documentId}/${fieldKey}/${crypto.randomUUID()}.${ext}`;
540
- const { error: uploadError } = await db.storage.from("documents").upload(storagePath, file, { contentType: file.type });
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 });
541
557
  if (uploadError) throw new Error(uploadError.message);
542
- const { data: urlData } = db.storage.from("documents").getPublicUrl(storagePath);
543
- const { data, error } = await db.from(T.documentFiles).insert({
544
- tenant_id: tenantId,
558
+ const { data: urlData } = db2.storage.from("documents").getPublicUrl(storagePath);
559
+ const { data, error } = await db2.from(T.documentFiles).insert({
560
+ tenant_id: tenantId2,
545
561
  document_id: documentId,
546
562
  field_key: fieldKey,
547
563
  file_url: urlData.publicUrl,
@@ -552,9 +568,74 @@ function createSupabaseFormsProvider() {
552
568
  if (error) throw new Error(error.message);
553
569
  return mapFile(data);
554
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
+ },
555
636
  async deleteFile(fileId) {
556
- const db = getClient();
557
- const { error } = await db.from(T.documentFiles).delete().eq("id", fileId);
637
+ const db2 = getClient();
638
+ const { error } = await db2.from(T.documentFiles).delete().eq("id", fileId);
558
639
  if (error) throw new Error(error.message);
559
640
  }
560
641
  };
@@ -717,6 +798,8 @@ var en = {
717
798
  "customForms.archiveTemplateConfirm": "Archive this form template? It leaves the list and the document picker, but is kept and existing documents are preserved.",
718
799
  // Categories
719
800
  "customForms.category.anamnesis": "Anamnesis",
801
+ "customForms.category.evaluation": "Evaluation",
802
+ "customForms.category.photos": "Photos",
720
803
  "customForms.category.evolution": "Evolution Note",
721
804
  "customForms.category.report": "Report",
722
805
  "customForms.category.contract": "Contract",
@@ -764,6 +847,11 @@ var en = {
764
847
  "customForms.fieldType.image": "Image / Drawing",
765
848
  "customForms.fieldType.gallery": "Gallery",
766
849
  "customForms.fieldType.budget": "Budget",
850
+ "customForms.fieldType.yes_no": "Yes or No",
851
+ "customForms.fieldType.yes_no_text": "Yes or No + Text",
852
+ "customForms.fieldType.stencil_photo": "Photo with stencil",
853
+ "customForms.fieldType.recommendation": "Recommendations",
854
+ "customForms.fieldType.signature": "Signature (drawn on screen)",
767
855
  // Documents
768
856
  "customForms.documents": "Documents",
769
857
  "customForms.addDocument": "Add Document",
@@ -784,8 +872,8 @@ var en = {
784
872
  "customForms.showContent": "Show content",
785
873
  "customForms.hideContent": "Hide content",
786
874
  "customForms.previewEmpty": "This record has no filled field.",
787
- "customForms.previewMore": "See the other {count} fields",
788
- "customForms.loadMore": "Load {count} more",
875
+ "customForms.previewMore": "See the other {{count}} fields",
876
+ "customForms.loadMore": "Load {{count}} more",
789
877
  "customForms.loadingMore": "Loading\u2026",
790
878
  "customForms.noRecords": "No records yet",
791
879
  "customForms.noRecordsDescription": 'Anamnesis, evolution, report or contract \u2014 start with "New".',
@@ -799,7 +887,7 @@ var en = {
799
887
  "customForms.deleteDocumentConfirm": "Delete this document?",
800
888
  "customForms.archive": "Archive",
801
889
  "customForms.archiveTitle": "Archive record",
802
- "customForms.archiveDescription": "{name} leaves this record's list but stays stored \u2014 archiving does not delete.",
890
+ "customForms.archiveDescription": "{{name}} leaves this record's list but stays stored \u2014 archiving does not delete.",
803
891
  "customForms.archiveDocumentConfirm": "Archive this document? It leaves the active list but is kept on file.",
804
892
  "customForms.printPdf": "Print / PDF",
805
893
  "customForms.usageCount": "{{count}} filled in",
@@ -847,6 +935,8 @@ var ptBR = {
847
935
  "customForms.archiveTemplateConfirm": "Arquivar este modelo? Ele sai da lista e do seletor de documentos, mas fica guardado e os documentos existentes s\xE3o preservados.",
848
936
  // Categories
849
937
  "customForms.category.anamnesis": "Anamnese",
938
+ "customForms.category.evaluation": "Avalia\xE7\xE3o",
939
+ "customForms.category.photos": "Fotos",
850
940
  "customForms.category.evolution": "Evolu\xE7\xE3o",
851
941
  "customForms.category.report": "Laudo",
852
942
  "customForms.category.contract": "Contrato",
@@ -894,6 +984,11 @@ var ptBR = {
894
984
  "customForms.fieldType.image": "Imagem / Desenho",
895
985
  "customForms.fieldType.gallery": "Galeria",
896
986
  "customForms.fieldType.budget": "Or\xE7amento",
987
+ "customForms.fieldType.yes_no": "Sim ou N\xE3o",
988
+ "customForms.fieldType.yes_no_text": "Sim ou N\xE3o + Texto",
989
+ "customForms.fieldType.stencil_photo": "Foto com Gabarito",
990
+ "customForms.fieldType.recommendation": "Indica\xE7\xF5es",
991
+ "customForms.fieldType.signature": "Assinatura (escrita em tela)",
897
992
  // Documents
898
993
  "customForms.documents": "Documentos",
899
994
  "customForms.addDocument": "Novo Documento",
@@ -917,8 +1012,8 @@ var ptBR = {
917
1012
  "customForms.showContent": "Mostrar conte\xFAdo",
918
1013
  "customForms.hideContent": "Ocultar conte\xFAdo",
919
1014
  "customForms.previewEmpty": "Registro sem nenhum campo preenchido.",
920
- "customForms.previewMore": "Ver os outros {count} campos",
921
- "customForms.loadMore": "Carregar mais {count}",
1015
+ "customForms.previewMore": "Ver os outros {{count}} campos",
1016
+ "customForms.loadMore": "Carregar mais {{count}}",
922
1017
  "customForms.loadingMore": "Carregando\u2026",
923
1018
  "customForms.noRecords": "Nenhum registro",
924
1019
  "customForms.noRecordsDescription": 'Anamnese, evolu\xE7\xE3o, laudo ou contrato \u2014 comece em "Novo".',
@@ -935,7 +1030,7 @@ var ptBR = {
935
1030
  // O diálogo diz DE QUAL registro se trata: "Tem certeza?" sozinho é a
936
1031
  // pergunta que ninguém consegue responder.
937
1032
  "customForms.archiveTitle": "Arquivar registro",
938
- "customForms.archiveDescription": "{name} sai da lista desta ficha, mas continua guardado \u2014 arquivar n\xE3o apaga.",
1033
+ "customForms.archiveDescription": "{{name}} sai da lista desta ficha, mas continua guardado \u2014 arquivar n\xE3o apaga.",
939
1034
  "customForms.printPdf": "Imprimir / PDF",
940
1035
  "customForms.usageCount": "{{count}} preenchidos",
941
1036
  "customForms.usageLast": "\xFAltimo em {{date}}",
@@ -958,24 +1053,24 @@ var customFormsLocales = {
958
1053
  en,
959
1054
  "pt-BR": ptBR
960
1055
  };
961
-
962
- // src/config.ts
963
- var DEFAULT_LABELS = {
964
- pageTitle: "Custom Forms",
965
- settingsLabel: "Forms & Documents",
966
- settingsSubtitle: "Create and manage custom forms for your business",
967
- templates: "Templates",
968
- documents: "Documents",
969
- newTemplate: "New Template",
970
- addDocument: "Add Document"
971
- };
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
+ });
972
1065
  function resolveConfig(options) {
973
1066
  return {
974
- labels: { ...DEFAULT_LABELS, ...options?.labels }
1067
+ labels: { ...defaultLabels(), ...options?.labels }
975
1068
  };
976
1069
  }
977
1070
  var CATEGORY_COLORS = {
978
1071
  anamnesis: "bg-info-soft text-info-soft-foreground",
1072
+ evaluation: "bg-info-soft text-info-soft-foreground",
1073
+ photos: "bg-muted text-muted-foreground",
979
1074
  evolution: "bg-success-soft text-success-soft-foreground",
980
1075
  report: "bg-magic-soft text-magic-soft-foreground",
981
1076
  contract: "bg-warning-soft text-warning-soft-foreground",
@@ -1223,7 +1318,7 @@ function CustomFormsSettingsTab({ config, provider, store }) {
1223
1318
  window.location.hash = `#${ROUTE_BASE}/forms`;
1224
1319
  store.getState().fetchTemplates();
1225
1320
  }, [store]);
1226
- React11.useEffect(() => {
1321
+ React12.useEffect(() => {
1227
1322
  const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
1228
1323
  window.addEventListener("hashchange", sync);
1229
1324
  return () => window.removeEventListener("hashchange", sync);
@@ -1254,11 +1349,11 @@ function BuilderWrapper({
1254
1349
  config,
1255
1350
  onBack
1256
1351
  }) {
1257
- const FormBuilderLazy = React11.lazy(
1258
- () => import('./FormBuilder-AO6V2YOT.js').then((m) => ({ default: m.FormBuilder }))
1352
+ const FormBuilderLazy = React12.lazy(
1353
+ () => import('./FormBuilder-N4R5SENC.js').then((m) => ({ default: m.FormBuilder }))
1259
1354
  );
1260
- return /* @__PURE__ */ jsx(
1261
- React11.Suspense,
1355
+ return /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
1356
+ React12.Suspense,
1262
1357
  {
1263
1358
  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" }) }),
1264
1359
  children: /* @__PURE__ */ jsx(
@@ -1272,7 +1367,7 @@ function BuilderWrapper({
1272
1367
  }
1273
1368
  )
1274
1369
  }
1275
- );
1370
+ ) });
1276
1371
  }
1277
1372
  var MAX_FIELDS = 8;
1278
1373
  var MAX_CHARS = 280;
@@ -1287,6 +1382,35 @@ function asText(value) {
1287
1382
  if (typeof value === "boolean") return value ? "Sim" : "N\xE3o";
1288
1383
  return htmlToPlainText(String(value)) ?? String(value);
1289
1384
  }
1385
+ function PreviewMedia({ items, provider }) {
1386
+ const [urls, setUrls] = useState([]);
1387
+ useEffect(() => {
1388
+ let alive = true;
1389
+ Promise.all(items.map((m) => Promise.resolve(provider.mediaUrl?.(m.path) ?? null).catch(() => null))).then((u) => {
1390
+ if (alive) setUrls(u);
1391
+ });
1392
+ return () => {
1393
+ alive = false;
1394
+ };
1395
+ }, [items.map((m) => m.path).join("|"), provider]);
1396
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 pt-1", children: items.map((m, i) => urls[i] ? m.type === "video" ? /* @__PURE__ */ jsx("video", { src: urls[i], className: "h-20 w-20 rounded-md border object-cover", preload: "metadata" }, m.path) : /* @__PURE__ */ jsx("img", { src: urls[i], alt: "", className: `h-20 rounded-md border object-cover ${m.type === "signature" ? "bg-white" : "w-16"}` }, m.path) : /* @__PURE__ */ jsx("div", { className: "h-20 w-16 animate-pulse rounded-md bg-muted" }, m.path)) });
1397
+ }
1398
+ function mediaOf(field, value) {
1399
+ if (field.type === "stencil_photo" || field.type === "signature") return isMedia(value) ? [value] : null;
1400
+ if (field.type === "gallery") return Array.isArray(value) ? value.filter(isMedia) : null;
1401
+ return null;
1402
+ }
1403
+ function fieldText(field, value) {
1404
+ if (field.type === "yes_no" || field.type === "yes_no_text") {
1405
+ const a = yesNoAnswer(value);
1406
+ const extra = value && typeof value === "object" ? value.text?.trim() : "";
1407
+ return `${a === "sim" ? "Sim" : a === "nao" ? "N\xE3o" : ""}${extra ? ` \u2014 ${extra}` : ""}`;
1408
+ }
1409
+ if (field.type === "recommendation" && Array.isArray(value)) {
1410
+ return value.map((l) => `${l.quantity > 1 ? `${l.quantity}\xD7 ` : ""}${l.name}`).join(" \xB7 ");
1411
+ }
1412
+ return asText(value);
1413
+ }
1290
1414
  function truncate(text) {
1291
1415
  return text.length > MAX_CHARS ? `${text.slice(0, MAX_CHARS).trimEnd()}\u2026` : text;
1292
1416
  }
@@ -1347,11 +1471,12 @@ function DocumentPreview({
1347
1471
  const hidden = answered.length - shown.length;
1348
1472
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1349
1473
  /* @__PURE__ */ jsx("dl", { className: "grid gap-x-6 gap-y-2 sm:grid-cols-2", children: shown.map((field) => {
1350
- const text = truncate(asText(doc.data[field.id]));
1351
- const wide = field.type === "memo" || field.type === "richtext" || text.length > 60;
1474
+ const media = mediaOf(field, doc.data[field.id]);
1475
+ const text = media ? "" : truncate(fieldText(field, doc.data[field.id]));
1476
+ const wide = field.type === "memo" || field.type === "richtext" || field.type === "gallery" || text.length > 60;
1352
1477
  return /* @__PURE__ */ jsxs("div", { className: `min-w-0 ${wide ? "sm:col-span-2" : ""}`, children: [
1353
1478
  /* @__PURE__ */ jsx("dt", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: htmlToPlainText(field.label) ?? field.label }),
1354
- /* @__PURE__ */ jsx("dd", { className: "whitespace-pre-wrap text-sm leading-6 text-foreground", children: text })
1479
+ /* @__PURE__ */ jsx("dd", { className: "whitespace-pre-wrap text-sm leading-6 text-foreground", children: media ? /* @__PURE__ */ jsx(PreviewMedia, { items: media, provider }) : text })
1355
1480
  ] }, field.id);
1356
1481
  }) }),
1357
1482
  hidden > 0 && /* @__PURE__ */ jsx(
@@ -1367,6 +1492,8 @@ function DocumentPreview({
1367
1492
  }
1368
1493
  var CATEGORY_ORDER = [
1369
1494
  "anamnesis",
1495
+ "evaluation",
1496
+ "photos",
1370
1497
  "evolution",
1371
1498
  "report",
1372
1499
  "contract",
@@ -1374,6 +1501,8 @@ var CATEGORY_ORDER = [
1374
1501
  ];
1375
1502
  var CATEGORY_GROUP_ORDER = {
1376
1503
  anamnesis: 10,
1504
+ evaluation: 12,
1505
+ photos: 14,
1377
1506
  evolution: 20,
1378
1507
  report: 30,
1379
1508
  contract: 40,
@@ -1381,6 +1510,8 @@ var CATEGORY_GROUP_ORDER = {
1381
1510
  };
1382
1511
  var CATEGORY_ICONS = {
1383
1512
  anamnesis: ClipboardList,
1513
+ evaluation: Stethoscope,
1514
+ photos: Camera,
1384
1515
  evolution: Activity,
1385
1516
  report: FileCheck2,
1386
1517
  contract: FileSignature,
@@ -1390,6 +1521,10 @@ function categoryLabel(category) {
1390
1521
  switch (category) {
1391
1522
  case "anamnesis":
1392
1523
  return tl("Anamnesis", "Anamnese");
1524
+ case "evaluation":
1525
+ return tl("Evaluation", "Avalia\xE7\xE3o");
1526
+ case "photos":
1527
+ return tl("Photos", "Fotos");
1393
1528
  case "evolution":
1394
1529
  return tl("Evolution", "Evolu\xE7\xE3o");
1395
1530
  case "report":
@@ -1405,6 +1540,8 @@ function normalizeCategory(value) {
1405
1540
  }
1406
1541
  var CATEGORY_TIMELINE_ICON = {
1407
1542
  anamnesis: "ClipboardList",
1543
+ evaluation: "Stethoscope",
1544
+ photos: "Camera",
1408
1545
  evolution: "Activity",
1409
1546
  report: "FileCheck2",
1410
1547
  contract: "FileSignature",
@@ -1412,6 +1549,8 @@ var CATEGORY_TIMELINE_ICON = {
1412
1549
  };
1413
1550
  var CATEGORY_TONE = {
1414
1551
  anamnesis: "primary",
1552
+ evaluation: "primary",
1553
+ photos: "info",
1415
1554
  evolution: "info",
1416
1555
  report: "success",
1417
1556
  contract: "warning",
@@ -1781,7 +1920,7 @@ function DocumentFormDialog({
1781
1920
  provider.getTemplateById(existingDocument.templateId).then(setTemplate);
1782
1921
  }
1783
1922
  }, [existingDocument, template, provider]);
1784
- const prefilled = React11.useRef(!!initialTemplate || !!existingDocument);
1923
+ const prefilled = React12.useRef(!!initialTemplate || !!existingDocument);
1785
1924
  useEffect(() => {
1786
1925
  if (prefilled.current || !template || existingDocument) return;
1787
1926
  prefilled.current = true;
@@ -1813,7 +1952,7 @@ function DocumentFormDialog({
1813
1952
  },
1814
1953
  [template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
1815
1954
  );
1816
- const snapshot = React11.useRef(null);
1955
+ const snapshot = React12.useRef(null);
1817
1956
  useEffect(() => {
1818
1957
  if (template && snapshot.current === null) {
1819
1958
  snapshot.current = JSON.stringify(existingDocument?.data ?? {});
@@ -2063,7 +2202,7 @@ function AddDocumentDropdown({ personId, onSelect, optionFilter, label }) {
2063
2202
  )
2064
2203
  ] }),
2065
2204
  /* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-[min(60vh,20rem)] overflow-y-auto overscroll-contain p-1", children: [
2066
- groups.map((group, gi) => /* @__PURE__ */ jsxs(React11.Fragment, { children: [
2205
+ groups.map((group, gi) => /* @__PURE__ */ jsxs(React12.Fragment, { children: [
2067
2206
  gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
2068
2207
  /* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
2069
2208
  group.items.map((opt) => {
@@ -2278,6 +2417,91 @@ function FileDocumentViewer({ document: doc, onBack }) {
2278
2417
  ] }) })
2279
2418
  ] });
2280
2419
  }
2420
+ var RANK = { danger: 3, warning: 2, info: 1 };
2421
+ var TONE = {
2422
+ danger: { wrap: "border-destructive/40 bg-destructive/5", chip: "bg-destructive text-destructive-foreground", dot: "bg-destructive" },
2423
+ warning: { wrap: "border-warning/50 bg-warning-soft", chip: "bg-warning text-warning-foreground", dot: "bg-warning" },
2424
+ info: { wrap: "border-info/40 bg-info-soft", chip: "bg-info text-white", dot: "bg-info" }
2425
+ };
2426
+ function PersonAnswerAlerts({ personId, provider }) {
2427
+ const [alerts, setAlerts] = React12.useState([]);
2428
+ const [open, setOpen] = React12.useState(false);
2429
+ React12.useEffect(() => {
2430
+ if (!personId || !provider.getLatestAnswers) return;
2431
+ let alive = true;
2432
+ (async () => {
2433
+ try {
2434
+ const { data: templates } = await provider.getTemplates({ pageSize: 200 });
2435
+ const withAlerts = templates.filter((t) => t.isActive && t.schema.fields.some((f) => alertOf(f)));
2436
+ if (!withAlerts.length) return;
2437
+ const answers = await provider.getLatestAnswers(personId, withAlerts.map((t) => t.id));
2438
+ const out = [];
2439
+ for (const a of answers) {
2440
+ const t = withAlerts.find((x) => x.id === a.templateId);
2441
+ if (!t) continue;
2442
+ for (const f of t.schema.fields) {
2443
+ const hit = answerAlerts(f, a.data[f.id]);
2444
+ if (!hit) continue;
2445
+ out.push({
2446
+ key: `${t.id}:${f.id}`,
2447
+ label: hit.message || f.label,
2448
+ severity: hit.severity ?? "danger",
2449
+ answer: yesNoAnswer(a.data[f.id]),
2450
+ templateName: t.name
2451
+ });
2452
+ }
2453
+ }
2454
+ out.sort((x, y) => RANK[y.severity] - RANK[x.severity]);
2455
+ if (alive) setAlerts(out);
2456
+ } catch {
2457
+ }
2458
+ })();
2459
+ return () => {
2460
+ alive = false;
2461
+ };
2462
+ }, [personId, provider]);
2463
+ if (!alerts.length) return null;
2464
+ const tone = TONE[alerts[0].severity];
2465
+ return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-lg border-2 ${tone.wrap}`, children: [
2466
+ /* @__PURE__ */ jsxs(
2467
+ "button",
2468
+ {
2469
+ type: "button",
2470
+ onClick: () => setOpen((o) => !o),
2471
+ "aria-expanded": open,
2472
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left",
2473
+ children: [
2474
+ /* @__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" }) }),
2475
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2476
+ /* @__PURE__ */ jsxs("span", { className: "block text-sm font-semibold leading-tight", children: [
2477
+ alerts.length,
2478
+ " alerta",
2479
+ alerts.length > 1 ? "s" : "",
2480
+ " no hist\xF3rico do cliente"
2481
+ ] }),
2482
+ /* @__PURE__ */ jsxs("span", { className: "block truncate text-xs text-muted-foreground", children: [
2483
+ alerts.slice(0, 3).map((a) => a.label).join(" \xB7 "),
2484
+ alerts.length > 3 ? "\u2026" : ""
2485
+ ] })
2486
+ ] }),
2487
+ open ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4 text-muted-foreground" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })
2488
+ ]
2489
+ }
2490
+ ),
2491
+ open && /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/40 border-t px-3", children: alerts.map((a) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2 py-2", children: [
2492
+ /* @__PURE__ */ jsx("span", { className: `mt-1.5 h-2 w-2 shrink-0 rounded-full ${TONE[a.severity].dot}` }),
2493
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
2494
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium leading-tight", children: a.label }),
2495
+ /* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-[11px] leading-tight text-muted-foreground", children: [
2496
+ "Respondeu ",
2497
+ /* @__PURE__ */ jsx("strong", { className: "uppercase", children: a.answer === "sim" ? "Sim" : "N\xE3o" }),
2498
+ " \xB7 ",
2499
+ a.templateName
2500
+ ] })
2501
+ ] })
2502
+ ] }, a.key)) })
2503
+ ] });
2504
+ }
2281
2505
  function registerFormsTimelineSource() {
2282
2506
  registerTimelineSource({
2283
2507
  id: "documents",
@@ -2285,9 +2509,9 @@ function registerFormsTimelineSource() {
2285
2509
  icon: "FileText",
2286
2510
  tone: "primary",
2287
2511
  async fetch(args) {
2288
- const db = getSupabaseClientOptional();
2289
- if (!db) return [];
2290
- let query = db.from("v_documents").select("id, title, kind, status, template_name, created_at").eq("tenant_id", args.tenantId).eq("person_id", args.personId).eq("is_active", true).order("created_at", { ascending: false }).limit(args.limit);
2512
+ const db2 = getSupabaseClientOptional();
2513
+ if (!db2) return [];
2514
+ let query = db2.from("v_documents").select("id, title, kind, status, template_name, created_at").eq("tenant_id", args.tenantId).eq("person_id", args.personId).eq("is_active", true).order("created_at", { ascending: false }).limit(args.limit);
2291
2515
  if (args.before) query = query.lt("created_at", args.before);
2292
2516
  const { data, error } = await query;
2293
2517
  if (error) return [];
@@ -2309,10 +2533,10 @@ function registerFormsTimelineSource() {
2309
2533
  function registerFormsTabCounts() {
2310
2534
  registerTabCountProvider({
2311
2535
  id: "custom_forms:documents",
2312
- async fetch({ tenantId, personId }) {
2313
- const db = getSupabaseClientOptional();
2314
- if (!db) return {};
2315
- const { data, error } = await db.from("documents").select("kind").eq("tenant_id", tenantId).eq("person_id", personId).eq("is_active", true).neq("status", "archived");
2536
+ async fetch({ tenantId: tenantId2, personId }) {
2537
+ const db2 = getSupabaseClientOptional();
2538
+ if (!db2) return {};
2539
+ const { data, error } = await db2.from("documents").select("kind").eq("tenant_id", tenantId2).eq("person_id", personId).eq("is_active", true).neq("status", "archived");
2316
2540
  if (error) return {};
2317
2541
  let records = 0;
2318
2542
  let files = 0;
@@ -2407,10 +2631,10 @@ function buildFormsAnalyticsCards() {
2407
2631
  }
2408
2632
  ];
2409
2633
  }
2410
- async function getPersonFormRequirements(tenantId, personId) {
2634
+ async function getPersonFormRequirements(tenantId2, personId) {
2411
2635
  const client = getFayzSupabaseClient();
2412
2636
  const { data, error } = await client.rpc("forms_person_requirements", {
2413
- p_tenant_id: tenantId,
2637
+ p_tenant_id: tenantId2,
2414
2638
  p_person_id: personId
2415
2639
  });
2416
2640
  if (error) throw error;
@@ -2452,17 +2676,17 @@ function Linha({ item }) {
2452
2676
  )
2453
2677
  ] });
2454
2678
  }
2455
- function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, className }) {
2456
- const tenantId = useOrganizationStore((s) => s.currentOrg?.id);
2457
- const [rows, setRows] = React11.useState(null);
2458
- React11.useEffect(() => {
2459
- if (!personId || !tenantId) {
2679
+ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, sendLink, className }) {
2680
+ const tenantId2 = useOrganizationStore((s) => s.currentOrg?.id);
2681
+ const [rows, setRows] = React12.useState(null);
2682
+ React12.useEffect(() => {
2683
+ if (!personId || !tenantId2) {
2460
2684
  setRows([]);
2461
2685
  return;
2462
2686
  }
2463
2687
  let cancelled = false;
2464
2688
  setRows(null);
2465
- getPersonFormRequirements(tenantId, personId).then((r) => {
2689
+ getPersonFormRequirements(tenantId2, personId).then((r) => {
2466
2690
  if (!cancelled) setRows(r);
2467
2691
  }).catch(() => {
2468
2692
  if (!cancelled) setRows([]);
@@ -2470,8 +2694,8 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, cl
2470
2694
  return () => {
2471
2695
  cancelled = true;
2472
2696
  };
2473
- }, [personId, tenantId, version]);
2474
- const itens = React11.useMemo(() => [
2697
+ }, [personId, tenantId2, version]);
2698
+ const itens = React12.useMemo(() => [
2475
2699
  // O QUE NÃO ALERTA NÃO INTERROMPE. Exigir um formulário põe a bola no
2476
2700
  // checklist do atendimento; ALERTAR é a segunda decisão do V1
2477
2701
  // (`alert_when_missing`, desligada por padrão) e é ela que autoriza a
@@ -2498,49 +2722,101 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, cl
2498
2722
  const pendentes = itens.filter((i) => i.state !== "done");
2499
2723
  const faltando = pendentes.length;
2500
2724
  const chave = pendentes.map((i) => i.label).join("|");
2501
- React11.useEffect(() => {
2725
+ React12.useEffect(() => {
2502
2726
  onPending?.(chave ? chave.split("|") : []);
2503
2727
  }, [chave]);
2504
2728
  if (rows === null) return /* @__PURE__ */ jsx(Skeleton, { className: `h-12 w-full ${className ?? ""}` });
2505
2729
  if (itens.length === 0) return null;
2506
2730
  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: [
2507
- /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 border-b border-inherit px-3 py-1.5 text-[11px] font-medium text-foreground", children: [
2731
+ /* @__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: [
2508
2732
  /* @__PURE__ */ jsx(ClipboardList, { className: `h-3.5 w-3.5 ${faltando ? "text-warning" : "text-success"}`, "aria-hidden": true }),
2509
- faltando === 0 ? "Checagem do paciente \u2014 tudo em dia" : faltando === 1 ? "1 item pendente com o paciente" : `${faltando} itens pendentes com o paciente`
2733
+ faltando === 0 ? "Checagem do paciente \u2014 tudo em dia" : faltando === 1 ? "1 item pendente com o paciente" : `${faltando} itens pendentes com o paciente`,
2734
+ sendLink && /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
2735
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
2736
+ "button",
2737
+ {
2738
+ type: "button",
2739
+ "aria-label": "Enviar link ao paciente pelo WhatsApp",
2740
+ "aria-disabled": sendLink === "soon",
2741
+ onClick: sendLink === "soon" ? void 0 : sendLink,
2742
+ 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"}`,
2743
+ children: /* @__PURE__ */ jsx(QrCode, { className: "h-3.5 w-3.5", "aria-hidden": true })
2744
+ }
2745
+ ) }),
2746
+ /* @__PURE__ */ jsx(TooltipContent, { side: "left", children: sendLink === "soon" ? "Enviar pelo WhatsApp (QR Code) \u2014 em breve" : "Enviar pelo WhatsApp (QR Code)" })
2747
+ ] }) })
2510
2748
  ] }),
2511
2749
  /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/60", children: itens.map((i) => /* @__PURE__ */ jsx(Linha, { item: i }, i.id)) })
2512
2750
  ] });
2513
2751
  }
2514
- var Ctx = createContext(null);
2515
- function CustomFormsProvider({
2516
- value,
2517
- children
2518
- }) {
2519
- return React11.createElement(Ctx.Provider, { value }, children);
2752
+ function recommendationsOf(template, data) {
2753
+ const by = /* @__PURE__ */ new Map();
2754
+ for (const f of template.schema.fields) {
2755
+ if (f.type !== "recommendation" || !Array.isArray(data[f.id])) continue;
2756
+ for (const l of data[f.id]) {
2757
+ const cur = by.get(l.serviceId);
2758
+ by.set(l.serviceId, cur ? { ...cur, quantity: cur.quantity + l.quantity } : { ...l });
2759
+ }
2760
+ }
2761
+ return [...by.values()];
2520
2762
  }
2521
- function FormFillDialog({ templateId, personId, personName, onSaved, onClose, frame = "modal" }) {
2522
- const provider = React11.useMemo(() => createSafeDataProvider(
2763
+ function FormFillDialog({
2764
+ templateId,
2765
+ personId,
2766
+ personName,
2767
+ onSaved,
2768
+ onClose,
2769
+ frame = "modal",
2770
+ subject,
2771
+ productId,
2772
+ professionalId,
2773
+ recommendationShortcuts
2774
+ }) {
2775
+ const provider = React12.useMemo(() => createSafeDataProvider(
2523
2776
  () => createSupabaseFormsProvider(),
2524
2777
  () => createMockFormsProvider()
2525
2778
  ), []);
2526
- const store = React11.useMemo(() => createCustomFormsStore(provider), [provider]);
2527
- const config = React11.useMemo(() => resolveConfig(), []);
2528
- const [template, setTemplate] = React11.useState(null);
2529
- const [erro, setErro] = React11.useState(null);
2530
- const [data, setData] = React11.useState({});
2531
- const [salvando, setSalvando] = React11.useState(false);
2779
+ const store = React12.useMemo(() => createCustomFormsStore(provider), [provider]);
2780
+ const config = React12.useMemo(() => resolveConfig(), []);
2781
+ const [template, setTemplate] = React12.useState(null);
2782
+ const [erro, setErro] = React12.useState(null);
2783
+ const [data, setData] = React12.useState({});
2784
+ const [salvando, setSalvando] = React12.useState(false);
2785
+ const [missing, setMissing] = React12.useState(/* @__PURE__ */ new Set());
2786
+ React12.useEffect(() => {
2787
+ if (!template || missing.size === 0) return;
2788
+ const still = new Set([...missing].filter((id) => {
2789
+ const f = template.schema.fields.find((x) => x.id === id);
2790
+ return f ? isFieldEmpty(f, data[id]) : false;
2791
+ }));
2792
+ if (still.size !== missing.size) setMissing(still);
2793
+ if (still.size === 0) setErro(null);
2794
+ }, [data, template, missing]);
2532
2795
  async function salvar() {
2533
2796
  if (!template) return;
2797
+ const faltando = template.schema.fields.filter((f) => f.required && isFieldEmpty(f, data[f.id]));
2798
+ if (faltando.length) {
2799
+ setMissing(new Set(faltando.map((f) => f.id)));
2800
+ const primeiro = [...faltando].sort((a, b) => a.row - b.row || a.col - b.col)[0];
2801
+ requestAnimationFrame(() => document.getElementById(`ff-field-${primeiro.id}`)?.scrollIntoView({ block: "center" }));
2802
+ setErro(faltando.length === 1 ? `Preencha o campo obrigat\xF3rio destacado: ${faltando[0].label}` : `Preencha os ${faltando.length} campos obrigat\xF3rios destacados.`);
2803
+ return;
2804
+ }
2805
+ setErro(null);
2534
2806
  setSalvando(true);
2535
2807
  try {
2536
- await store.getState().createDocument({
2808
+ const doc = await store.getState().createDocument({
2537
2809
  templateId: template.id,
2538
2810
  personId,
2539
2811
  title: personName ? `${template.name} \u2014 ${personName}` : template.name,
2540
2812
  data,
2541
- status: "completed"
2813
+ status: "completed",
2814
+ subjectType: subject?.type,
2815
+ subjectId: subject?.id,
2816
+ productId: productId ?? void 0,
2817
+ professionalId: professionalId ?? void 0
2542
2818
  });
2543
- onSaved?.();
2819
+ await onSaved?.({ documentId: doc.id, template, data, recommendations: recommendationsOf(template, data) });
2544
2820
  onClose();
2545
2821
  } catch (e) {
2546
2822
  setErro(e instanceof Error ? e.message : "N\xE3o foi poss\xEDvel salvar o formul\xE1rio.");
@@ -2548,7 +2824,7 @@ function FormFillDialog({ templateId, personId, personName, onSaved, onClose, fr
2548
2824
  setSalvando(false);
2549
2825
  }
2550
2826
  }
2551
- React11.useEffect(() => {
2827
+ React12.useEffect(() => {
2552
2828
  let vivo = true;
2553
2829
  (async () => {
2554
2830
  try {
@@ -2563,13 +2839,25 @@ function FormFillDialog({ templateId, personId, personName, onSaved, onClose, fr
2563
2839
  };
2564
2840
  }, [templateId, store]);
2565
2841
  const corpo = /* @__PURE__ */ jsxs(Fragment, { children: [
2566
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: erro ? /* @__PURE__ */ jsx("p", { className: "rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: erro }) : !template ? (
2567
- /* Esqueleto com a FORMA do formulário: três campos e um rótulo. */
2568
- /* @__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: [
2569
- /* @__PURE__ */ jsx("div", { className: "h-3 w-24 animate-pulse rounded bg-muted/70" }),
2570
- /* @__PURE__ */ jsx("div", { className: "h-9 w-full animate-pulse rounded-lg bg-muted" })
2571
- ] }, i)) })
2572
- ) : /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(FormRenderer, { schema: template.schema, data, onChange: setData }) }) }),
2842
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: [
2843
+ erro && /* @__PURE__ */ jsx("p", { role: "alert", className: "mb-3 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: erro }),
2844
+ !template ? erro ? null : (
2845
+ /* Esqueleto com a FORMA do formulário: três campos e um rótulo. */
2846
+ /* @__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: [
2847
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-24 animate-pulse rounded bg-muted/70" }),
2848
+ /* @__PURE__ */ jsx("div", { className: "h-9 w-full animate-pulse rounded-lg bg-muted" })
2849
+ ] }, i)) })
2850
+ ) : /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
2851
+ FormRenderer,
2852
+ {
2853
+ schema: template.schema,
2854
+ data,
2855
+ onChange: setData,
2856
+ missing,
2857
+ recommendationShortcuts
2858
+ }
2859
+ ) })
2860
+ ] }),
2573
2861
  /* @__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: [
2574
2862
  /* @__PURE__ */ jsx(
2575
2863
  "button",
@@ -2601,6 +2889,544 @@ function FormFillDialog({ templateId, personId, personName, onSaved, onClose, fr
2601
2889
  ] }) });
2602
2890
  }
2603
2891
 
2892
+ // src/lib/contract-render.ts
2893
+ var MONTHS = ["janeiro", "fevereiro", "mar\xE7o", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
2894
+ var brl = (n) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(n);
2895
+ var pad = (n) => String(n).padStart(2, "0");
2896
+ var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2897
+ function dateBR(value) {
2898
+ if (!value) return "";
2899
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
2900
+ return m ? `${m[3]}/${m[2]}/${m[1]}` : value;
2901
+ }
2902
+ function itemsTable(items, withDiscount) {
2903
+ if (!items?.length) return "";
2904
+ const head = ["Item", "Qtd", "Valor Unit.", ...withDiscount ? ["Desconto"] : [], "Total"];
2905
+ const rows = items.map((i) => [
2906
+ esc(i.description),
2907
+ String(i.quantity),
2908
+ brl(i.unitPrice),
2909
+ ...withDiscount ? [brl(i.discount ?? 0)] : [],
2910
+ brl(i.total)
2911
+ ]);
2912
+ 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>`;
2913
+ }
2914
+ function renderContractTags(templateHtml, ctx) {
2915
+ const now2 = ctx.now ?? /* @__PURE__ */ new Date();
2916
+ const c = ctx.client ?? {};
2917
+ const u = ctx.unit ?? {};
2918
+ const services = ctx.services ?? [];
2919
+ const names = services.map((s) => esc(s.name)).join(", ");
2920
+ const map = {
2921
+ "Cliente.Nome": c.name ?? "",
2922
+ "Cliente.NomeSocial": c.socialName ?? "",
2923
+ "Cliente.CPF": c.cpf ?? "",
2924
+ "Cliente.RG": c.rg ?? "",
2925
+ "Cliente.Nascimento": dateBR(c.birthDate),
2926
+ "Cliente.Email": c.email ?? "",
2927
+ "Cliente.Endereco": c.street ?? "",
2928
+ "Cliente.Numero": c.number ?? "",
2929
+ "Cliente.Complemento": c.complement ?? "",
2930
+ "Cliente.Bairro": c.district ?? "",
2931
+ "Cliente.Cidade": c.city ?? "",
2932
+ "Cliente.Estado": c.state ?? "",
2933
+ "Cliente.Cep": c.postalCode ?? "",
2934
+ "Cliente.Telefone": c.phone ?? "",
2935
+ "Cliente.Celular": c.mobile ?? "",
2936
+ "Unidade.NomeFantasia": u.tradeName ?? "",
2937
+ "Unidade.RazaoSocial": u.legalName ?? "",
2938
+ "Unidade.CNPJ": u.cnpj ?? "",
2939
+ "Unidade.Endereco": u.street ?? "",
2940
+ "Unidade.Numero": u.number ?? "",
2941
+ "Unidade.Complemento": u.complement ?? "",
2942
+ "Unidade.Bairro": u.district ?? "",
2943
+ "Unidade.Cidade": u.city ?? "",
2944
+ "Unidade.Estado": u.state ?? "",
2945
+ "Unidade.Cep": u.postalCode ?? "",
2946
+ "Unidade.Telefone": u.phone ?? "",
2947
+ "Unidade.Celular": u.mobile ?? "",
2948
+ "Unidade.Email": u.email ?? "",
2949
+ "Profissional.Nome": ctx.professional?.name ?? "",
2950
+ "Servicos.Lista": names,
2951
+ "Servicos.Nome": names,
2952
+ "Servicos.PrecoTotal": brl(services.reduce((s, x) => s + x.price, 0)),
2953
+ "Servicos.TextoContrato": services.filter((s) => s.contractText).map((s) => s.contractText).join("<br/><br/>"),
2954
+ "FormasPagto.Lista": (ctx.paymentMethods ?? []).map(esc).join(", "),
2955
+ "Orcamento.Itens": itemsTable(ctx.approvedItems, true),
2956
+ "Orcamento.ItensAprovados": itemsTable(ctx.approvedItems, true),
2957
+ "Indicacoes.Itens": itemsTable(ctx.recommendedItems, false),
2958
+ "Indicacoes.Lista": (ctx.recommendedItems ?? []).map((i) => esc(i.description)).join(", "),
2959
+ "Sistema.Data": `${pad(now2.getDate())}/${pad(now2.getMonth() + 1)}/${now2.getFullYear()}`,
2960
+ "Sistema.DataExtenso": `${pad(now2.getDate())} de ${MONTHS[now2.getMonth()]} de ${now2.getFullYear()}`,
2961
+ "Sistema.Hora": `${pad(now2.getHours())}:${pad(now2.getMinutes())}`
2962
+ };
2963
+ const built = /* @__PURE__ */ new Set([
2964
+ "Orcamento.Itens",
2965
+ "Orcamento.ItensAprovados",
2966
+ "Indicacoes.Itens",
2967
+ "Servicos.TextoContrato",
2968
+ "Servicos.Lista",
2969
+ "Servicos.Nome",
2970
+ "Indicacoes.Lista",
2971
+ "FormasPagto.Lista"
2972
+ ]);
2973
+ return templateHtml.replace(/\{\{\s*([A-Za-z]+\.[A-Za-z]+)\s*\}\}/g, (whole, key) => {
2974
+ if (!(key in map)) return whole;
2975
+ return built.has(key) ? map[key] : esc(map[key]);
2976
+ });
2977
+ }
2978
+
2979
+ // src/lib/attendance-contracts.ts
2980
+ var db = () => {
2981
+ const c = getSupabaseClientOptional();
2982
+ if (!c) throw new Error("Supabase not initialized");
2983
+ return c;
2984
+ };
2985
+ var tenantId = () => getFormsTenantId() ?? getActiveTenantId() ?? "";
2986
+ var BUCKET = "attachments";
2987
+ function toLine(s) {
2988
+ const total = s.unitPrice * s.quantity - (s.discount ?? 0);
2989
+ return { description: s.name, quantity: s.quantity, unitPrice: s.unitPrice, discount: s.discount ?? 0, total };
2990
+ }
2991
+ function mapContract(row, templates, sessionTermId) {
2992
+ const m = row.metadata?.contract ?? {};
2993
+ const t = templates.get(m.templateId);
2994
+ return {
2995
+ id: row.id,
2996
+ templateId: m.templateId,
2997
+ templateName: t?.name ?? row.title ?? "Contrato",
2998
+ status: row.status === "signed" ? "signed" : "pending",
2999
+ html: m.html ?? "",
3000
+ serviceIds: m.serviceIds ?? [],
3001
+ isSessionTerm: !!sessionTermId && m.templateId === sessionTermId,
3002
+ requireSignature: t?.require_screen_signature ?? true,
3003
+ requireSelfie: t?.require_selfie ?? false,
3004
+ blocksFinish: t?.require_signature_for_execution ?? false,
3005
+ signedByName: m.signedByName ?? null,
3006
+ signedAt: m.signedAt ?? null,
3007
+ signaturePath: m.signaturePath ?? null,
3008
+ selfiePath: m.selfiePath ?? null
3009
+ };
3010
+ }
3011
+ async function loadTemplates() {
3012
+ const { data, error } = await db().from("plg_forms_contract_templates").select("*").eq("tenant_id", tenantId());
3013
+ if (error) throw new Error(error.message);
3014
+ return new Map((data ?? []).map((t) => [t.id, t]));
3015
+ }
3016
+ async function sessionTermTemplateId() {
3017
+ const { data } = await db().from("tenants").select("settings").eq("id", tenantId()).maybeSingle();
3018
+ return data?.settings?.attendance_flow?.session_term_template_id ?? null;
3019
+ }
3020
+ async function loadContext(input) {
3021
+ const c = db();
3022
+ const [person, address, unit, pro] = await Promise.all([
3023
+ c.from("people").select("name, document_number, date_of_birth, email, phone, address, city, state, postal_code").eq("id", input.personId).maybeSingle(),
3024
+ c.from("addresses").select("street, number, complement, district, city, state, postal_code, is_default").eq("owner_type", "person").eq("owner_id", input.personId).order("is_default", { ascending: false }).limit(1).maybeSingle(),
3025
+ input.unitId ? c.from("v_units").select("name, legal_name, cnpj, address_street, address_number, address_complement, address_district, city, state, postal_code, phone, mobile_1, email").eq("id", input.unitId).maybeSingle() : Promise.resolve({ data: null }),
3026
+ input.professionalId ? c.from("people").select("name").eq("id", input.professionalId).maybeSingle() : Promise.resolve({ data: null })
3027
+ ]);
3028
+ const p = person.data ?? {};
3029
+ const a = address.data ?? {};
3030
+ const u = unit.data ?? {};
3031
+ return {
3032
+ client: {
3033
+ name: p.name,
3034
+ cpf: p.document_number,
3035
+ birthDate: p.date_of_birth,
3036
+ email: p.email,
3037
+ phone: p.phone,
3038
+ mobile: p.phone,
3039
+ street: a.street ?? p.address,
3040
+ number: a.number,
3041
+ complement: a.complement,
3042
+ district: a.district,
3043
+ city: a.city ?? p.city,
3044
+ state: a.state ?? p.state,
3045
+ postalCode: a.postal_code ?? p.postal_code
3046
+ },
3047
+ unit: {
3048
+ tradeName: u.name,
3049
+ legalName: u.legal_name ?? u.name,
3050
+ cnpj: u.cnpj,
3051
+ street: u.address_street,
3052
+ number: u.address_number,
3053
+ complement: u.address_complement,
3054
+ district: u.address_district,
3055
+ city: u.city,
3056
+ state: u.state,
3057
+ postalCode: u.postal_code,
3058
+ phone: u.phone,
3059
+ mobile: u.mobile_1,
3060
+ email: u.email
3061
+ },
3062
+ professional: { name: pro.data?.name }
3063
+ };
3064
+ }
3065
+ async function serviceTexts(ids) {
3066
+ if (!ids.length) return /* @__PURE__ */ new Map();
3067
+ const { data } = await db().from("products").select("id, metadata").in("id", ids);
3068
+ return new Map((data ?? []).map((r) => [r.id, r.metadata?.beauty?.contract_text ?? ""]));
3069
+ }
3070
+ async function listAttendanceContracts(bookingId) {
3071
+ const [templates, termId, rows] = await Promise.all([
3072
+ loadTemplates(),
3073
+ sessionTermTemplateId(),
3074
+ db().from("documents").select("id, title, status, metadata, created_at").eq("tenant_id", tenantId()).eq("kind", "contract").eq("subject_type", "appointment").eq("subject_id", bookingId).eq("is_active", true).order("created_at")
3075
+ ]);
3076
+ if (rows.error) throw new Error(rows.error.message);
3077
+ return (rows.data ?? []).map((r) => mapContract(r, templates, termId));
3078
+ }
3079
+ var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
3080
+ function ensureAttendanceContracts(input) {
3081
+ const prev = queue.get(input.bookingId) ?? Promise.resolve([]);
3082
+ const next = prev.catch(() => []).then(() => generate(input));
3083
+ queue.set(input.bookingId, next);
3084
+ next.finally(() => {
3085
+ if (queue.get(input.bookingId) === next) queue.delete(input.bookingId);
3086
+ }).catch(() => {
3087
+ });
3088
+ return next;
3089
+ }
3090
+ var queue = /* @__PURE__ */ new Map();
3091
+ async function generate(input) {
3092
+ const [templates, termId, existing] = await Promise.all([
3093
+ loadTemplates(),
3094
+ sessionTermTemplateId(),
3095
+ listAttendanceContracts(input.bookingId)
3096
+ ]);
3097
+ const active = [...templates.values()].filter((t) => t.is_active);
3098
+ const wanted = [];
3099
+ if (input.contracted.length) {
3100
+ for (const t of active) {
3101
+ if (t.id === termId) continue;
3102
+ const allBilled = t.generate_on_quote_approval || t.generate_on_execution && t.execution_scope === "all_billed_services";
3103
+ const linked = t.generate_on_execution && (t.execution_scope ?? "linked_services") === "linked_services" ? input.contracted.filter((s) => t.metadata?.service_ids?.includes(s.serviceId)) : [];
3104
+ const services = allBilled ? input.contracted : linked;
3105
+ if (!services.length) continue;
3106
+ if (t.group_services || allBilled) wanted.push({ template: t, services });
3107
+ else for (const s of services) wanted.push({ template: t, services: [s] });
3108
+ }
3109
+ }
3110
+ const term = termId ? templates.get(termId) : void 0;
3111
+ if (term?.is_active && input.sessionServices.length) wanted.push({ template: term, services: input.sessionServices });
3112
+ if (!wanted.length) return existing;
3113
+ const ctx = await loadContext(input);
3114
+ const texts = await serviceTexts([...new Set(wanted.flatMap((w) => w.services.map((s) => s.serviceId)))]);
3115
+ const c = db();
3116
+ for (const w of wanted) {
3117
+ const ids = [...new Set(w.services.map((s) => s.serviceId))].sort();
3118
+ const mine = existing.filter((e) => e.templateId === w.template.id);
3119
+ const perService = !w.template.group_services && w.template.id !== termId && !w.template.generate_on_quote_approval;
3120
+ const scope = perService ? mine.filter((e) => sameSet(e.serviceIds, ids)) : mine;
3121
+ if (scope.some((e) => e.status === "signed" && ids.every((id) => e.serviceIds.includes(id)))) continue;
3122
+ const html = renderContractTags(w.template.content ?? "", {
3123
+ ...ctx,
3124
+ services: w.services.map((s) => ({ name: s.name, price: s.unitPrice * s.quantity, contractText: texts.get(s.serviceId) || void 0 })),
3125
+ approvedItems: input.contracted.map(toLine),
3126
+ recommendedItems: input.indicated.map(toLine)
3127
+ });
3128
+ const meta = { contract: { templateId: w.template.id, html, serviceIds: ids } };
3129
+ const pending = scope.find((e) => e.status === "pending");
3130
+ if (pending) {
3131
+ if (sameSet(pending.serviceIds, ids)) continue;
3132
+ const { error: error2 } = await c.from("documents").update({ metadata: meta, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", pending.id).eq("status", "pending");
3133
+ if (error2) throw new Error(error2.message);
3134
+ continue;
3135
+ }
3136
+ const { error } = await c.from("documents").insert({
3137
+ tenant_id: tenantId(),
3138
+ kind: "contract",
3139
+ person_id: input.personId,
3140
+ unit_id: input.unitId,
3141
+ title: w.template.name,
3142
+ status: "pending",
3143
+ subject_type: "appointment",
3144
+ subject_id: input.bookingId,
3145
+ metadata: meta
3146
+ });
3147
+ if (error) throw new Error(error.message);
3148
+ }
3149
+ return listAttendanceContracts(input.bookingId);
3150
+ }
3151
+ async function signAttendanceContract(contract, input) {
3152
+ const c = db();
3153
+ const up = async (blob, name) => {
3154
+ const path = `${tenantId()}/contracts/${contract.id}/${Date.now()}-${name}`;
3155
+ const { error: error2 } = await c.storage.from(BUCKET).upload(path, blob, { contentType: blob.type || void 0 });
3156
+ if (error2) throw new Error(error2.message);
3157
+ return path;
3158
+ };
3159
+ const signaturePath = input.signature ? await up(input.signature, "assinatura.png") : null;
3160
+ const selfiePath = input.selfie ? await up(input.selfie, "selfie.jpg") : null;
3161
+ const { data: row, error: readErr } = await c.from("documents").select("metadata").eq("id", contract.id).maybeSingle();
3162
+ if (readErr) throw new Error(readErr.message);
3163
+ const metadata = {
3164
+ ...row?.metadata ?? {},
3165
+ contract: {
3166
+ ...row?.metadata?.contract ?? {},
3167
+ signedByName: input.signerName.trim() || null,
3168
+ signedAt: (/* @__PURE__ */ new Date()).toISOString(),
3169
+ signaturePath,
3170
+ selfiePath
3171
+ }
3172
+ };
3173
+ const { data, error } = await c.from("documents").update({ status: "signed", metadata, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", contract.id).eq("status", "pending").select("id");
3174
+ if (error) throw new Error(error.message);
3175
+ if (!data?.length) throw new Error("Este contrato j\xE1 foi assinado.");
3176
+ }
3177
+ async function contractMediaUrl(path) {
3178
+ if (!path) return null;
3179
+ const { data } = await db().storage.from(BUCKET).createSignedUrl(path, 3600);
3180
+ return data?.signedUrl ?? null;
3181
+ }
3182
+ var CONTRACT_TEXT_CLASS = [
3183
+ "space-y-2 text-sm leading-relaxed",
3184
+ "[&_b]:font-semibold [&_strong]:font-semibold [&_h1]:text-base [&_h1]:font-semibold [&_h2]:font-semibold [&_h3]:font-semibold",
3185
+ "[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5",
3186
+ "[&_table]:my-2 [&_table]:w-full [&_table]:text-xs [&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1",
3187
+ "[&_th]:border [&_th]:border-border [&_th]:bg-muted/50 [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:font-medium"
3188
+ ].join(" ");
3189
+ function useSigned(path) {
3190
+ const [url, setUrl] = React12.useState(null);
3191
+ React12.useEffect(() => {
3192
+ let alive = true;
3193
+ contractMediaUrl(path).then((u) => {
3194
+ if (alive) setUrl(u);
3195
+ }).catch(() => {
3196
+ });
3197
+ return () => {
3198
+ alive = false;
3199
+ };
3200
+ }, [path]);
3201
+ return url;
3202
+ }
3203
+ function SignaturePad({ onChange }) {
3204
+ const ref = React12.useRef(null);
3205
+ const drawing = React12.useRef(false);
3206
+ React12.useEffect(() => {
3207
+ const c = ref.current;
3208
+ if (!c) return;
3209
+ const r = window.devicePixelRatio || 1;
3210
+ c.width = c.offsetWidth * r;
3211
+ c.height = c.offsetHeight * r;
3212
+ const ctx = c.getContext("2d");
3213
+ ctx.scale(r, r);
3214
+ ctx.lineWidth = 2;
3215
+ ctx.lineCap = "round";
3216
+ ctx.strokeStyle = "#111827";
3217
+ }, []);
3218
+ const pos = (e) => {
3219
+ const b = e.currentTarget.getBoundingClientRect();
3220
+ return [e.clientX - b.left, e.clientY - b.top];
3221
+ };
3222
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
3223
+ /* @__PURE__ */ jsx(
3224
+ "canvas",
3225
+ {
3226
+ ref,
3227
+ "aria-label": "\xC1rea de assinatura",
3228
+ className: "h-36 w-full touch-none rounded-md border bg-white",
3229
+ onPointerDown: (e) => {
3230
+ drawing.current = true;
3231
+ e.currentTarget.setPointerCapture(e.pointerId);
3232
+ const ctx = e.currentTarget.getContext("2d");
3233
+ const [x, y] = pos(e);
3234
+ ctx.beginPath();
3235
+ ctx.moveTo(x, y);
3236
+ },
3237
+ onPointerMove: (e) => {
3238
+ if (!drawing.current) return;
3239
+ const ctx = e.currentTarget.getContext("2d");
3240
+ const [x, y] = pos(e);
3241
+ ctx.lineTo(x, y);
3242
+ ctx.stroke();
3243
+ },
3244
+ onPointerUp: (e) => {
3245
+ drawing.current = false;
3246
+ e.currentTarget.toBlob((b) => onChange(b), "image/png");
3247
+ }
3248
+ }
3249
+ ),
3250
+ /* @__PURE__ */ jsx(
3251
+ "button",
3252
+ {
3253
+ type: "button",
3254
+ className: "text-xs text-muted-foreground hover:text-foreground",
3255
+ onClick: () => {
3256
+ const c = ref.current;
3257
+ if (c) {
3258
+ c.getContext("2d").clearRect(0, 0, c.width, c.height);
3259
+ onChange(null);
3260
+ }
3261
+ },
3262
+ children: "Limpar assinatura"
3263
+ }
3264
+ )
3265
+ ] });
3266
+ }
3267
+ function SelfieCapture({ value, onChange }) {
3268
+ const video = React12.useRef(null);
3269
+ const stream = React12.useRef(null);
3270
+ const [on, setOn] = React12.useState(false);
3271
+ const [erro, setErro] = React12.useState(null);
3272
+ const preview = React12.useMemo(() => value ? URL.createObjectURL(value) : null, [value]);
3273
+ const stop = () => {
3274
+ stream.current?.getTracks().forEach((t) => t.stop());
3275
+ stream.current = null;
3276
+ setOn(false);
3277
+ };
3278
+ React12.useEffect(() => () => stop(), []);
3279
+ async function start() {
3280
+ setErro(null);
3281
+ try {
3282
+ stream.current = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" } });
3283
+ setOn(true);
3284
+ requestAnimationFrame(() => {
3285
+ if (video.current) {
3286
+ video.current.srcObject = stream.current;
3287
+ video.current.play().catch(() => {
3288
+ });
3289
+ }
3290
+ });
3291
+ } catch {
3292
+ setErro("N\xE3o foi poss\xEDvel acessar a c\xE2mera.");
3293
+ }
3294
+ }
3295
+ function capture() {
3296
+ const v = video.current;
3297
+ if (!v?.videoWidth) return;
3298
+ const c = document.createElement("canvas");
3299
+ c.width = v.videoWidth;
3300
+ c.height = v.videoHeight;
3301
+ c.getContext("2d").drawImage(v, 0, 0);
3302
+ c.toBlob((b) => {
3303
+ onChange(b);
3304
+ stop();
3305
+ }, "image/jpeg", 0.85);
3306
+ }
3307
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
3308
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Selfie" }),
3309
+ preview ? /* @__PURE__ */ jsxs("div", { className: "relative w-40", children: [
3310
+ /* @__PURE__ */ jsx("img", { src: preview, alt: "Selfie", className: "rounded-lg border" }),
3311
+ /* @__PURE__ */ jsx(
3312
+ "button",
3313
+ {
3314
+ type: "button",
3315
+ "aria-label": "Remover selfie",
3316
+ onClick: () => onChange(null),
3317
+ className: "absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-background/90",
3318
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
3319
+ }
3320
+ )
3321
+ ] }) : on ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
3322
+ /* @__PURE__ */ jsx("video", { ref: video, playsInline: true, muted: true, className: "w-full max-w-[240px] rounded-lg border" }),
3323
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3324
+ /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: capture, children: [
3325
+ /* @__PURE__ */ jsx(Camera, { className: "mr-1 h-4 w-4" }),
3326
+ " Capturar"
3327
+ ] }),
3328
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: stop, children: "Cancelar" })
3329
+ ] })
3330
+ ] }) : /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: start, children: [
3331
+ /* @__PURE__ */ jsx(Camera, { className: "mr-1 h-4 w-4" }),
3332
+ " Tirar selfie"
3333
+ ] }),
3334
+ erro && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: erro })
3335
+ ] });
3336
+ }
3337
+ function printContract(contract, signatureUrl) {
3338
+ const w = window.open("", "_blank");
3339
+ if (!w) return;
3340
+ const signed = contract.status === "signed" ? `<div style="margin-top:32px">${signatureUrl ? `<img src="${signatureUrl}" style="max-height:120px"/>` : ""}
3341
+ <p>Assinado ${contract.signedByName ? `por ${contract.signedByName.replace(/</g, "&lt;")} ` : ""}em ${contract.signedAt ? new Date(contract.signedAt).toLocaleString("pt-BR") : ""}</p></div>` : "";
3342
+ w.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${contract.templateName.replace(/</g, "&lt;")}</title>
3343
+ <style>body{font:14px/1.5 system-ui,sans-serif;max-width:760px;margin:32px auto;padding:0 16px;color:#111}
3344
+ table{border-collapse:collapse;width:100%;margin:8px 0}td,th{border:1px solid #ccc;padding:4px 6px;text-align:left}</style>
3345
+ </head><body>${sanitizeHtml(contract.html)}${signed}</body></html>`);
3346
+ w.document.close();
3347
+ w.focus();
3348
+ setTimeout(() => w.print(), 300);
3349
+ }
3350
+ function ContractSignPane({ contract, defaultSignerName, onSigned, onClose }) {
3351
+ const [signer, setSigner] = React12.useState(defaultSignerName ?? "");
3352
+ const [signature, setSignature] = React12.useState(null);
3353
+ const [selfie, setSelfie] = React12.useState(null);
3354
+ const [saving, setSaving] = React12.useState(false);
3355
+ const [erro, setErro] = React12.useState(null);
3356
+ const signatureUrl = useSigned(contract.signaturePath);
3357
+ const selfieUrl = useSigned(contract.selfiePath);
3358
+ const signed = contract.status === "signed";
3359
+ const html = React12.useMemo(() => sanitizeHtml(contract.html), [contract.html]);
3360
+ React12.useEffect(() => {
3361
+ setErro(null);
3362
+ }, [signature, selfie, signer]);
3363
+ 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;
3364
+ async function sign() {
3365
+ if (missing) {
3366
+ setErro(missing);
3367
+ return;
3368
+ }
3369
+ setSaving(true);
3370
+ setErro(null);
3371
+ try {
3372
+ await signAttendanceContract(contract, { signerName: signer, signature, selfie });
3373
+ onSigned();
3374
+ } catch (e) {
3375
+ setErro(e instanceof Error ? e.message : "N\xE3o foi poss\xEDvel assinar o contrato.");
3376
+ } finally {
3377
+ setSaving(false);
3378
+ }
3379
+ }
3380
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3381
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
3382
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5", onClick: () => printContract(contract, signatureUrl), children: [
3383
+ /* @__PURE__ */ jsx(Printer, { className: "h-3.5 w-3.5" }),
3384
+ " Imprimir"
3385
+ ] }) }),
3386
+ /* @__PURE__ */ jsx("div", { className: "max-h-[55vh] overflow-y-auto rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx("div", { className: CONTRACT_TEXT_CLASS, dangerouslySetInnerHTML: { __html: html } }) }),
3387
+ signed ? /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
3388
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 rounded-lg border border-success/40 bg-success-soft px-3 py-2 text-sm text-success-soft-foreground", children: [
3389
+ /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
3390
+ "Assinado em ",
3391
+ contract.signedAt ? new Date(contract.signedAt).toLocaleString("pt-BR") : "",
3392
+ contract.signedByName ? ` por ${contract.signedByName}` : ""
3393
+ ] }),
3394
+ signatureUrl && /* @__PURE__ */ jsx("img", { src: signatureUrl, alt: "Assinatura", className: "max-h-32 rounded-md border bg-white p-2" }),
3395
+ selfieUrl && /* @__PURE__ */ jsx("img", { src: selfieUrl, alt: "Selfie", className: "max-h-40 rounded-lg border" })
3396
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3397
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
3398
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium", htmlFor: "contract-signer", children: "Nome do Assinante" }),
3399
+ /* @__PURE__ */ jsx(Input, { id: "contract-signer", value: signer, onChange: (e) => setSigner(e.target.value), placeholder: "Nome completo" })
3400
+ ] }),
3401
+ contract.requireSignature && /* @__PURE__ */ jsx(SignaturePad, { onChange: setSignature }),
3402
+ contract.requireSelfie && /* @__PURE__ */ jsx(SelfieCapture, { value: selfie, onChange: setSelfie })
3403
+ ] }),
3404
+ erro && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: erro })
3405
+ ] }),
3406
+ /* @__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: [
3407
+ /* @__PURE__ */ jsx(
3408
+ "button",
3409
+ {
3410
+ type: "button",
3411
+ onClick: onClose,
3412
+ className: "rounded-button px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground",
3413
+ children: signed ? "Fechar" : "Agora n\xE3o"
3414
+ }
3415
+ ),
3416
+ !signed && /* @__PURE__ */ jsx(
3417
+ "button",
3418
+ {
3419
+ type: "button",
3420
+ onClick: sign,
3421
+ disabled: saving,
3422
+ className: "rounded-button bg-foreground px-3 py-1.5 text-xs font-semibold text-background disabled:opacity-40",
3423
+ children: saving ? "Assinando\u2026" : "Confirmar Assinatura"
3424
+ }
3425
+ )
3426
+ ] }) })
3427
+ ] });
3428
+ }
3429
+
2604
3430
  // src/index.ts
2605
3431
  function registerBuiltInProviders(formsProvider) {
2606
3432
  registerDocumentTypeProvider({
@@ -2757,14 +3583,14 @@ function createCustomFormsPlugin(options) {
2757
3583
  }
2758
3584
  ];
2759
3585
  const registries = [...formRegistries, ...options?.settingsRegistries ?? []];
2760
- const SettingsComponent = () => React11.createElement(PluginSettingsPanel, {
3586
+ const SettingsComponent = () => React12.createElement(PluginSettingsPanel, {
2761
3587
  title: config.labels.settingsLabel,
2762
3588
  customTabs: [
2763
3589
  {
2764
3590
  id: "forms",
2765
3591
  label: config.labels.pageTitle,
2766
3592
  icon: "FileText",
2767
- content: React11.createElement(CustomFormsSettingsTab, { config, provider, store })
3593
+ content: React12.createElement(CustomFormsSettingsTab, { config, provider, store })
2768
3594
  }
2769
3595
  ],
2770
3596
  registries,
@@ -2772,10 +3598,12 @@ function createCustomFormsPlugin(options) {
2772
3598
  hostPluginId: "custom_forms"
2773
3599
  });
2774
3600
  SettingsComponent.displayName = "CustomFormsSettingsPanel";
2775
- const PersonDocumentsWidget2 = (props) => React11.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "records" });
3601
+ const PersonDocumentsWidget2 = (props) => React12.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "records" });
2776
3602
  PersonDocumentsWidget2.displayName = "PersonDocumentsWidget";
2777
- const PersonFilesWidget = (props) => React11.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "files" });
3603
+ const PersonFilesWidget = (props) => React12.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "files" });
2778
3604
  PersonFilesWidget.displayName = "PersonFilesWidget";
3605
+ const PersonAnswerAlertsWidget = (props) => React12.createElement(PersonAnswerAlerts, { personId: props.item?.id, provider });
3606
+ PersonAnswerAlertsWidget.displayName = "PersonAnswerAlertsWidget";
2779
3607
  return {
2780
3608
  id: "custom_forms",
2781
3609
  defaultAgentRole: "setup",
@@ -2820,6 +3648,13 @@ function createCustomFormsPlugin(options) {
2820
3648
  zone: "person.detail.files",
2821
3649
  component: PersonFilesWidget,
2822
3650
  order: 0
3651
+ },
3652
+ {
3653
+ // Renders nothing unless a form marks an answer as an alert (V1 alert_config).
3654
+ id: "person-answer-alerts",
3655
+ zone: "person.detail.above-tabs",
3656
+ component: PersonAnswerAlertsWidget,
3657
+ order: -1
2823
3658
  }
2824
3659
  ],
2825
3660
  registries,
@@ -2886,6 +3721,6 @@ function createCustomFormsPlugin(options) {
2886
3721
  };
2887
3722
  }
2888
3723
 
2889
- export { FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, getPersonFormRequirements, isOutstanding, registerDocumentTypeProvider };
3724
+ export { ContractSignPane, FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, ensureAttendanceContracts, getPersonFormRequirements, isOutstanding, listAttendanceContracts, recommendationsOf, registerDocumentTypeProvider };
2890
3725
  //# sourceMappingURL=index.js.map
2891
3726
  //# sourceMappingURL=index.js.map