@fayz-ai/plugin-forms 0.8.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 (62) hide show
  1. package/dist/{FormBuilder-KDANEL6Z.js → FormBuilder-7SWZQKFN.js} +130 -23
  2. package/dist/FormBuilder-7SWZQKFN.js.map +1 -0
  3. package/dist/{FormBuilder-4VKYVZAC.cjs → FormBuilder-YB75LEEH.cjs} +129 -22
  4. package/dist/FormBuilder-YB75LEEH.cjs.map +1 -0
  5. package/dist/agent-tools.d.ts +4 -0
  6. package/dist/agent-tools.d.ts.map +1 -0
  7. package/dist/chunk-QM2AF3DZ.cjs +446 -0
  8. package/dist/chunk-QM2AF3DZ.cjs.map +1 -0
  9. package/dist/chunk-W2ZNJGQC.js +438 -0
  10. package/dist/chunk-W2ZNJGQC.js.map +1 -0
  11. package/dist/components/DocumentFormDialog.d.ts +3 -1
  12. package/dist/components/DocumentFormDialog.d.ts.map +1 -1
  13. package/dist/components/DocumentList.d.ts.map +1 -1
  14. package/dist/components/FieldConfigDialog.d.ts.map +1 -1
  15. package/dist/components/FormBuilder.d.ts.map +1 -1
  16. package/dist/components/FormViewer.d.ts.map +1 -1
  17. package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
  18. package/dist/data/supabase.d.ts.map +1 -1
  19. package/dist/index.cjs +142 -54
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +142 -54
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/person-fields.d.ts +16 -0
  25. package/dist/lib/person-fields.d.ts.map +1 -0
  26. package/dist/lib/print.d.ts +17 -0
  27. package/dist/lib/print.d.ts.map +1 -0
  28. package/dist/lib/tenant.d.ts.map +1 -1
  29. package/dist/locales/en.d.ts.map +1 -1
  30. package/dist/locales/pt-BR.d.ts.map +1 -1
  31. package/dist/migrations/index.d.ts +1 -0
  32. package/dist/migrations/index.d.ts.map +1 -1
  33. package/dist/types.d.ts +14 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
  36. package/dist/views/TemplateListView.d.ts.map +1 -1
  37. package/package.json +4 -4
  38. package/src/agent-tools.ts +73 -0
  39. package/src/components/DocumentFormDialog.tsx +37 -16
  40. package/src/components/DocumentList.tsx +12 -8
  41. package/src/components/FieldConfigDialog.tsx +40 -0
  42. package/src/components/FormBuilder.tsx +104 -22
  43. package/src/components/FormViewer.tsx +11 -1
  44. package/src/components/PersonDocumentsWidget.tsx +10 -1
  45. package/src/data/supabase.ts +2 -0
  46. package/src/index.ts +21 -0
  47. package/src/lib/person-fields.ts +67 -0
  48. package/src/lib/print.ts +207 -0
  49. package/src/lib/tenant.ts +6 -2
  50. package/src/locales/en.ts +20 -0
  51. package/src/locales/pt-BR.ts +20 -0
  52. package/src/migrations/003_agent_rpcs.sql +174 -0
  53. package/src/migrations/index.ts +178 -1
  54. package/src/types.ts +14 -0
  55. package/src/views/CustomFormsSettingsTab.tsx +23 -16
  56. package/src/views/TemplateListView.tsx +29 -18
  57. package/dist/FormBuilder-4VKYVZAC.cjs.map +0 -1
  58. package/dist/FormBuilder-KDANEL6Z.js.map +0 -1
  59. package/dist/chunk-32I43T37.js +0 -198
  60. package/dist/chunk-32I43T37.js.map +0 -1
  61. package/dist/chunk-XLUULIVL.cjs +0 -200
  62. package/dist/chunk-XLUULIVL.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkXLUULIVL_cjs = require('./chunk-XLUULIVL.cjs');
3
+ var chunkQM2AF3DZ_cjs = require('./chunk-QM2AF3DZ.cjs');
4
4
  var React2 = require('react');
