@fayz-ai/plugin-forms 0.8.0 → 0.9.0-next.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 (59) 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/agent-tools.d.ts +4 -0
  4. package/dist/agent-tools.d.ts.map +1 -0
  5. package/dist/chunk-W2ZNJGQC.js +438 -0
  6. package/dist/chunk-W2ZNJGQC.js.map +1 -0
  7. package/dist/components/DocumentFormDialog.d.ts +3 -1
  8. package/dist/components/DocumentFormDialog.d.ts.map +1 -1
  9. package/dist/components/DocumentList.d.ts.map +1 -1
  10. package/dist/components/FieldConfigDialog.d.ts.map +1 -1
  11. package/dist/components/FormBuilder.d.ts.map +1 -1
  12. package/dist/components/FormViewer.d.ts.map +1 -1
  13. package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
  14. package/dist/data/supabase.d.ts.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +142 -54
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/person-fields.d.ts +16 -0
  19. package/dist/lib/person-fields.d.ts.map +1 -0
  20. package/dist/lib/print.d.ts +17 -0
  21. package/dist/lib/print.d.ts.map +1 -0
  22. package/dist/lib/tenant.d.ts.map +1 -1
  23. package/dist/locales/en.d.ts.map +1 -1
  24. package/dist/locales/pt-BR.d.ts.map +1 -1
  25. package/dist/migrations/index.d.ts +1 -0
  26. package/dist/migrations/index.d.ts.map +1 -1
  27. package/dist/types.d.ts +14 -0
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
  30. package/dist/views/TemplateListView.d.ts.map +1 -1
  31. package/package.json +13 -11
  32. package/src/agent-tools.ts +73 -0
  33. package/src/components/DocumentFormDialog.tsx +37 -16
  34. package/src/components/DocumentList.tsx +12 -8
  35. package/src/components/FieldConfigDialog.tsx +40 -0
  36. package/src/components/FormBuilder.tsx +104 -22
  37. package/src/components/FormViewer.tsx +11 -1
  38. package/src/components/PersonDocumentsWidget.tsx +10 -1
  39. package/src/data/supabase.ts +2 -0
  40. package/src/index.ts +21 -0
  41. package/src/lib/person-fields.ts +67 -0
  42. package/src/lib/print.ts +207 -0
  43. package/src/lib/tenant.ts +6 -2
  44. package/src/locales/en.ts +20 -0
  45. package/src/locales/pt-BR.ts +20 -0
  46. package/src/migrations/003_agent_rpcs.sql +174 -0
  47. package/src/migrations/index.ts +178 -1
  48. package/src/types.ts +14 -0
  49. package/src/views/CustomFormsSettingsTab.tsx +23 -16
  50. package/src/views/TemplateListView.tsx +29 -18
  51. package/dist/FormBuilder-4VKYVZAC.cjs +0 -619
  52. package/dist/FormBuilder-4VKYVZAC.cjs.map +0 -1
  53. package/dist/FormBuilder-KDANEL6Z.js.map +0 -1
  54. package/dist/chunk-32I43T37.js +0 -198
  55. package/dist/chunk-32I43T37.js.map +0 -1
  56. package/dist/chunk-XLUULIVL.cjs +0 -200
  57. package/dist/chunk-XLUULIVL.cjs.map +0 -1
  58. package/dist/index.cjs +0 -1629
  59. package/dist/index.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
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
 
@@ -178,14 +178,11 @@ function createMockFormsProvider() {
178
178
  }
179
179
  };
180
180
  }
181
-
182
- // src/lib/tenant.ts
183
- var currentTenantId;
184
181
  function getFormsTenantId() {
185
- return currentTenantId;
182
+ return getActiveTenantId();
186
183
  }
187
184
  function getTenantId() {
188
- return getActiveTenantId() ?? "";
185
+ return getFormsTenantId() ?? getActiveTenantId() ?? "";
189
186
  }
