@fayz-ai/plugin-forms 0.2.0 → 0.8.1

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 (69) hide show
  1. package/README.md +2 -0
  2. package/dist/{FormBuilder-KDANEL6Z.js → FormBuilder-7SWZQKFN.js} +130 -23
  3. package/dist/FormBuilder-7SWZQKFN.js.map +1 -0
  4. package/dist/{FormBuilder-4VKYVZAC.cjs → FormBuilder-YB75LEEH.cjs} +129 -22
  5. package/dist/FormBuilder-YB75LEEH.cjs.map +1 -0
  6. package/dist/agent-tools.d.ts +4 -0
  7. package/dist/agent-tools.d.ts.map +1 -0
  8. package/dist/chunk-QM2AF3DZ.cjs +446 -0
  9. package/dist/chunk-QM2AF3DZ.cjs.map +1 -0
  10. package/dist/chunk-W2ZNJGQC.js +438 -0
  11. package/dist/chunk-W2ZNJGQC.js.map +1 -0
  12. package/dist/components/DocumentFormDialog.d.ts +3 -1
  13. package/dist/components/DocumentFormDialog.d.ts.map +1 -1
  14. package/dist/components/DocumentList.d.ts.map +1 -1
  15. package/dist/components/FieldConfigDialog.d.ts.map +1 -1
  16. package/dist/components/FormBuilder.d.ts.map +1 -1
  17. package/dist/components/FormViewer.d.ts.map +1 -1
  18. package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
  19. package/dist/data/supabase.d.ts.map +1 -1
  20. package/dist/data/tables.d.ts +7 -0
  21. package/dist/data/tables.d.ts.map +1 -0
  22. package/dist/index.cjs +168 -72
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +168 -72
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/person-fields.d.ts +16 -0
  28. package/dist/lib/person-fields.d.ts.map +1 -0
  29. package/dist/lib/print.d.ts +17 -0
  30. package/dist/lib/print.d.ts.map +1 -0
  31. package/dist/lib/tenant.d.ts.map +1 -1
  32. package/dist/locales/en.d.ts.map +1 -1
  33. package/dist/locales/pt-BR.d.ts.map +1 -1
  34. package/dist/migrations/index.d.ts +9 -0
  35. package/dist/migrations/index.d.ts.map +1 -0
  36. package/dist/types.d.ts +15 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
  39. package/dist/views/TemplateListView.d.ts.map +1 -1
  40. package/package.json +10 -4
  41. package/src/agent-tools.ts +73 -0
  42. package/src/components/DocumentFormDialog.tsx +37 -16
  43. package/src/components/DocumentList.tsx +12 -8
  44. package/src/components/FieldConfigDialog.tsx +40 -0
  45. package/src/components/FormBuilder.tsx +104 -22
  46. package/src/components/FormViewer.tsx +11 -1
  47. package/src/components/PersonDocumentsWidget.tsx +10 -1
  48. package/src/data/supabase.ts +22 -19
  49. package/src/data/tables.ts +7 -0
  50. package/src/index.ts +23 -1
  51. package/src/lib/person-fields.ts +67 -0
  52. package/src/lib/print.ts +207 -0
  53. package/src/lib/tenant.ts +6 -2
  54. package/src/locales/en.ts +20 -0
  55. package/src/locales/pt-BR.ts +20 -0
  56. package/src/migrations/000_plg_rename.sql +21 -0
  57. package/src/migrations/001_frm_base.sql +92 -56
  58. package/src/migrations/002_document_archetype.sql +130 -42
  59. package/src/migrations/003_agent_rpcs.sql +174 -0
  60. package/src/migrations/index.ts +610 -0
  61. package/src/types.ts +18 -4
  62. package/src/views/CustomFormsSettingsTab.tsx +23 -16
  63. package/src/views/TemplateListView.tsx +29 -18
  64. package/dist/FormBuilder-4VKYVZAC.cjs.map +0 -1
  65. package/dist/FormBuilder-KDANEL6Z.js.map +0 -1
  66. package/dist/chunk-32I43T37.js +0 -198
  67. package/dist/chunk-32I43T37.js.map +0 -1
  68. package/dist/chunk-XLUULIVL.cjs +0 -200
  69. package/dist/chunk-XLUULIVL.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AASnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAA;AAyCxD,YAAY,EAAE,wBAAwB,EAAE,CAAA;AACxC,OAAO,EAAE,4BAA4B,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAEnH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,cAAc,CA6J1F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAUnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAA;AA0CxD,YAAY,EAAE,wBAAwB,EAAE,CAAA;AACxC,OAAO,EAAE,4BAA4B,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAEnH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAiL1F"}
