@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.
- package/dist/{FormBuilder-KDANEL6Z.js → FormBuilder-7SWZQKFN.js} +130 -23
- package/dist/FormBuilder-7SWZQKFN.js.map +1 -0
- package/dist/{FormBuilder-4VKYVZAC.cjs → FormBuilder-YB75LEEH.cjs} +129 -22
- package/dist/FormBuilder-YB75LEEH.cjs.map +1 -0
- package/dist/agent-tools.d.ts +4 -0
- package/dist/agent-tools.d.ts.map +1 -0
- package/dist/chunk-QM2AF3DZ.cjs +446 -0
- package/dist/chunk-QM2AF3DZ.cjs.map +1 -0
- package/dist/chunk-W2ZNJGQC.js +438 -0
- package/dist/chunk-W2ZNJGQC.js.map +1 -0
- package/dist/components/DocumentFormDialog.d.ts +3 -1
- package/dist/components/DocumentFormDialog.d.ts.map +1 -1
- package/dist/components/DocumentList.d.ts.map +1 -1
- package/dist/components/FieldConfigDialog.d.ts.map +1 -1
- package/dist/components/FormBuilder.d.ts.map +1 -1
- package/dist/components/FormViewer.d.ts.map +1 -1
- package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.cjs +142 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -54
- package/dist/index.js.map +1 -1
- package/dist/lib/person-fields.d.ts +16 -0
- package/dist/lib/person-fields.d.ts.map +1 -0
- package/dist/lib/print.d.ts +17 -0
- package/dist/lib/print.d.ts.map +1 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
- package/dist/views/TemplateListView.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agent-tools.ts +73 -0
- package/src/components/DocumentFormDialog.tsx +37 -16
- package/src/components/DocumentList.tsx +12 -8
- package/src/components/FieldConfigDialog.tsx +40 -0
- package/src/components/FormBuilder.tsx +104 -22
- package/src/components/FormViewer.tsx +11 -1
- package/src/components/PersonDocumentsWidget.tsx +10 -1
- package/src/data/supabase.ts +2 -0
- package/src/index.ts +21 -0
- package/src/lib/person-fields.ts +67 -0
- package/src/lib/print.ts +207 -0
- package/src/lib/tenant.ts +6 -2
- package/src/locales/en.ts +20 -0
- package/src/locales/pt-BR.ts +20 -0
- package/src/migrations/003_agent_rpcs.sql +174 -0
- package/src/migrations/index.ts +178 -1
- package/src/types.ts +14 -0
- package/src/views/CustomFormsSettingsTab.tsx +23 -16
- package/src/views/TemplateListView.tsx +29 -18
- package/dist/FormBuilder-4VKYVZAC.cjs.map +0 -1
- package/dist/FormBuilder-KDANEL6Z.js.map +0 -1
- package/dist/chunk-32I43T37.js +0 -198
- package/dist/chunk-32I43T37.js.map +0 -1
- package/dist/chunk-XLUULIVL.cjs +0 -200
- package/dist/chunk-XLUULIVL.cjs.map +0 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -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;AAUnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAA;
|
|
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,9 +1,9 @@
|
|
|
1
|
-
import { FormRenderer } from './chunk-
|
|
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,
|
|
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
|
|
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
|
|
734
|
-
if (!window.confirm(t("customForms.
|
|
735
|
-
await store.getState().
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
834
|
+
handleArchive(template);
|
|
795
835
|
},
|
|
796
|
-
className: "p-1.5 rounded-md hover:bg-
|
|
797
|
-
|
|
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(
|
|
877
|
+
const [builderTemplateId, setBuilderTemplateId] = useState(readBuilderIdFromHash);
|
|
830
878
|
const handleNew = useCallback(() => {
|
|
831
|
-
|
|
879
|
+
window.location.hash = `#${ROUTE_BASE}/templates/new`;
|
|
832
880
|
}, []);
|
|
833
881
|
const handleEdit = useCallback((template) => {
|
|
834
|
-
|
|
882
|
+
window.location.hash = `#${ROUTE_BASE}/templates/${template.id}`;
|
|
835
883
|
}, []);
|
|
836
884
|
const handleBackFromBuilder = useCallback(() => {
|
|
837
|
-
|
|
885
|
+
window.location.hash = `#${ROUTE_BASE}/forms`;
|
|
838
886
|
store.getState().fetchTemplates();
|
|
839
887
|
}, [store]);
|
|
840
888
|
React2.useEffect(() => {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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-
|
|
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
|
|
959
|
-
if (!window.confirm(t("customForms.
|
|
960
|
-
await provider.
|
|
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
|
-
|
|
1099
|
+
handleArchive(doc);
|
|
1059
1100
|
},
|
|
1060
|
-
className: "p-1.5 rounded-md hover:bg-
|
|
1061
|
-
|
|
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(
|
|
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)
|
|
1125
|
-
|
|
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
|
-
|
|
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
|