190
187
  function getClient() {
191
188
  const supabase = getSupabaseClientOptional();
@@ -263,7 +260,7 @@ function createSupabaseFormsProvider() {
263
260
  // ── Templates ──────────────────────────────────────────
264
261
  async getTemplates(query) {
265
262
  const db = getClient();
266
- let qb = db.from(T.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);
267
264
  if (query.category) qb = qb.eq("category", query.category);
268
265
  if (query.search) qb = qb.ilike("name", `%${query.search}%`);
269
266
  const page = query.page ?? 1;
@@ -569,6 +566,7 @@ var en = {
569
566
  "customForms.noTemplates": "No form templates yet",
570
567
  "customForms.noTemplatesDescription": "Create your first custom form to start collecting structured data.",
571
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.",
572
570
  // Categories
573
571
  "customForms.category.anamnesis": "Anamnesis",
574
572
  "customForms.category.evolution": "Evolution Note",
@@ -590,6 +588,21 @@ var en = {
590
588
  "customForms.builder.addOption": "Add option",
591
589
  "customForms.builder.colSpan": "Column span",
592
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",
593
606
  // Field types
594
607
  "customForms.fieldType.title": "Title",
595
608
  "customForms.fieldType.text": "Simple Text",
@@ -613,6 +626,9 @@ var en = {
613
626
  "customForms.saveAsDraft": "Save as Draft",
614
627
  "customForms.saveAndComplete": "Save & Complete",
615
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",
616
632
  // Registry
617
633
  "registry.form-categories": "Categories",
618
634
  "registry.form-categories.description": "Categories for organizing form templates",
@@ -639,6 +655,7 @@ var ptBR = {
639
655
  "customForms.noTemplates": "Nenhum modelo criado",
640
656
  "customForms.noTemplatesDescription": "Crie seu primeiro formul\xE1rio personalizado para come\xE7ar a coletar dados estruturados.",
641
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.",
642
659
  // Categories
643
660
  "customForms.category.anamnesis": "Anamnese",
644
661
  "customForms.category.evolution": "Evolu\xE7\xE3o",
@@ -660,6 +677,21 @@ var ptBR = {
660
677
  "customForms.builder.addOption": "Adicionar op\xE7\xE3o",
661
678
  "customForms.builder.colSpan": "Largura em colunas",
662
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",
663
695
  // Field types
664
696
  "customForms.fieldType.title": "T\xEDtulo",
665
697
  "customForms.fieldType.text": "Texto Simples",
@@ -683,6 +715,9 @@ var ptBR = {
683
715
  "customForms.saveAsDraft": "Salvar Rascunho",
684
716
  "customForms.saveAndComplete": "Salvar e Finalizar",
685
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",
686
721
  // Registry
687
722
  "registry.form-categories": "Categorias",
688
723
  "registry.form-categories.description": "Categorias para organizar modelos de formul\xE1rios",
@@ -728,12 +763,17 @@ function TemplateListView({ store, config, onEdit, onNew }) {
728
763
  const loading = store((s) => s.templatesLoading);
729
764
  const orgId = getFormsTenantId();
730
765
  useEffect(() => {
731
- return;
766
+ if (!orgId) return;
767
+ const timer = setTimeout(() => {
768
+ store.getState().fetchTemplates({ search: search || void 0 });
769
+ }, 50);
770
+ return () => clearTimeout(timer);
732
771
  }, [orgId, search]);
733
- const handleDelete = useCallback(async (template) => {
734
- if (!window.confirm(t("customForms.deleteTemplateConfirm"))) return;
735
- 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 });
736
775
  }, [store, t]);
776
+ const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
737
777
  const categoryLabel = (cat) => t(`customForms.category.${cat}`);
738
778
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
739
779
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
@@ -749,21 +789,21 @@ function TemplateListView({ store, config, onEdit, onNew }) {
749
789
  }
750
790
  )
751
791
  ] }),
752
- /* @__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: [
753
793
  /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
754
794
  t("customForms.newTemplate")
755
- ] })
795
+ ] }) })
756
796
  ] }),
757
- !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: [
758
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" }) }),
759
799
  /* @__PURE__ */ jsx("p", { className: "font-medium", children: t("customForms.noTemplates") }),
760
800
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-sm", children: t("customForms.noTemplatesDescription") }),
761
- /* @__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: [
762
802
  /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
763
803
  t("customForms.newTemplate")
764
- ] })
804
+ ] }) })
765
805
  ] }) }),
766
- 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(
767
807
  Card,
768
808
  {
769
809
  className: "group cursor-pointer hover:shadow-md transition-shadow",
@@ -791,10 +831,11 @@ function TemplateListView({ store, config, onEdit, onNew }) {
791
831
  {
792
832
  onClick: (e) => {
793
833
  e.stopPropagation();
794
- handleDelete(template);
834
+ handleArchive(template);
795
835
  },
796
- className: "p-1.5 rounded-md hover:bg-destructive/10",
797
- 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" })
798
839
  }
799
840
  )
800
841
  ] })
@@ -825,29 +866,29 @@ function TemplateListView({ store, config, onEdit, onNew }) {
825
866
  ] }) }, i)) })
826
867
  ] });
827
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
+ }
828
876
  function CustomFormsSettingsTab({ config, provider, store }) {
829
- const [builderTemplateId, setBuilderTemplateId] = useState(null);
877
+ const [builderTemplateId, setBuilderTemplateId] = useState(readBuilderIdFromHash);
830
878
  const handleNew = useCallback(() => {
831
- setBuilderTemplateId("__new__");
879
+ window.location.hash = `#${ROUTE_BASE}/templates/new`;
832
880
  }, []);
833
881
  const handleEdit = useCallback((template) => {
834
- setBuilderTemplateId(template.id);
882
+ window.location.hash = `#${ROUTE_BASE}/templates/${template.id}`;
835
883
  }, []);
836
884
  const handleBackFromBuilder = useCallback(() => {
837
- setBuilderTemplateId(null);
885
+ window.location.hash = `#${ROUTE_BASE}/forms`;
838
886
  store.getState().fetchTemplates();
839
887
  }, [store]);
840
888
  React2.useEffect(() => {
841
- function checkHash() {
842
- const hash = window.location.hash.slice(1) || "/";
843
- const builderMatch = hash.match(/\/settings\/custom_forms\/templates\/(.+)/);
844
- if (builderMatch && builderMatch[1] !== "new") {
845
- setBuilderTemplateId(builderMatch[1]);
846
- } else if (builderMatch && builderMatch[1] === "new") {
847
- setBuilderTemplateId("__new__");
848
- }
849
- }
850
- checkHash();
889
+ const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
890
+ window.addEventListener("hashchange", sync);
891
+ return () => window.removeEventListener("hashchange", sync);
851
892
  }, []);
852
893
  return builderTemplateId ? /* @__PURE__ */ jsx(
853
894
  BuilderWrapper,
@@ -876,7 +917,7 @@ function BuilderWrapper({
876
917
  onBack
877
918
  }) {
878
919
  const FormBuilderLazy = React2.lazy(
879
- () => import('./FormBuilder-KDANEL6Z.js').then((m) => ({ default: m.FormBuilder }))
920
+ () => import('./FormBuilder-7SWZQKFN.js').then((m) => ({ default: m.FormBuilder }))
880
921
  );
881
922
  return /* @__PURE__ */ jsx(
882
923
  React2.Suspense,
@@ -942,7 +983,7 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
942
983
  setLoading(true);
943
984
  try {
944
985
  const result = await provider.getDocuments({ personId });
945
- setDocuments(result.data);
986
+ setDocuments(result.data.filter((d) => d.status !== "archived"));
946
987
  } finally {
947
988
  setLoading(false);
948
989
  }
@@ -955,9 +996,9 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
955
996
  const unsub = store.subscribe(() => fetchDocs());
956
997
  return unsub;
957
998
  }, [store, fetchDocs]);
958
- const handleDelete = useCallback(async (doc) => {
959
- if (!window.confirm(t("customForms.deleteDocumentConfirm"))) return;
960
- 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" });
961
1002
  setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
962
1003
  }, [provider, t]);
963
1004
  const handleDragEnter = useCallback((e) => {
@@ -1055,10 +1096,11 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
1055
1096
  {
1056
1097
  onClick: (e) => {
1057
1098
  e.stopPropagation();
1058
- handleDelete(doc);
1099
+ handleArchive(doc);
1059
1100
  },
1060
- className: "p-1.5 rounded-md hover:bg-destructive/10",
1061
- 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" })
1062
1104
  }
1063
1105
  )
1064
1106
  ] })
@@ -1081,23 +1123,34 @@ function DocumentFormDialog({
1081
1123
  existingDocument,
1082
1124
  personId,
1083
1125
  personName,
1126
+ person,
1084
1127
  provider,
1085
1128
  store,
1086
1129
  onSaved,
1087
1130
  onBack
1088
1131
  }) {
1089
1132
  const t = useTranslation();
1133
+ const guardDocuments = useLimitGuard("documents_month");
1090
1134
  const [template, setTemplate] = useState(initialTemplate);
1091
- const [data, setData] = useState(existingDocument?.data ?? {});
1135
+ const [data, setData] = useState(
1136
+ () => existingDocument ? existingDocument.data : prefillFromPerson(initialTemplate?.schema, person)
1137
+ );
1092
1138
  const [saving, setSaving] = useState(false);
1093
1139
  useEffect(() => {
1094
1140
  if (existingDocument && !template) {
1095
1141
  provider.getTemplateById(existingDocument.templateId).then(setTemplate);
1096
1142
  }
1097
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]);
1098
1150
  const handleSave = useCallback(
1099
1151
  async (status) => {
1100
1152
  if (!template) return;
1153
+ if (!existingDocument && await guardDocuments() === "blocked") return;
1101
1154
  setSaving(true);
1102
1155
  try {
1103
1156
  const autoTitle = generateTitle(template.name, personName);
@@ -1111,18 +1164,21 @@ function DocumentFormDialog({
1111
1164
  data,
1112
1165
  status
1113
1166
  });
1167
+ invalidateLimit("documents_month");
1114
1168
  }
1115
1169
  onSaved();
1116
1170
  } finally {
1117
1171
  setSaving(false);
1118
1172
  }
1119
1173
  },
1120
- [template, existingDocument, data, personId, personName, store, onSaved]
1174
+ [template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
1121
1175
  );
1122
1176
  const snapshot = React2.useRef(null);
1123
1177
  useEffect(() => {
1124
- if (template && snapshot.current === null) snapshot.current = JSON.stringify(data);
1125
- }, [template]);
1178
+ if (template && snapshot.current === null) {
1179
+ snapshot.current = JSON.stringify(existingDocument?.data ?? {});
1180
+ }
1181
+ }, [template, existingDocument]);
1126
1182
  const dirty = snapshot.current !== null && JSON.stringify(data) !== snapshot.current;
1127
1183
  useSaveBar({
1128
1184
  dirty,
@@ -1164,11 +1220,7 @@ function DocumentFormDialog({
1164
1220
  data,
1165
1221
  onChange: setData
1166
1222
  }
1167
- ) }),
1168
- /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => handleSave("draft"), disabled: saving, children: [
1169
- 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" }),
1170
- t("customForms.saveAsDraft")
1171
- ] }) })
1223
+ ) })
1172
1224
  ] });