package/dist/index.js CHANGED
@@ -1,12 +1,20 @@
1
- import { FormRenderer } from './chunk-32I43T37.js';
1
+ import { createOrUpdateFormTool, prefillFromPerson, FormRenderer, printDocument } from './chunk-W2ZNJGQC.js';
2
2
  import React2, { useState, useCallback, useRef, useEffect } from 'react';
3
3
  import { registerTranslations, createSafeDataProvider, useTranslation, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
4
- import { PluginSettingsPanel } from '@fayz-ai/saas';
4
+ import { PluginSettingsPanel, useLimitGuard, invalidateLimit, PermissionGate } from '@fayz-ai/saas';
5
5
  import { create } from 'zustand';
6
- import { FileText, Image, Paperclip, Search, Plus, Pencil, Trash2, ArrowLeft, Loader2, Save, ChevronDown, Upload, Eye } from 'lucide-react';
6
+ import { FileText, Image, Paperclip, Search, Plus, Pencil, Archive, ArrowLeft, Printer, ChevronDown, Upload, Eye } from 'lucide-react';
7
7
  import { Input, Button, Card, CardContent, Badge, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, DropdownItem } from '@fayz-ai/ui';
8
8
  import { jsx, jsxs } from 'react/jsx-runtime';
9
9
 
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
+
10
18
  // src/data/mock.ts
11
19
  function uuid() {
12
20
  return crypto.randomUUID();
@@ -170,14 +178,11 @@ function createMockFormsProvider() {
170
178
  }
171
179
  };
172
180
  }
173
-
174
- // src/lib/tenant.ts
175
- var currentTenantId;
176
181
  function getFormsTenantId() {
177
- return currentTenantId;
182
+ return getActiveTenantId();
178
183
  }
179
184
  function getTenantId() {
180
- return getActiveTenantId() ?? "";
185
+ return getFormsTenantId() ?? getActiveTenantId() ?? "";
181
186
  }