5
5
  var core = require('@fayz-ai/core');
6
6
  var saas = require('@fayz-ai/saas');
@@ -184,14 +184,11 @@ function createMockFormsProvider() {
184
184
  }
185
185
  };
186
186
  }
187
-
188
- // src/lib/tenant.ts
189
- var currentTenantId;
190
187
  function getFormsTenantId() {
191
- return currentTenantId;
188
+ return core.getActiveTenantId();
192
189
  }
193
190
  function getTenantId() {
194
- return core.getActiveTenantId() ?? "";
191
+ return getFormsTenantId() ?? core.getActiveTenantId() ?? "";
195
192
  }
196
193
  function getClient() {
197
194
  const supabase = core.getSupabaseClientOptional();
@@ -269,7 +266,7 @@ function createSupabaseFormsProvider() {
269
266
  // ── Templates ──────────────────────────────────────────
270
267
  async getTemplates(query) {
271
268
  const db = getClient();
272
- let qb = db.from(T.templates).select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_current", true).eq("is_deleted", false);
269
+ 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);
273
270
  if (query.category) qb = qb.eq("category", query.category);
274
271
  if (query.search) qb = qb.ilike("name", `%${query.search}%`);
275
272
  const page = query.page ?? 1;
@@ -575,6 +572,7 @@ var en = {
575
572
  "customForms.noTemplates": "No form templates yet",
576
573
  "customForms.noTemplatesDescription": "Create your first custom form to start collecting structured data.",
577
574
  "customForms.deleteTemplateConfirm": "Delete this form template? Existing documents will be preserved.",
575
+ "customForms.archiveTemplateConfirm": "Archive this form template? It leaves the list and the document picker, but is kept and existing documents are preserved.",
578
576
  // Categories
579
577
  "customForms.category.anamnesis": "Anamnesis",
580
578
  "customForms.category.evolution": "Evolution Note",
@@ -596,6 +594,21 @@ var en = {
596
594
  "customForms.builder.addOption": "Add option",
597
595
  "customForms.builder.colSpan": "Column span",
598
596
  "customForms.builder.deleteField": "Remove field",
597
+ "customForms.builder.mapToPerson": "Auto-fill from record",
598
+ "customForms.builder.mapNone": "Don't auto-fill",
599
+ "customForms.builder.mapHint": "When creating the document from a person's profile, this field is pre-filled with the chosen value (still editable).",
600
+ "customForms.preview.editor": "Editor",
601
+ "customForms.preview.print": "Print",
602
+ // Person field mappings (auto-fill)
603
+ "customForms.personField.name": "Name",
604
+ "customForms.personField.documentNumber": "ID / Document",
605
+ "customForms.personField.email": "Email",
606
+ "customForms.personField.phone": "Phone",
607
+ "customForms.personField.dateOfBirth": "Date of Birth",
608
+ "customForms.personField.guardianName": "Guardian",
609
+ "customForms.personField.address": "Address",
610
+ "customForms.personField.city": "City",
611
+ "customForms.personField.state": "State",
599
612
  // Field types
600
613
  "customForms.fieldType.title": "Title",
601
614
  "customForms.fieldType.text": "Simple Text",
@@ -619,6 +632,9 @@ var en = {
619
632
  "customForms.saveAsDraft": "Save as Draft",
620
633
  "customForms.saveAndComplete": "Save & Complete",
621
634
  "customForms.deleteDocumentConfirm": "Delete this document?",
635
+ "customForms.archive": "Archive",
636
+ "customForms.archiveDocumentConfirm": "Archive this document? It leaves the active list but is kept on file.",
637
+ "customForms.printPdf": "Print / PDF",
622
638
  // Registry
623
639
  "registry.form-categories": "Categories",
624
640
  "registry.form-categories.description": "Categories for organizing form templates",
@@ -645,6 +661,7 @@ var ptBR = {
645
661
  "customForms.noTemplates": "Nenhum modelo criado",
646
662
  "customForms.noTemplatesDescription": "Crie seu primeiro formul\xE1rio personalizado para come\xE7ar a coletar dados estruturados.",
647
663
  "customForms.deleteTemplateConfirm": "Excluir este modelo de formul\xE1rio? Documentos existentes ser\xE3o preservados.",
664
+ "customForms.archiveTemplateConfirm": "Arquivar este modelo? Ele sai da lista e do seletor de documentos, mas fica guardado e os documentos existentes s\xE3o preservados.",
648
665
  // Categories
649
666
  "customForms.category.anamnesis": "Anamnese",
650
667
  "customForms.category.evolution": "Evolu\xE7\xE3o",
@@ -666,6 +683,21 @@ var ptBR = {
666
683
  "customForms.builder.addOption": "Adicionar op\xE7\xE3o",
667
684
  "customForms.builder.colSpan": "Largura em colunas",
668
685
  "customForms.builder.deleteField": "Remover campo",
686
+ "customForms.builder.mapToPerson": "Preencher com dado da ficha",
687
+ "customForms.builder.mapNone": "N\xE3o preencher",
688
+ "customForms.builder.mapHint": "Ao criar o documento na ficha da pessoa, este campo j\xE1 vem preenchido com o dado escolhido (edit\xE1vel).",
689
+ "customForms.preview.editor": "Editor",
690
+ "customForms.preview.print": "Impress\xE3o",
691
+ // Person field mappings (auto-fill)
692
+ "customForms.personField.name": "Nome",
693
+ "customForms.personField.documentNumber": "CPF / Documento",
694
+ "customForms.personField.email": "E-mail",
695
+ "customForms.personField.phone": "Telefone",
696
+ "customForms.personField.dateOfBirth": "Data de Nascimento",
697
+ "customForms.personField.guardianName": "Respons\xE1vel",
698
+ "customForms.personField.address": "Endere\xE7o",
699
+ "customForms.personField.city": "Cidade",
700
+ "customForms.personField.state": "Estado",
669
701
  // Field types
670
702
  "customForms.fieldType.title": "T\xEDtulo",
671
703
  "customForms.fieldType.text": "Texto Simples",
@@ -689,6 +721,9 @@ var ptBR = {
689
721
  "customForms.saveAsDraft": "Salvar Rascunho",
690
722
  "customForms.saveAndComplete": "Salvar e Finalizar",
691
723
  "customForms.deleteDocumentConfirm": "Excluir este documento?",
724
+ "customForms.archive": "Arquivar",
725
+ "customForms.archiveDocumentConfirm": "Arquivar este documento? Ele sai da lista ativa mas fica guardado.",
726
+ "customForms.printPdf": "Imprimir / PDF",
692
727
  // Registry
693
728
  "registry.form-categories": "Categorias",
694
729
  "registry.form-categories.description": "Categorias para organizar modelos de formul\xE1rios",
@@ -734,12 +769,17 @@ function TemplateListView({ store, config, onEdit, onNew }) {
734
769
  const loading = store((s) => s.templatesLoading);
735
770
  const orgId = getFormsTenantId();
736
771
  React2.useEffect(() => {
737
- return;
772
+ if (!orgId) return;
773
+ const timer = setTimeout(() => {
774
+ store.getState().fetchTemplates({ search: search || void 0 });
775
+ }, 50);
776
+ return () => clearTimeout(timer);
738
777
  }, [orgId, search]);
739
- const handleDelete = React2.useCallback(async (template) => {
740
- if (!window.confirm(t("customForms.deleteTemplateConfirm"))) return;
741
- await store.getState().deleteTemplate(template.id);
778
+ const handleArchive = React2.useCallback(async (template) => {
779
+ if (!window.confirm(t("customForms.archiveTemplateConfirm"))) return;
780
+ await store.getState().updateTemplate(template.id, { isActive: false });
742
781
  }, [store, t]);
782
+ const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
743
783
  const categoryLabel = (cat) => t(`customForms.category.${cat}`);
744
784
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
745
785
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
@@ -755,21 +795,21 @@ function TemplateListView({ store, config, onEdit, onNew }) {
755
795
  }
756
796
  )
757
797
  ] }),
758
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: onNew, size: "sm", className: "h-9", children: [
798
+ /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: onNew, size: "sm", className: "h-9", children: [
759
799
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
760
800
  t("customForms.newTemplate")
761
- ] })
801
+ ] }) })
762
802
  ] }),