1173
1225
  }
1174
1226
  var STATUS_COLORS2 = {
@@ -1205,6 +1257,19 @@ function FormViewer({ document: doc, provider, onBack, onEdit }) {
1205
1257
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: new Date(doc.createdAt).toLocaleDateString() })
1206
1258
  ] }),
1207
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
+ ),
1208
1273
  onEdit && doc.status === "draft" && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => onEdit(doc), children: [
1209
1274
  /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 mr-1" }),
1210
1275
  t("common.edit")
@@ -1288,6 +1353,7 @@ function AddDocumentDropdown({ personId, onSelect }) {
1288
1353
  }
1289
1354
  function PersonDocumentsWidget({ item, config, provider, store }) {
1290
1355
  const t = useTranslation();
1356
+ const guardDocuments = useLimitGuard("documents_month");
1291
1357
  const [view, setView] = useState({ type: "list" });
1292
1358
  const fileInputRef = useRef(null);
1293
1359
  const [pendingFileAccept, setPendingFileAccept] = useState("*/*");
@@ -1312,6 +1378,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1312
1378
  }
1313
1379
  }, [provider]);
1314
1380
  const handleFiles = useCallback(async (files) => {
1381
+ if (await guardDocuments(files.length) === "blocked") return;
1315
1382
  for (const file of files) {
1316
1383
  const isImage = file.type.startsWith("image/");
1317
1384
  await store.getState().createDocument({
@@ -1324,7 +1391,8 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1324
1391
  status: "completed"
1325
1392
  });
1326
1393
  }
1327
- }, [store, item.id]);
1394
+ invalidateLimit("documents_month");
1395
+ }, [store, item.id, guardDocuments]);
1328
1396
  const handleFileInputChange = useCallback((e) => {
1329
1397
  const files = Array.from(e.target.files ?? []);
1330
1398
  if (files.length > 0) handleFiles(files);
@@ -1349,6 +1417,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1349
1417
  template: view.template,
1350
1418
  personId: item.id,
1351
1419
  personName: item.name,
1420
+ person: item,
1352
1421
  provider,
1353
1422
  store,
1354
1423
  onSaved: handleSaved,
@@ -1380,6 +1449,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
1380
1449
  existingDocument: view.document,
1381
1450
  personId: item.id,
1382
1451
  personName: item.name,
1452
+ person: item,
1383
1453
  provider,
1384
1454
  store,
1385
1455
  onSaved: handleSaved,
@@ -1520,6 +1590,9 @@ function createCustomFormsPlugin(options) {
1520
1590
  const SettingsComponent = () => React2.createElement(PluginSettingsPanel, {
1521
1591
  title: config.labels.settingsLabel,
1522
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,
1523
1596
  customTabs: [
1524
1597
  {
1525
1598
  id: "forms",
@@ -1547,6 +1620,10 @@ function createCustomFormsPlugin(options) {
1547
1620
  declaredFeatures: [
1548
1621
  { id: "custom_forms", label: config.labels.pageTitle, group: config.labels.pageTitle }
1549
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
+ ],
1550
1627
  navigation: [],
1551
1628
  routes: [],
1552
1629
  settings: [
@@ -1611,6 +1688,17 @@ function createCustomFormsPlugin(options) {
1611
1688
  { label: "Show this client's documents" }
1612
1689
  ],
1613
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
1614
1702
  }
1615
1703
  ],
1616
1704
  locales: customFormsLocales