182
187
  function getClient() {
183
188
  const supabase = getSupabaseClientOptional();
@@ -255,7 +260,7 @@ function createSupabaseFormsProvider() {
255
260
  // ── Templates ──────────────────────────────────────────
256
261
  async getTemplates(query) {
257
262
  const db = getClient();
258
- let qb = db.from("frm_templates").select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_current", true).eq("is_deleted", false);
263
+ 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);
259
264
  if (query.category) qb = qb.eq("category", query.category);
260
265
  if (query.search) qb = qb.ilike("name", `%${query.search}%`);
261
266
  const page = query.page ?? 1;
@@ -267,14 +272,14 @@ function createSupabaseFormsProvider() {
267
272
  },
268
273
  async getTemplateById(id) {
269
274
  const db = getClient();
270
- const { data, error } = await db.from("frm_templates").select("*").eq("id", id).eq("is_deleted", false).single();
275
+ const { data, error } = await db.from(T.templates).select("*").eq("id", id).eq("is_deleted", false).single();
271
276
  if (error || !data) return null;
272
277
  return mapTemplate(data);
273
278
  },
274
279
  async createTemplate(input) {
275
280
  const db = getClient();
276
281
  const tenantId = getTenantId();
277
- const { data, error } = await db.from("frm_templates").insert({
282
+ const { data, error } = await db.from(T.templates).insert({
278
283
  tenant_id: tenantId,
279
284
  name: input.name,
280
285
  description: input.description,
@@ -290,12 +295,12 @@ function createSupabaseFormsProvider() {
290
295
  async updateTemplate(id, input) {
291
296
  const db = getClient();
292
297
  const tenantId = getTenantId();
293
- const { count } = await db.from("frm_documents").select("document_id", { count: "exact", head: true }).eq("template_id", id);
298
+ const { count } = await db.from(T.documents).select("document_id", { count: "exact", head: true }).eq("template_id", id);
294
299
  const hasDocuments = (count ?? 0) > 0;
295
300
  if (hasDocuments && input.schema) {
296
301
  const existing = await this.getTemplateById(id);
297
302
  if (!existing) throw new Error("Template not found");
298
- const { data: newRow, error: insertError } = await db.from("frm_templates").insert({
303
+ const { data: newRow, error: insertError } = await db.from(T.templates).insert({
299
304
  tenant_id: tenantId,
300
305
  name: input.name ?? existing.name,
301
306
  description: input.description ?? existing.description,
@@ -310,7 +315,7 @@ function createSupabaseFormsProvider() {
310
315
  parent_id: existing.parentId ?? existing.id
311
316
  }).select().single();
312
317
  if (insertError) throw new Error(insertError.message);
313
- await db.from("frm_templates").update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
318
+ await db.from(T.templates).update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
314
319
  return mapTemplate(newRow);
315
320
  }
316
321
  const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
@@ -322,16 +327,16 @@ function createSupabaseFormsProvider() {
322
327
  if (input.tags !== void 0) updates.tags = input.tags;
323
328
  if (input.metadata !== void 0) updates.metadata = input.metadata;
324
329
  if (input.isActive !== void 0) updates.is_active = input.isActive;
325
- const { data, error } = await db.from("frm_templates").update(updates).eq("id", id).select().single();
330
+ const { data, error } = await db.from(T.templates).update(updates).eq("id", id).select().single();
326
331
  if (error) throw new Error(error.message);
327
332
  return mapTemplate(data);
328
333
  },
329
334
  async deleteTemplate(id) {
330
335
  const db = getClient();
331
- const { error } = await db.from("frm_templates").update({ is_deleted: true, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
336
+ const { error } = await db.from(T.templates).update({ is_deleted: true, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
332
337
  if (error) throw new Error(error.message);
333
338
  },
334
- // ── Documents (saas_core.documents + frm_documents extension) ──
339
+ // ── Documents (public.documents + plg_forms_documents extension) ──
335
340
  async getDocuments(query) {
336
341
  const db = getClient();
337
342
  let qb = db.from("v_documents").select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_active", true);
@@ -358,7 +363,7 @@ function createSupabaseFormsProvider() {
358
363
  async createDocument(input) {
359
364
  const tenantId = getTenantId();
360
365
  const isForm = !!input.templateId;
361
- const { data: coreDoc, error: coreError } = await getClient().from("frm_core_documents").insert({
366
+ const { data: coreDoc, error: coreError } = await getClient().from("documents").insert({
362
367
  tenant_id: tenantId,
363
368
  kind: input.kind ?? (isForm ? "form" : "attachment"),
364
369
  person_id: input.personId,
@@ -373,14 +378,14 @@ function createSupabaseFormsProvider() {
373
378
  if (coreError) throw new Error(coreError.message);
374
379
  if (isForm && input.templateId) {
375
380
  const db = getClient();
376
- const { error: extError } = await db.from("frm_documents").insert({
381
+ const { error: extError } = await db.from(T.documents).insert({
377
382
  document_id: coreDoc.id,
378
383
  tenant_id: tenantId,
379
384
  template_id: input.templateId,
380
385
  data: input.data ?? {}
381
386
  });
382
387
  if (extError) {
383
- await getClient().from("frm_core_documents").delete().eq("id", coreDoc.id);
388
+ await getClient().from("documents").delete().eq("id", coreDoc.id);
384
389
  throw new Error(extError.message);
385
390
  }
386
391
  }
@@ -393,23 +398,23 @@ function createSupabaseFormsProvider() {
393
398
  if (input.status !== void 0) coreUpdates.status = input.status;
394
399
  if (input.title !== void 0) coreUpdates.title = input.title;
395
400
  if (input.notes !== void 0) coreUpdates.notes = input.notes;
396
- const { error: coreError } = await getClient().from("frm_core_documents").update(coreUpdates).eq("id", id);
401
+ const { error: coreError } = await getClient().from("documents").update(coreUpdates).eq("id", id);
397
402
  if (coreError) throw new Error(coreError.message);
398
403
  if (input.data !== void 0) {
399
404
  const db = getClient();
400
- await db.from("frm_documents").update({ data: input.data, updated_at: now2 }).eq("document_id", id);
405
+ await db.from(T.documents).update({ data: input.data, updated_at: now2 }).eq("document_id", id);
401
406
  }
402
407
  const doc = await this.getDocumentById(id);
403
408
  return doc;
404
409
  },
405
410
  async deleteDocument(id) {
406
- const { error } = await getClient().from("frm_core_documents").update({ is_active: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
411
+ const { error } = await getClient().from("documents").update({ is_active: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
407
412
  if (error) throw new Error(error.message);
408
413
  },
409
414
  // ── Files ──────────────────────────────────────────────
410
415
  async getDocumentFiles(documentId) {
411
416
  const db = getClient();
412
- const { data, error } = await db.from("frm_document_files").select("*").eq("document_id", documentId).order("sort_order");
417
+ const { data, error } = await db.from(T.documentFiles).select("*").eq("document_id", documentId).order("sort_order");
413
418
  if (error) throw new Error(error.message);
414
419
  return (data ?? []).map(mapFile);
415
420
  },
@@ -421,7 +426,7 @@ function createSupabaseFormsProvider() {
421
426
  const { error: uploadError } = await db.storage.from("documents").upload(storagePath, file, { contentType: file.type });
422
427
  if (uploadError) throw new Error(uploadError.message);
423
428
  const { data: urlData } = db.storage.from("documents").getPublicUrl(storagePath);
424
- const { data, error } = await db.from("frm_document_files").insert({
429
+ const { data, error } = await db.from(T.documentFiles).insert({
425
430
  tenant_id: tenantId,
426
431
  document_id: documentId,
427
432
  field_key: fieldKey,
@@ -435,7 +440,7 @@ function createSupabaseFormsProvider() {
435
440
  },
436
441
  async deleteFile(fileId) {
437
442
  const db = getClient();
438
- const { error } = await db.from("frm_document_files").delete().eq("id", fileId);
443
+ const { error } = await db.from(T.documentFiles).delete().eq("id", fileId);
439
444
  if (error) throw new Error(error.message);
440
445
  }
441
446
  };
@@ -561,6 +566,7 @@ var en = {
561
566
  "customForms.noTemplates": "No form templates yet",
562
567
  "customForms.noTemplatesDescription": "Create your first custom form to start collecting structured data.",
563
568
  "customForms.deleteTemplateConfirm": "Delete this form template? Existing documents will be preserved.",
569
+ "customForms.archiveTemplateConfirm": "Archive this form template? It leaves the list and the document picker, but is kept and existing documents are preserved.",
564
570
  // Categories
565
571
  "customForms.category.anamnesis": "Anamnesis",
566
572
  "customForms.category.evolution": "Evolution Note",
@@ -582,6 +588,21 @@ var en = {
582
588
  "customForms.builder.addOption": "Add option",
583
589
  "customForms.builder.colSpan": "Column span",
584
590
  "customForms.builder.deleteField": "Remove field",
591
+ "customForms.builder.mapToPerson": "Auto-fill from record",
592
+ "customForms.builder.mapNone": "Don't auto-fill",
593
+ "customForms.builder.mapHint": "When creating the document from a person's profile, this field is pre-filled with the chosen value (still editable).",
594
+ "customForms.preview.editor": "Editor",
595
+ "customForms.preview.print": "Print",
596
+ // Person field mappings (auto-fill)
597
+ "customForms.personField.name": "Name",
598
+ "customForms.personField.documentNumber": "ID / Document",
599
+ "customForms.personField.email": "Email",
600
+ "customForms.personField.phone": "Phone",
601
+ "customForms.personField.dateOfBirth": "Date of Birth",
602
+ "customForms.personField.guardianName": "Guardian",
603
+ "customForms.personField.address": "Address",
604
+ "customForms.personField.city": "City",
605
+ "customForms.personField.state": "State",
585
606
  // Field types
586
607
  "customForms.fieldType.title": "Title",
587
608
  "customForms.fieldType.text": "Simple Text",
@@ -605,6 +626,9 @@ var en = {
605
626
  "customForms.saveAsDraft": "Save as Draft",
606
627
  "customForms.saveAndComplete": "Save & Complete",
607
628
  "customForms.deleteDocumentConfirm": "Delete this document?",
629
+ "customForms.archive": "Archive",
630
+ "customForms.archiveDocumentConfirm": "Archive this document? It leaves the active list but is kept on file.",
631
+ "customForms.printPdf": "Print / PDF",
608
632
  // Registry
609
633
  "registry.form-categories": "Categories",
610
634
  "registry.form-categories.description": "Categories for organizing form templates",
@@ -631,6 +655,7 @@ var ptBR = {
631
655
  "customForms.noTemplates": "Nenhum modelo criado",
632
656
  "customForms.noTemplatesDescription": "Crie seu primeiro formul\xE1rio personalizado para come\xE7ar a coletar dados estruturados.",
633
657
  "customForms.deleteTemplateConfirm": "Excluir este modelo de formul\xE1rio? Documentos existentes ser\xE3o preservados.",
658
+ "customForms.archiveTemplateConfirm": "Arquivar este modelo? Ele sai da lista e do seletor de documentos, mas fica guardado e os documentos existentes s\xE3o preservados.",
634
659
  // Categories
635
660
  "customForms.category.anamnesis": "Anamnese",
636
661
  "customForms.category.evolution": "Evolu\xE7\xE3o",
@@ -652,6 +677,21 @@ var ptBR = {
652
677
  "customForms.builder.addOption": "Adicionar op\xE7\xE3o",
653
678
  "customForms.builder.colSpan": "Largura em colunas",
654
679
  "customForms.builder.deleteField": "Remover campo",
680
+ "customForms.builder.mapToPerson": "Preencher com dado da ficha",
681
+ "customForms.builder.mapNone": "N\xE3o preencher",
682
+ "customForms.builder.mapHint": "Ao criar o documento na ficha da pessoa, este campo j\xE1 vem preenchido com o dado escolhido (edit\xE1vel).",
683
+ "customForms.preview.editor": "Editor",
684
+ "customForms.preview.print": "Impress\xE3o",
685
+ // Person field mappings (auto-fill)
686
+ "customForms.personField.name": "Nome",
687
+ "customForms.personField.documentNumber": "CPF / Documento",
688
+ "customForms.personField.email": "E-mail",
689
+ "customForms.personField.phone": "Telefone",
690
+ "customForms.personField.dateOfBirth": "Data de Nascimento",
691
+ "customForms.personField.guardianName": "Respons\xE1vel",
692
+ "customForms.personField.address": "Endere\xE7o",
693
+ "customForms.personField.city": "Cidade",
694
+ "customForms.personField.state": "Estado",
655
695
  // Field types
656
696
  "customForms.fieldType.title": "T\xEDtulo",
657
697
  "customForms.fieldType.text": "Texto Simples",
@@ -675,6 +715,9 @@ var ptBR = {
675
715
  "customForms.saveAsDraft": "Salvar Rascunho",
676
716
  "customForms.saveAndComplete": "Salvar e Finalizar",
677
717
  "customForms.deleteDocumentConfirm": "Excluir este documento?",
718
+ "customForms.archive": "Arquivar",
719
+ "customForms.archiveDocumentConfirm": "Arquivar este documento? Ele sai da lista ativa mas fica guardado.",
720
+ "customForms.printPdf": "Imprimir / PDF",
678
721
  // Registry
679
722
  "registry.form-categories": "Categorias",
680
723
  "registry.form-categories.description": "Categorias para organizar modelos de formul\xE1rios",
@@ -720,12 +763,17 @@ function TemplateListView({ store, config, onEdit, onNew }) {
720
763
  const loading = store((s) => s.templatesLoading);
721
764
  const orgId = getFormsTenantId();
722
765
  useEffect(() => {
723
- return;
766
+ if (!orgId) return;
767
+ const timer = setTimeout(() => {
768
+ store.getState().fetchTemplates({ search: search || void 0 });
769
+ }, 50);
770
+ return () => clearTimeout(timer);
724
771
  }, [orgId, search]);
725
- const handleDelete = useCallback(async (template) => {
726
- if (!window.confirm(t("customForms.deleteTemplateConfirm"))) return;
727
- await store.getState().deleteTemplate(template.id);
772
+ const handleArchive = useCallback(async (template) => {
773
+ if (!window.confirm(t("customForms.archiveTemplateConfirm"))) return;
774
+ await store.getState().updateTemplate(template.id, { isActive: false });
728
775
  }, [store, t]);
776
+ const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
729
777
  const categoryLabel = (cat) => t(`customForms.category.${cat}`);
730
778
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
731
779
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
@@ -741,21 +789,21 @@ function TemplateListView({ store, config, onEdit, onNew }) {
741
789
  }
742
790
  )
743
791
  ] }),
744
- /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "h-9", children: [
792
+ /* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "h-9", children: [
745
793
  /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
746
794
  t("customForms.newTemplate")
747
- ] })
795
+ ] }) })
748
796
  ] }),
749
- !loading && templates.length === 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-center justify-center py-16 text-center", children: [
797
+ !loading && visibleTemplates.length === 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-center justify-center py-16 text-center", children: [
750
798
  /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4", children: /* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 text-muted-foreground" }) }),
751
799
  /* @__PURE__ */ jsx("p", { className: "font-medium", children: t("customForms.noTemplates") }),
752
800
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-sm", children: t("customForms.noTemplatesDescription") }),
753
- /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
801
+ /* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
754
802
  /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
755
803
  t("customForms.newTemplate")
756
- ] })
804
+ ] }) })
757
805
  ] }) }),
758
- templates.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: templates.map((template) => /* @__PURE__ */ jsx(
806
+ visibleTemplates.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: visibleTemplates.map((template) => /* @__PURE__ */ jsx(
759
807
  Card,
760
808
  {
761
809
  className: "group cursor-pointer hover:shadow-md transition-shadow",
@@ -783,10 +831,11 @@ function TemplateListView({ store, config, onEdit, onNew }) {
783
831
  {
784
832
  onClick: (e) => {
785
833
  e.stopPropagation();
786
- handleDelete(template);
834
+ handleArchive(template);
787
835
  },
788
- className: "p-1.5 rounded-md hover:bg-destructive/10",
789
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5 text-destructive" })
836
+ className: "p-1.5 rounded-md hover:bg-muted",
837
+ title: t("customForms.archive"),
838
+ children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
790
839
  }
791
840
  )
792
841
  ] })
@@ -817,29 +866,29 @@ function TemplateListView({ store, config, onEdit, onNew }) {
817
866
  ] }) }, i)) })
818
867
  ] });
819
868
  }
869
+ var ROUTE_BASE = "/settings/custom_forms";
870
+ function readBuilderIdFromHash() {
871
+ const hash = window.location.hash.slice(1) || "/";
872
+ const m = hash.match(/\/settings\/custom_forms\/templates\/([^/]+)/);
873
+ if (!m) return null;
874
+ return m[1] === "new" ? "__new__" : m[1];
875
+ }
820
876
  function CustomFormsSettingsTab({ config, provider, store }) {
821
- const [builderTemplateId, setBuilderTemplateId] = useState(null);
877
+ const [builderTemplateId, setBuilderTemplateId] = useState(readBuilderIdFromHash);
822
878
  const handleNew = useCallback(() => {
823
- setBuilderTemplateId("__new__");
879
+ window.location.hash = `#${ROUTE_BASE}/templates/new`;
824
880
  }, []);
825
881
  const handleEdit = useCallback((template) => {
826
- setBuilderTemplateId(template.id);
882
+ window.location.hash = `#${ROUTE_BASE}/templates/${template.id}`;
827
883
  }, []);
828
884
  const handleBackFromBuilder = useCallback(() => {
829
- setBuilderTemplateId(null);
885
+ window.location.hash = `#${ROUTE_BASE}/forms`;
830
886
  store.getState().fetchTemplates();
831
887
  }, [store]);
832
888
  React2.useEffect(() => {
833
- function checkHash() {
834
- const hash = window.location.hash.slice(1) || "/";
835
- const builderMatch = hash.match(/\/settings\/custom_forms\/templates\/(.+)/);
836
- if (builderMatch && builderMatch[1] !== "new") {
837
- setBuilderTemplateId(builderMatch[1]);
838
- } else if (builderMatch && builderMatch[1] === "new") {
839
- setBuilderTemplateId("__new__");
840
- }
841
- }
842
- checkHash();
889
+ const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
890
+ window.addEventListener("hashchange", sync);
891
+ return () => window.removeEventListener("hashchange", sync);
843
892
  }, []);
844
893
  return builderTemplateId ? /* @__PURE__ */ jsx(
845
894
  BuilderWrapper,
@@ -868,7 +917,7 @@ function BuilderWrapper({
868
917
  onBack
869
918
  }) {
870
919
  const FormBuilderLazy = React2.lazy(
871
- () => import('./FormBuilder-KDANEL6Z.js').then((m) => ({ default: m.FormBuilder }))
920
+ () => import('./FormBuilder-7SWZQKFN.js').then((m) => ({ default: m.FormBuilder }))
872
921
  );
873
922
  return /* @__PURE__ */ jsx(
874
923
  React2.Suspense,
@@ -934,7 +983,7 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
934
983
  setLoading(true);
935
984
  try {
936
985
  const result = await provider.getDocuments({ personId });
937
- setDocuments(result.data);
986
+ setDocuments(result.data.filter((d) => d.status !== "archived"));
938
987
  } finally {
939
988
  setLoading(false);
940
989
  }
@@ -947,9 +996,9 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
947
996
  const unsub = store.subscribe(() => fetchDocs());
948
997
  return unsub;
949
998
  }, [store, fetchDocs]);
950
- const handleDelete = useCallback(async (doc) => {
951
- if (!window.confirm(t("customForms.deleteDocumentConfirm"))) return;
952
- await provider.deleteDocument(doc.id);
999
+ const handleArchive = useCallback(async (doc) => {
1000
+ if (!window.confirm(t("customForms.archiveDocumentConfirm"))) return;
1001
+ await provider.updateDocument(doc.id, { status: "archived" });
953
1002
  setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
954
1003
  }, [provider, t]);
955
1004
  const handleDragEnter = useCallback((e) => {
@@ -1047,10 +1096,11 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
1047
1096
  {
1048
1097
  onClick: (e) => {
1049
1098
  e.stopPropagation();
1050
- handleDelete(doc);
1099
+ handleArchive(doc);
1051
1100
  },
1052
- className: "p-1.5 rounded-md hover:bg-destructive/10",
1053
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5 text-destructive" })
1101
+ className: "p-1.5 rounded-md hover:bg-muted",
1102
+ title: t("customForms.archive"),
1103
+ children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
1054
1104
  }
1055
1105
  )
1056
1106
  ] })
@@ -1073,23 +1123,34 @@ function DocumentFormDialog({
1073
1123
  existingDocument,
1074
1124
  personId,
1075
1125
  personName,
1126
+ person,
1076
1127
  provider,
1077
1128
  store,
1078
1129
  onSaved,
1079
1130
  onBack
1080
1131
  }) {
1081
1132
  const t = useTranslation();
1133
+ const guardDocuments = useLimitGuard("documents_month");
1082
1134
  const [template, setTemplate] = useState(initialTemplate);
1083
- const [data, setData] = useState(existingDocument?.data ?? {});
1135
+ const [data, setData] = useState(
1136
+ () => existingDocument ? existingDocument.data : prefillFromPerson(initialTemplate?.schema, person)
1137
+ );
1084
1138
  const [saving, setSaving] = useState(false);
1085
1139
  useEffect(() => {
1086
1140
  if (existingDocument && !template) {
1087
1141
  provider.getTemplateById(existingDocument.templateId).then(setTemplate);
1088
1142
  }
1089
1143
  }, [existingDocument, template, provider]);
1144
+ const prefilled = React2.useRef(!!initialTemplate || !!existingDocument);
1145
+ useEffect(() => {
1146
+ if (prefilled.current || !template || existingDocument) return;
1147
+ prefilled.current = true;
1148
+ setData((prev) => prefillFromPerson(template.schema, person, prev));
1149
+ }, [template, existingDocument, person]);
1090
1150
  const handleSave = useCallback(
1091
1151
  async (status) => {
1092
1152
  if (!template) return;
1153
+ if (!existingDocument && await guardDocuments() === "blocked") return;
1093
1154
  setSaving(true);
1094
1155
  try {
1095
1156
  const autoTitle = generateTitle(template.name, personName);
@@ -1103,18 +1164,21 @@ function DocumentFormDialog({
1103
1164
  data,
1104
1165
  status
1105
1166
  });
1167
+ invalidateLimit("documents_month");
1106
1168
  }
1107
1169
  onSaved();
1108
1170
  } finally {
1109
1171
  setSaving(false);
1110
1172
  }
1111
1173
  },
1112
- [template, existingDocument, data, personId, personName, store, onSaved]
1174
+ [template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
1113
1175
  );
1114
1176
  const snapshot = React2.useRef(null);
1115
1177
  useEffect(() => {
1116
- if (template && snapshot.current === null) snapshot.current = JSON.stringify(data);
1117
- }, [template]);
1178
+ if (template && snapshot.current === null) {
1179
+ snapshot.current = JSON.stringify(existingDocument?.data ?? {});
1180
+ }
1181
+ }, [template, existingDocument]);
1118
1182
  const dirty = snapshot.current !== null && JSON.stringify(data) !== snapshot.current;
1119
1183
  useSaveBar({
1120
1184
  dirty,
@@ -1156,11 +1220,7 @@ function DocumentFormDialog({
1156
1220
  data,
1157
1221
  onChange: setData
1158
1222
  }
1159
- ) }),
1160
- /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => handleSave("draft"), disabled: saving, children: [
1161
- saving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 mr-1.5 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3.5 w-3.5 mr-1.5" }),
1162
- t("customForms.saveAsDraft")
1163
- ] }) })
1223
+ ) })
1164
1224
  ] });
1165
1225
  }
1166
1226
  var STATUS_COLORS2 = {
@@ -1197,6 +1257,19 @@ function FormViewer({ document: doc, provider, onBack, onEdit }) {
1197
1257
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: new Date(doc.createdAt).toLocaleDateString() })
1198
1258
  ] }),
1199
1259
  /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${STATUS_COLORS2[doc.status] ?? ""}`, children: t(`customForms.status.${doc.status}`) }),
1260
+ /* @__PURE__ */ jsxs(
1261
+ Button,
1262
+ {
1263
+ variant: "outline",
1264
+ size: "sm",
1265
+ className: "h-8",
1266
+ onClick: () => printDocument(template, doc),
1267
+ children: [
1268
+ /* @__PURE__ */ jsx(Printer, { className: "h-3 w-3 mr-1" }),
1269
+ t("customForms.printPdf")
1270
+ ]
1271
+ }
1272
+ ),
1200
1273
  onEdit && doc.status === "draft" && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => onEdit(doc), children: [
1201
1274
  /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 mr-1" }),
1202
1275
  t("common.edit")
@@ -1280,6 +1353,7 @@ function AddDocumentDropdown({ personId, onSelect }) {
1280
1353
  }
1281
1354
  function PersonDocumentsWidget({ item, config, provider, store }) {
1282
1355
  const t = useTranslation();
1356
+ const guardDocuments = useLimitGuard("documents_month");
1283
1357
  const [view, setView] = useState({ type: "list" });
1284
1358
  const fileInputRef = useRef(null);
1285
1359
  const [pendingFileAccept, setPendingFileAccept] = useState("*/*");
@@ -1304,6 +1378,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1304
1378
  }
1305
1379
  }, [provider]);
1306
1380
  const handleFiles = useCallback(async (files) => {
1381
+ if (await guardDocuments(files.length) === "blocked") return;
1307
1382
  for (const file of files) {
1308
1383
  const isImage = file.type.startsWith("image/");
1309
1384
  await store.getState().createDocument({
@@ -1316,7 +1391,8 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1316
1391
  status: "completed"
1317
1392
  });
1318
1393
  }
1319
- }, [store, item.id]);
1394
+ invalidateLimit("documents_month");
1395
+ }, [store, item.id, guardDocuments]);
1320
1396
  const handleFileInputChange = useCallback((e) => {
1321
1397
  const files = Array.from(e.target.files ?? []);
1322
1398
  if (files.length > 0) handleFiles(files);
@@ -1341,6 +1417,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1341
1417
  template: view.template,
1342
1418
  personId: item.id,
1343
1419
  personName: item.name,
1420
+ person: item,
1344
1421
  provider,
1345
1422
  store,
1346
1423
  onSaved: handleSaved,
@@ -1372,6 +1449,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1372
1449
  existingDocument: view.document,
1373
1450
  personId: item.id,
1374
1451
  personName: item.name,
1452
+ person: item,
1375
1453
  provider,
1376
1454
  store,
1377
1455
  onSaved: handleSaved,
@@ -1493,7 +1571,7 @@ function createCustomFormsPlugin(options) {
1493
1571
  { key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
1494
1572
  ],
1495
1573
  data: {
1496
- table: "frm_categories",
1574
+ table: T.categories,
1497
1575
  tenantScoped: true
1498
1576
  }
1499
1577
  },
@@ -1512,6 +1590,9 @@ function createCustomFormsPlugin(options) {
1512
1590
  const SettingsComponent = () => React2.createElement(PluginSettingsPanel, {
1513
1591
  title: config.labels.settingsLabel,
1514
1592
  subtitle: config.labels.settingsSubtitle,
1593
+ // The settings shell already renders the section label as the page H1;
1594
+ // hide the panel's own duplicate title (keeps the subtitle).
1595
+ hideTitle: true,
1515
1596
  customTabs: [
1516
1597
  {
1517
1598
  id: "forms",
@@ -1539,6 +1620,10 @@ function createCustomFormsPlugin(options) {
1539
1620
  declaredFeatures: [
1540
1621
  { id: "custom_forms", label: config.labels.pageTitle, group: config.labels.pageTitle }
1541
1622
  ],
1623
+ declaredLimits: [
1624
+ { key: "form_templates", label: "Form templates", table: "plg_forms_templates" },
1625
+ { key: "documents_month", label: "Documents / month", table: "documents", period: "month" }
1626
+ ],
1542
1627
  navigation: [],
1543
1628
  routes: [],
1544
1629
  settings: [
@@ -1603,6 +1688,17 @@ function createCustomFormsPlugin(options) {
1603
1688
  { label: "Show this client's documents" }
1604
1689
  ],
1605
1690
  permission: { feature: "custom_forms", action: "read" }
1691
+ },
1692
+ // Server-plane write: builds/edits a form template via the pool RPC, so
1693
+ // the assistant can montar formulários from any page or channel.
1694
+ createOrUpdateFormTool
1695
+ ],
1696
+ declaredRpcs: [
1697
+ {
1698
+ name: "agent_forms_upsert_template",
1699
+ kind: "write",
1700
+ description: "Guarded form-template upsert: agent_guard (role\u2192plan\u2192form_templates cap), grid layout computed server-side, create or edit by id, audited.",
1701
+ audits: true
1606
1702
  }
1607
1703
  ],
1608
1704
  locales: customFormsLocales