763
- !loading && templates.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.CardContent, { className: "flex flex-col items-center justify-center py-16 text-center", children: [
803
+ !loading && visibleTemplates.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.CardContent, { className: "flex flex-col items-center justify-center py-16 text-center", children: [
764
804
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-5 w-5 text-muted-foreground" }) }),
765
805
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t("customForms.noTemplates") }),
766
806
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-sm", children: t("customForms.noTemplatesDescription") }),
767
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
807
+ /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
768
808
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
769
809
  t("customForms.newTemplate")
770
- ] })
810
+ ] }) })
771
811
  ] }) }),
772
- templates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: templates.map((template) => /* @__PURE__ */ jsxRuntime.jsx(
812
+ visibleTemplates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: visibleTemplates.map((template) => /* @__PURE__ */ jsxRuntime.jsx(
773
813
  ui.Card,
774
814
  {
775
815
  className: "group cursor-pointer hover:shadow-md transition-shadow",
@@ -797,10 +837,11 @@ function TemplateListView({ store, config, onEdit, onNew }) {
797
837
  {
798
838
  onClick: (e) => {
799
839
  e.stopPropagation();
800
- handleDelete(template);
840
+ handleArchive(template);
801
841
  },
802
- className: "p-1.5 rounded-md hover:bg-destructive/10",
803
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5 text-destructive" })
842
+ className: "p-1.5 rounded-md hover:bg-muted",
843
+ title: t("customForms.archive"),
844
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
804
845
  }
805
846
  )
806
847
  ] })
@@ -831,29 +872,29 @@ function TemplateListView({ store, config, onEdit, onNew }) {
831
872
  ] }) }, i)) })
832
873
  ] });
833
874
  }
875
+ var ROUTE_BASE = "/settings/custom_forms";
876
+ function readBuilderIdFromHash() {
877
+ const hash = window.location.hash.slice(1) || "/";
878
+ const m = hash.match(/\/settings\/custom_forms\/templates\/([^/]+)/);
879
+ if (!m) return null;
880
+ return m[1] === "new" ? "__new__" : m[1];
881
+ }
834
882
  function CustomFormsSettingsTab({ config, provider, store }) {
835
- const [builderTemplateId, setBuilderTemplateId] = React2.useState(null);
883
+ const [builderTemplateId, setBuilderTemplateId] = React2.useState(readBuilderIdFromHash);
836
884
  const handleNew = React2.useCallback(() => {
837
- setBuilderTemplateId("__new__");
885
+ window.location.hash = `#${ROUTE_BASE}/templates/new`;
838
886
  }, []);
839
887
  const handleEdit = React2.useCallback((template) => {
840
- setBuilderTemplateId(template.id);
888
+ window.location.hash = `#${ROUTE_BASE}/templates/${template.id}`;
841
889
  }, []);
842
890
  const handleBackFromBuilder = React2.useCallback(() => {
843
- setBuilderTemplateId(null);
891
+ window.location.hash = `#${ROUTE_BASE}/forms`;
844
892
  store.getState().fetchTemplates();
845
893
  }, [store]);
846
894
  React2__default.default.useEffect(() => {
847
- function checkHash() {
848
- const hash = window.location.hash.slice(1) || "/";
849
- const builderMatch = hash.match(/\/settings\/custom_forms\/templates\/(.+)/);
850
- if (builderMatch && builderMatch[1] !== "new") {
851
- setBuilderTemplateId(builderMatch[1]);
852
- } else if (builderMatch && builderMatch[1] === "new") {
853
- setBuilderTemplateId("__new__");
854
- }
855
- }
856
- checkHash();
895
+ const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
896
+ window.addEventListener("hashchange", sync);
897
+ return () => window.removeEventListener("hashchange", sync);
857
898
  }, []);
858
899
  return builderTemplateId ? /* @__PURE__ */ jsxRuntime.jsx(
859
900
  BuilderWrapper,
@@ -882,7 +923,7 @@ function BuilderWrapper({
882
923
  onBack
883
924
  }) {
884
925
  const FormBuilderLazy = React2__default.default.lazy(
885
- () => import('./FormBuilder-4VKYVZAC.cjs').then((m) => ({ default: m.FormBuilder }))
926
+ () => import('./FormBuilder-YB75LEEH.cjs').then((m) => ({ default: m.FormBuilder }))
886
927
  );
887
928
  return /* @__PURE__ */ jsxRuntime.jsx(
888
929
  React2__default.default.Suspense,
@@ -948,7 +989,7 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
948
989
  setLoading(true);
949
990
  try {
950
991
  const result = await provider.getDocuments({ personId });
951
- setDocuments(result.data);
992
+ setDocuments(result.data.filter((d) => d.status !== "archived"));
952
993
  } finally {
953
994
  setLoading(false);
954
995
  }
@@ -961,9 +1002,9 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
961
1002
  const unsub = store.subscribe(() => fetchDocs());
962
1003
  return unsub;
963
1004
  }, [store, fetchDocs]);
964
- const handleDelete = React2.useCallback(async (doc) => {
965
- if (!window.confirm(t("customForms.deleteDocumentConfirm"))) return;
966
- await provider.deleteDocument(doc.id);
1005
+ const handleArchive = React2.useCallback(async (doc) => {
1006
+ if (!window.confirm(t("customForms.archiveDocumentConfirm"))) return;
1007
+ await provider.updateDocument(doc.id, { status: "archived" });
967
1008
  setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
968
1009
  }, [provider, t]);
969
1010
  const handleDragEnter = React2.useCallback((e) => {
@@ -1061,10 +1102,11 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
1061
1102
  {
1062
1103
  onClick: (e) => {
1063
1104
  e.stopPropagation();
1064
- handleDelete(doc);
1105
+ handleArchive(doc);
1065
1106
  },
1066
- className: "p-1.5 rounded-md hover:bg-destructive/10",
1067
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5 text-destructive" })
1107
+ className: "p-1.5 rounded-md hover:bg-muted",
1108
+ title: t("customForms.archive"),
1109
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
1068
1110
  }
1069
1111
  )
1070
1112
  ] })
@@ -1087,23 +1129,34 @@ function DocumentFormDialog({
1087
1129
  existingDocument,
1088
1130
  personId,
1089
1131
  personName,
1132
+ person,
1090
1133
  provider,
1091
1134
  store,
1092
1135
  onSaved,
1093
1136
  onBack
1094
1137
  }) {
1095
1138
  const t = core.useTranslation();
1139
+ const guardDocuments = saas.useLimitGuard("documents_month");
1096
1140
  const [template, setTemplate] = React2.useState(initialTemplate);
1097
- const [data, setData] = React2.useState(existingDocument?.data ?? {});
1141
+ const [data, setData] = React2.useState(
1142
+ () => existingDocument ? existingDocument.data : chunkQM2AF3DZ_cjs.prefillFromPerson(initialTemplate?.schema, person)
1143
+ );
1098
1144
  const [saving, setSaving] = React2.useState(false);
1099
1145
  React2.useEffect(() => {
1100
1146
  if (existingDocument && !template) {
1101
1147
  provider.getTemplateById(existingDocument.templateId).then(setTemplate);
1102
1148
  }
1103
1149
  }, [existingDocument, template, provider]);
1150
+ const prefilled = React2__default.default.useRef(!!initialTemplate || !!existingDocument);
1151
+ React2.useEffect(() => {
1152
+ if (prefilled.current || !template || existingDocument) return;
1153
+ prefilled.current = true;
1154
+ setData((prev) => chunkQM2AF3DZ_cjs.prefillFromPerson(template.schema, person, prev));
1155
+ }, [template, existingDocument, person]);
1104
1156
  const handleSave = React2.useCallback(
1105
1157
  async (status) => {
1106
1158
  if (!template) return;
1159
+ if (!existingDocument && await guardDocuments() === "blocked") return;
1107
1160
  setSaving(true);
1108
1161
  try {
1109
1162
  const autoTitle = generateTitle(template.name, personName);
@@ -1117,18 +1170,21 @@ function DocumentFormDialog({
1117
1170
  data,
1118
1171
  status
1119
1172
  });
1173
+ saas.invalidateLimit("documents_month");
1120
1174
  }
1121
1175
  onSaved();
1122
1176
  } finally {
1123
1177
  setSaving(false);
1124
1178
  }
1125
1179
  },
1126
- [template, existingDocument, data, personId, personName, store, onSaved]
1180
+ [template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
1127
1181
  );
1128
1182
  const snapshot = React2__default.default.useRef(null);
1129
1183
  React2.useEffect(() => {
1130
- if (template && snapshot.current === null) snapshot.current = JSON.stringify(data);
1131
- }, [template]);
1184
+ if (template && snapshot.current === null) {
1185
+ snapshot.current = JSON.stringify(existingDocument?.data ?? {});
1186
+ }
1187
+ }, [template, existingDocument]);
1132
1188
  const dirty = snapshot.current !== null && JSON.stringify(data) !== snapshot.current;
1133
1189
  ui.useSaveBar({
1134
1190
  dirty,
@@ -1164,17 +1220,13 @@ function DocumentFormDialog({
1164
1220
  ] })
1165
1221
  ] }),
1166
1222
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border p-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1167
- chunkXLUULIVL_cjs.FormRenderer,
1223
+ chunkQM2AF3DZ_cjs.FormRenderer,
1168
1224
  {
1169
1225
  schema: template.schema,
1170
1226
  data,
1171
1227
  onChange: setData
1172
1228
  }
1173
- ) }),
1174
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => handleSave("draft"), disabled: saving, children: [
1175
- saving ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3.5 w-3.5 mr-1.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "h-3.5 w-3.5 mr-1.5" }),
1176
- t("customForms.saveAsDraft")
1177
- ] }) })
1229
+ ) })
1178
1230
  ] });
1179
1231
  }
1180
1232
  var STATUS_COLORS2 = {
@@ -1211,13 +1263,26 @@ function FormViewer({ document: doc, provider, onBack, onEdit }) {
1211
1263
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: new Date(doc.createdAt).toLocaleDateString() })
1212
1264
  ] }),
1213
1265
  /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: "secondary", className: `text-[10px] ${STATUS_COLORS2[doc.status] ?? ""}`, children: t(`customForms.status.${doc.status}`) }),
1266
+ /* @__PURE__ */ jsxRuntime.jsxs(
1267
+ ui.Button,
1268
+ {
1269
+ variant: "outline",
1270
+ size: "sm",
1271
+ className: "h-8",
1272
+ onClick: () => chunkQM2AF3DZ_cjs.printDocument(template, doc),
1273
+ children: [
1274
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Printer, { className: "h-3 w-3 mr-1" }),
1275
+ t("customForms.printPdf")
1276
+ ]
1277
+ }
1278
+ ),
1214
1279
  onEdit && doc.status === "draft" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => onEdit(doc), children: [
1215
1280
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-3 w-3 mr-1" }),
1216
1281
  t("common.edit")
1217
1282
  ] })
1218
1283
  ] }),
1219
1284
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border p-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1220
- chunkXLUULIVL_cjs.FormRenderer,
1285
+ chunkQM2AF3DZ_cjs.FormRenderer,
1221
1286
  {
1222
1287
  schema: template.schema,
1223
1288
  data: doc.data,
@@ -1294,6 +1359,7 @@ function AddDocumentDropdown({ personId, onSelect }) {
1294
1359
  }
1295
1360
  function PersonDocumentsWidget({ item, config, provider, store }) {
1296
1361
  const t = core.useTranslation();
1362
+ const guardDocuments = saas.useLimitGuard("documents_month");
1297
1363
  const [view, setView] = React2.useState({ type: "list" });
1298
1364
  const fileInputRef = React2.useRef(null);
1299
1365
  const [pendingFileAccept, setPendingFileAccept] = React2.useState("*/*");
@@ -1318,6 +1384,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1318
1384
  }
1319
1385
  }, [provider]);
1320
1386
  const handleFiles = React2.useCallback(async (files) => {
1387
+ if (await guardDocuments(files.length) === "blocked") return;
1321
1388
  for (const file of files) {
1322
1389
  const isImage = file.type.startsWith("image/");
1323
1390
  await store.getState().createDocument({
@@ -1330,7 +1397,8 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1330
1397
  status: "completed"
1331
1398
  });
1332
1399
  }
1333
- }, [store, item.id]);
1400
+ saas.invalidateLimit("documents_month");
1401
+ }, [store, item.id, guardDocuments]);
1334
1402
  const handleFileInputChange = React2.useCallback((e) => {
1335
1403
  const files = Array.from(e.target.files ?? []);
1336
1404
  if (files.length > 0) handleFiles(files);
@@ -1355,6 +1423,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1355
1423
  template: view.template,
1356
1424
  personId: item.id,
1357
1425
  personName: item.name,
1426
+ person: item,
1358
1427
  provider,
1359
1428
  store,
1360
1429
  onSaved: handleSaved,
@@ -1386,6 +1455,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1386
1455
  existingDocument: view.document,
1387
1456
  personId: item.id,
1388
1457
  personName: item.name,
1458
+ person: item,
1389
1459
  provider,
1390
1460
  store,
1391
1461
  onSaved: handleSaved,
@@ -1526,6 +1596,9 @@ function createCustomFormsPlugin(options) {
1526
1596
  const SettingsComponent = () => React2__default.default.createElement(saas.PluginSettingsPanel, {
1527
1597
  title: config.labels.settingsLabel,
1528
1598
  subtitle: config.labels.settingsSubtitle,
1599
+ // The settings shell already renders the section label as the page H1;
1600
+ // hide the panel's own duplicate title (keeps the subtitle).
1601
+ hideTitle: true,
1529
1602
  customTabs: [
1530
1603
  {
1531
1604
  id: "forms",
@@ -1553,6 +1626,10 @@ function createCustomFormsPlugin(options) {
1553
1626
  declaredFeatures: [
1554
1627
  { id: "custom_forms", label: config.labels.pageTitle, group: config.labels.pageTitle }
1555
1628
  ],
1629
+ declaredLimits: [
1630
+ { key: "form_templates", label: "Form templates", table: "plg_forms_templates" },
1631
+ { key: "documents_month", label: "Documents / month", table: "documents", period: "month" }
1632
+ ],
1556
1633
  navigation: [],
1557
1634
  routes: [],
1558
1635
  settings: [
@@ -1617,6 +1694,17 @@ function createCustomFormsPlugin(options) {
1617
1694
  { label: "Show this client's documents" }
1618
1695
  ],
1619
1696
  permission: { feature: "custom_forms", action: "read" }
1697
+ },
1698
+ // Server-plane write: builds/edits a form template via the pool RPC, so
1699
+ // the assistant can montar formulários from any page or channel.
1700
+ chunkQM2AF3DZ_cjs.createOrUpdateFormTool
1701
+ ],
1702
+ declaredRpcs: [
1703
+ {
1704
+ name: "agent_forms_upsert_template",
1705
+ kind: "write",
1706
+ description: "Guarded form-template upsert: agent_guard (role\u2192plan\u2192form_templates cap), grid layout computed server-side, create or edit by id, audited.",
1707
+ audits: true
1620
1708
  }
1621
1709
  ],
1622
1710
  locales: customFormsLocales