@fayz-ai/plugin-forms 0.9.4 → 0.10.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/analytics/cards.d.ts +3 -0
- package/dist/analytics/cards.d.ts.map +1 -0
- package/dist/components/AddDocumentDropdown.d.ts.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +384 -127
- package/dist/index.js.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 +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/store.d.ts +7 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/views/TemplateListView.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createOrUpdateFormTool, prefillFromPerson, FormRenderer, printDocument } from './chunk-RBLXIKWB.js';
|
|
2
|
-
import React2, { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
-
import { registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, getActiveTenantId } from '@fayz-ai/core';
|
|
4
|
-
import { PluginSettingsPanel, registerTimelineSource, useLimitGuard, invalidateLimit, PermissionGate } from '@fayz-ai/admin';
|
|
2
|
+
import React2, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
|
3
|
+
import { tl, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, getActiveTenantId, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
|
|
4
|
+
import { PluginSettingsPanel, registerTimelineSource, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions } from '@fayz-ai/admin';
|
|
5
5
|
import { create } from 'zustand';
|
|
6
|
-
import { FileText, Image, Paperclip,
|
|
7
|
-
import {
|
|
6
|
+
import { FileText, Image, Paperclip, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft, Printer, ChevronDown, Upload, Eye } from 'lucide-react';
|
|
7
|
+
import { Badge, RowActions, Button, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel } from '@fayz-ai/ui';
|
|
8
8
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
9
9
|
|
|
10
10
|
// src/data/tables.ts
|
|
@@ -503,6 +503,14 @@ function createSupabaseFormsProvider() {
|
|
|
503
503
|
}
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
|
+
var FORM_TEMPLATE_EVENT = "forms.template.";
|
|
507
|
+
function emitTemplateEvent(action, id, name) {
|
|
508
|
+
emitDomainEvent(
|
|
509
|
+
`${FORM_TEMPLATE_EVENT}${action}`,
|
|
510
|
+
{ record_id: id, record_name: name ?? null },
|
|
511
|
+
{ tenantId: getActiveTenantId() ?? null, subject: { type: "form_template", id } }
|
|
512
|
+
);
|
|
513
|
+
}
|
|
506
514
|
function createCustomFormsStore(provider) {
|
|
507
515
|
return create((set) => ({
|
|
508
516
|
templates: [],
|
|
@@ -557,6 +565,7 @@ function createCustomFormsStore(provider) {
|
|
|
557
565
|
async createTemplate(input) {
|
|
558
566
|
const template = await provider.createTemplate(input);
|
|
559
567
|
set((s) => ({ templates: [template, ...s.templates], templatesTotal: s.templatesTotal + 1 }));
|
|
568
|
+
emitTemplateEvent("created", template.id, template.name);
|
|
560
569
|
return template;
|
|
561
570
|
},
|
|
562
571
|
async updateTemplate(id, input) {
|
|
@@ -565,10 +574,12 @@ function createCustomFormsStore(provider) {
|
|
|
565
574
|
templates: s.templates.map((t) => t.id === id ? template : t),
|
|
566
575
|
activeTemplate: s.activeTemplate?.id === id ? template : s.activeTemplate
|
|
567
576
|
}));
|
|
577
|
+
emitTemplateEvent("updated", template.id, template.name);
|
|
568
578
|
return template;
|
|
569
579
|
},
|
|
570
580
|
async deleteTemplate(id) {
|
|
571
581
|
await provider.deleteTemplate(id);
|
|
582
|
+
emitTemplateEvent("deleted", id);
|
|
572
583
|
set((s) => ({
|
|
573
584
|
templates: s.templates.filter((t) => t.id !== id),
|
|
574
585
|
templatesTotal: s.templatesTotal - 1
|
|
@@ -628,6 +639,16 @@ var en = {
|
|
|
628
639
|
"customForms.settingsSubtitle": "Create and manage custom forms for your business",
|
|
629
640
|
// Templates
|
|
630
641
|
"customForms.templates": "Templates",
|
|
642
|
+
"customForms.newWithAIHint": "Describe the form and I build the fields.",
|
|
643
|
+
"customForms.newByHand": "Create by hand",
|
|
644
|
+
"customForms.newByHandHint": "Build it field by field in the builder.",
|
|
645
|
+
"customForms.askPlaceholder": "Describe the form\u2026",
|
|
646
|
+
"customForms.askPersona": "Form builder",
|
|
647
|
+
"customForms.askPitch": "Tell me the form you need.",
|
|
648
|
+
"customForms.askDescription": "Describe what it is for and I propose the fields, their types and options in one go. You check and adjust before saving.",
|
|
649
|
+
"customForms.askSuggestion.0": "A full intake form",
|
|
650
|
+
"customForms.askSuggestion.1": "Consent form for an aesthetic procedure",
|
|
651
|
+
"customForms.askSuggestion.2": "Session progress note",
|
|
631
652
|
"customForms.newTemplate": "New Template",
|
|
632
653
|
"customForms.editTemplate": "Edit Template",
|
|
633
654
|
"customForms.templateName": "Form name",
|
|
@@ -691,6 +712,8 @@ var en = {
|
|
|
691
712
|
"customForms.documents": "Documents",
|
|
692
713
|
"customForms.addDocument": "Add Document",
|
|
693
714
|
"customForms.selectTemplate": "Select a form",
|
|
715
|
+
"customForms.searchDocumentTypes": "Search forms or files...",
|
|
716
|
+
"customForms.noResults": "No results",
|
|
694
717
|
"customForms.noDocuments": "No documents yet",
|
|
695
718
|
"customForms.noDocumentsDescription": "Add a document to start recording information.",
|
|
696
719
|
"customForms.documentTitle": "Document title",
|
|
@@ -702,6 +725,10 @@ var en = {
|
|
|
702
725
|
"customForms.printPdf": "Print / PDF",
|
|
703
726
|
"customForms.usageCount": "{{count}} filled in",
|
|
704
727
|
"customForms.usageLast": "last on {{date}}",
|
|
728
|
+
// Template list columns
|
|
729
|
+
"customForms.template": "Template",
|
|
730
|
+
"customForms.categoryColumn": "Category",
|
|
731
|
+
"customForms.usageColumn": "Usage",
|
|
705
732
|
// Registry
|
|
706
733
|
"registry.form-categories": "Categories",
|
|
707
734
|
"registry.form-categories.description": "Categories for organizing form templates",
|
|
@@ -719,6 +746,16 @@ var ptBR = {
|
|
|
719
746
|
"customForms.settingsSubtitle": "Crie e gerencie formul\xE1rios personalizados para o seu neg\xF3cio",
|
|
720
747
|
// Templates
|
|
721
748
|
"customForms.templates": "Modelos",
|
|
749
|
+
"customForms.newWithAIHint": "Descreva o formul\xE1rio e eu monto os campos.",
|
|
750
|
+
"customForms.newByHand": "Criar manualmente",
|
|
751
|
+
"customForms.newByHandHint": "Montar campo a campo no construtor.",
|
|
752
|
+
"customForms.askPlaceholder": "Descreva o formul\xE1rio\u2026",
|
|
753
|
+
"customForms.askPersona": "Montador de formul\xE1rios",
|
|
754
|
+
"customForms.askPitch": "Diga o formul\xE1rio que voc\xEA precisa.",
|
|
755
|
+
"customForms.askDescription": "Descreva para que serve e eu proponho os campos, os tipos e as op\xE7\xF5es de uma vez. Voc\xEA confere e ajusta antes de salvar.",
|
|
756
|
+
"customForms.askSuggestion.0": "Uma ficha de anamnese completa",
|
|
757
|
+
"customForms.askSuggestion.1": "Termo de consentimento para procedimento est\xE9tico",
|
|
758
|
+
"customForms.askSuggestion.2": "Ficha de evolu\xE7\xE3o de atendimento",
|
|
722
759
|
"customForms.newTemplate": "Novo Modelo",
|
|
723
760
|
"customForms.editTemplate": "Editar Modelo",
|
|
724
761
|
"customForms.templateName": "Nome do formul\xE1rio",
|
|
@@ -782,6 +819,8 @@ var ptBR = {
|
|
|
782
819
|
"customForms.documents": "Documentos",
|
|
783
820
|
"customForms.addDocument": "Novo Documento",
|
|
784
821
|
"customForms.selectTemplate": "Selecione um formul\xE1rio",
|
|
822
|
+
"customForms.searchDocumentTypes": "Buscar formul\xE1rio ou arquivo...",
|
|
823
|
+
"customForms.noResults": "Nenhum resultado",
|
|
785
824
|
"customForms.noDocuments": "Nenhum documento",
|
|
786
825
|
"customForms.noDocumentsDescription": "Adicione um documento para come\xE7ar a registrar informa\xE7\xF5es.",
|
|
787
826
|
"customForms.documentTitle": "T\xEDtulo do documento",
|
|
@@ -793,6 +832,10 @@ var ptBR = {
|
|
|
793
832
|
"customForms.printPdf": "Imprimir / PDF",
|
|
794
833
|
"customForms.usageCount": "{{count}} preenchidos",
|
|
795
834
|
"customForms.usageLast": "\xFAltimo em {{date}}",
|
|
835
|
+
// Colunas da lista de modelos
|
|
836
|
+
"customForms.template": "Modelo",
|
|
837
|
+
"customForms.categoryColumn": "Categoria",
|
|
838
|
+
"customForms.usageColumn": "Uso",
|
|
796
839
|
// Registry
|
|
797
840
|
"registry.form-categories": "Categorias",
|
|
798
841
|
"registry.form-categories.description": "Categorias para organizar modelos de formul\xE1rios",
|
|
@@ -838,13 +881,48 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
838
881
|
const loading = store((s) => s.templatesLoading);
|
|
839
882
|
const usage = store((s) => s.templateUsage);
|
|
840
883
|
const orgId = getFormsTenantId();
|
|
884
|
+
const createActions = useMemo(() => {
|
|
885
|
+
const permission = { feature: "custom_forms", action: "create" };
|
|
886
|
+
return [
|
|
887
|
+
{
|
|
888
|
+
id: "new",
|
|
889
|
+
label: t("customForms.newTemplate"),
|
|
890
|
+
description: t("customForms.newByHandHint"),
|
|
891
|
+
icon: Plus,
|
|
892
|
+
intent: "primary",
|
|
893
|
+
creates: true,
|
|
894
|
+
onSelect: onNew,
|
|
895
|
+
permission
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
id: "ai",
|
|
899
|
+
label: t("assistant.askAI"),
|
|
900
|
+
description: t("customForms.newWithAIHint"),
|
|
901
|
+
icon: WandSparkles,
|
|
902
|
+
intent: "secondary",
|
|
903
|
+
creates: true,
|
|
904
|
+
onSelect: () => askAI({
|
|
905
|
+
hint: t("customForms.askPlaceholder"),
|
|
906
|
+
intro: {
|
|
907
|
+
role: "setup",
|
|
908
|
+
persona: t("customForms.askPersona"),
|
|
909
|
+
pitch: t("customForms.askPitch"),
|
|
910
|
+
description: t("customForms.askDescription"),
|
|
911
|
+
suggestions: [0, 1, 2].map((i) => ({ label: t(`customForms.askSuggestion.${i}`) }))
|
|
912
|
+
}
|
|
913
|
+
}),
|
|
914
|
+
permission
|
|
915
|
+
}
|
|
916
|
+
];
|
|
917
|
+
}, [t, onNew]);
|
|
918
|
+
const version = useDataVersion(`${FORM_TEMPLATE_EVENT}*`);
|
|
841
919
|
useEffect(() => {
|
|
842
920
|
if (!orgId) return;
|
|
843
921
|
const timer = setTimeout(() => {
|
|
844
922
|
store.getState().fetchTemplates({ search: search || void 0 });
|
|
845
923
|
}, 50);
|
|
846
924
|
return () => clearTimeout(timer);
|
|
847
|
-
}, [orgId, search]);
|
|
925
|
+
}, [orgId, search, version]);
|
|
848
926
|
useEffect(() => {
|
|
849
927
|
if (!orgId) return;
|
|
850
928
|
void store.getState().fetchTemplateUsage();
|
|
@@ -855,106 +933,115 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
855
933
|
}, [store, t]);
|
|
856
934
|
const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
|
|
857
935
|
const categoryLabel = (cat) => t(`customForms.category.${cat}`);
|
|
936
|
+
const columns = useMemo(() => [
|
|
937
|
+
{
|
|
938
|
+
id: "name",
|
|
939
|
+
header: t("customForms.template"),
|
|
940
|
+
cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
941
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: row.original.name }),
|
|
942
|
+
row.original.description && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: row.original.description })
|
|
943
|
+
] })
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
id: "category",
|
|
947
|
+
header: t("customForms.categoryColumn"),
|
|
948
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${CATEGORY_COLORS[row.original.category]}`, children: categoryLabel(row.original.category) })
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
id: "fields",
|
|
952
|
+
header: t("customForms.builder.fields"),
|
|
953
|
+
cell: ({ row }) => /* @__PURE__ */ jsxs("span", { className: "tabular-nums text-muted-foreground", children: [
|
|
954
|
+
row.original.schema.fields.length,
|
|
955
|
+
row.original.version > 1 && /* @__PURE__ */ jsxs("span", { className: "ml-2", children: [
|
|
956
|
+
"v",
|
|
957
|
+
row.original.version
|
|
958
|
+
] })
|
|
959
|
+
] })
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
id: "usage",
|
|
963
|
+
header: t("customForms.usageColumn"),
|
|
964
|
+
// "0 preenchimentos" é informação, não ausência dela: é o que separa um
|
|
965
|
+
// formulário que o salão usa todo dia de um que sobrou de uma importação.
|
|
966
|
+
cell: ({ row }) => {
|
|
967
|
+
const u = usage[row.original.id];
|
|
968
|
+
return /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
969
|
+
t("customForms.usageCount", { count: String(u?.documentCount ?? 0) }),
|
|
970
|
+
u?.lastDocumentAt && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
971
|
+
/* @__PURE__ */ jsx("span", { className: "px-1", children: "\xB7" }),
|
|
972
|
+
t("customForms.usageLast", { date: new Date(u.lastDocumentAt).toLocaleDateString() })
|
|
973
|
+
] })
|
|
974
|
+
] });
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
id: "actions",
|
|
979
|
+
header: "",
|
|
980
|
+
// RowActions is the one renderer for what can be done to a single row —
|
|
981
|
+
// it owns the sizing, the accessible name and the rule that keeps a
|
|
982
|
+
// destructive action off the row itself.
|
|
983
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(
|
|
984
|
+
RowActions,
|
|
985
|
+
{
|
|
986
|
+
actions: [
|
|
987
|
+
{
|
|
988
|
+
id: "edit",
|
|
989
|
+
label: t("common.edit"),
|
|
990
|
+
icon: Pencil,
|
|
991
|
+
onSelect: () => onEdit(row.original)
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
id: "archive",
|
|
995
|
+
label: t("customForms.archive"),
|
|
996
|
+
icon: Archive,
|
|
997
|
+
intent: "danger",
|
|
998
|
+
onSelect: () => {
|
|
999
|
+
void handleArchive(row.original);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
)
|
|
1005
|
+
}
|
|
1006
|
+
], [t, usage, onEdit, handleArchive]);
|
|
1007
|
+
const emptyState = /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-6 text-center", children: [
|
|
1008
|
+
/* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 text-muted-foreground" }) }),
|
|
1009
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: t("customForms.noTemplates") }),
|
|
1010
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: t("customForms.noTemplatesDescription") }),
|
|
1011
|
+
/* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
|
|
1012
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1013
|
+
t("customForms.newTemplate")
|
|
1014
|
+
] }) })
|
|
1015
|
+
] });
|
|
858
1016
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
859
|
-
/* @__PURE__ */
|
|
860
|
-
|
|
861
|
-
|
|
1017
|
+
/* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: createActions }) }),
|
|
1018
|
+
/* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
1019
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "relative min-w-[10rem] flex-1", children: [
|
|
1020
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
|
|
862
1021
|
/* @__PURE__ */ jsx(
|
|
863
|
-
|
|
1022
|
+
"input",
|
|
864
1023
|
{
|
|
1024
|
+
type: "text",
|
|
865
1025
|
value: search,
|
|
866
1026
|
onChange: (e) => setSearch(e.target.value),
|
|
867
1027
|
placeholder: t("common.search"),
|
|
868
|
-
className: "
|
|
1028
|
+
className: "w-full bg-transparent py-1.5 pl-8 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none"
|
|
869
1029
|
}
|
|
870
1030
|
)
|
|
871
|
-
] }),
|
|
872
|
-
/* @__PURE__ */ jsx(
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
] }) }),
|
|
886
|
-
visibleTemplates.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: visibleTemplates.map((template) => /* @__PURE__ */ jsx(
|
|
887
|
-
Card,
|
|
888
|
-
{
|
|
889
|
-
className: "group cursor-pointer hover:shadow-md transition-shadow",
|
|
890
|
-
onClick: () => onEdit(template),
|
|
891
|
-
children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4", children: [
|
|
892
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
893
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
894
|
-
/* @__PURE__ */ jsx("h3", { className: "font-medium text-sm truncate", children: template.name }),
|
|
895
|
-
template.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5 line-clamp-2", children: template.description })
|
|
896
|
-
] }),
|
|
897
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [
|
|
898
|
-
/* @__PURE__ */ jsx(
|
|
899
|
-
"button",
|
|
900
|
-
{
|
|
901
|
-
onClick: (e) => {
|
|
902
|
-
e.stopPropagation();
|
|
903
|
-
onEdit(template);
|
|
904
|
-
},
|
|
905
|
-
className: "p-1.5 rounded-md hover:bg-muted",
|
|
906
|
-
children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
907
|
-
}
|
|
908
|
-
),
|
|
909
|
-
/* @__PURE__ */ jsx(
|
|
910
|
-
"button",
|
|
911
|
-
{
|
|
912
|
-
onClick: (e) => {
|
|
913
|
-
e.stopPropagation();
|
|
914
|
-
handleArchive(template);
|
|
915
|
-
},
|
|
916
|
-
className: "p-1.5 rounded-md hover:bg-muted",
|
|
917
|
-
title: t("customForms.archive"),
|
|
918
|
-
children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
919
|
-
}
|
|
920
|
-
)
|
|
921
|
-
] })
|
|
922
|
-
] }),
|
|
923
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-3", children: [
|
|
924
|
-
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${CATEGORY_COLORS[template.category]}`, children: categoryLabel(template.category) }),
|
|
925
|
-
/* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
|
|
926
|
-
template.schema.fields.length,
|
|
927
|
-
" ",
|
|
928
|
-
t("customForms.builder.fields").toLowerCase()
|
|
929
|
-
] }),
|
|
930
|
-
template.version > 1 && /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
|
|
931
|
-
"v",
|
|
932
|
-
template.version
|
|
933
|
-
] })
|
|
934
|
-
] }),
|
|
935
|
-
/* @__PURE__ */ jsxs("p", { className: "mt-2 text-[10px] text-muted-foreground", children: [
|
|
936
|
-
t("customForms.usageCount", {
|
|
937
|
-
count: String(usage[template.id]?.documentCount ?? 0)
|
|
938
|
-
}),
|
|
939
|
-
usage[template.id]?.lastDocumentAt && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
940
|
-
/* @__PURE__ */ jsx("span", { className: "px-1", children: "\xB7" }),
|
|
941
|
-
t("customForms.usageLast", {
|
|
942
|
-
date: new Date(usage[template.id].lastDocumentAt).toLocaleDateString()
|
|
943
|
-
})
|
|
944
|
-
] })
|
|
945
|
-
] })
|
|
946
|
-
] })
|
|
947
|
-
},
|
|
948
|
-
template.id
|
|
949
|
-
)) }),
|
|
950
|
-
loading && /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4 space-y-3", children: [
|
|
951
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 bg-muted animate-pulse rounded" }),
|
|
952
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-full bg-muted animate-pulse rounded" }),
|
|
953
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
954
|
-
/* @__PURE__ */ jsx("div", { className: "h-5 w-16 bg-muted animate-pulse rounded-full" }),
|
|
955
|
-
/* @__PURE__ */ jsx("div", { className: "h-5 w-12 bg-muted animate-pulse rounded-full" })
|
|
956
|
-
] })
|
|
957
|
-
] }) }, i)) })
|
|
1031
|
+
] }) }),
|
|
1032
|
+
/* @__PURE__ */ jsx(
|
|
1033
|
+
DataTable,
|
|
1034
|
+
{
|
|
1035
|
+
variant: "bare",
|
|
1036
|
+
columns,
|
|
1037
|
+
data: loading ? [] : visibleTemplates,
|
|
1038
|
+
loading,
|
|
1039
|
+
skeletonRows: 3,
|
|
1040
|
+
emptyMessage: emptyState,
|
|
1041
|
+
onRowClick: (row) => onEdit(row)
|
|
1042
|
+
}
|
|
1043
|
+
)
|
|
1044
|
+
] })
|
|
958
1045
|
] });
|
|
959
1046
|
}
|
|
960
1047
|
var ROUTE_BASE = "/settings/custom_forms";
|
|
@@ -1431,13 +1518,24 @@ async function getAllDocumentTypes(personId) {
|
|
|
1431
1518
|
all.sort((a, b) => (a.groupOrder ?? 50) - (b.groupOrder ?? 50) || (a.order ?? 0) - (b.order ?? 0));
|
|
1432
1519
|
return all;
|
|
1433
1520
|
}
|
|
1521
|
+
var SEARCH_THRESHOLD = 8;
|
|
1522
|
+
var normalize = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
1434
1523
|
function AddDocumentDropdown({ personId, onSelect }) {
|
|
1435
1524
|
const t = useTranslation();
|
|
1525
|
+
const [open, setOpen] = useState(false);
|
|
1436
1526
|
const [options, setOptions] = useState([]);
|
|
1437
1527
|
const [loaded, setLoaded] = useState(false);
|
|
1528
|
+
const [query, setQuery] = useState("");
|
|
1529
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
1438
1530
|
const loadingRef = useRef(false);
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1531
|
+
const inputRef = useRef(null);
|
|
1532
|
+
const listRef = useRef(null);
|
|
1533
|
+
const handleOpenChange = useCallback((next) => {
|
|
1534
|
+
setOpen(next);
|
|
1535
|
+
if (!next) return;
|
|
1536
|
+
setQuery("");
|
|
1537
|
+
setActiveIndex(0);
|
|
1538
|
+
if (loaded || loadingRef.current) return;
|
|
1441
1539
|
loadingRef.current = true;
|
|
1442
1540
|
getAllDocumentTypes(personId).then((opts) => {
|
|
1443
1541
|
setOptions(opts);
|
|
@@ -1445,38 +1543,110 @@ function AddDocumentDropdown({ personId, onSelect }) {
|
|
|
1445
1543
|
loadingRef.current = false;
|
|
1446
1544
|
});
|
|
1447
1545
|
}, [personId, loaded]);
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1546
|
+
const filtered = useMemo(() => {
|
|
1547
|
+
const q = normalize(query.trim());
|
|
1548
|
+
if (!q) return options;
|
|
1549
|
+
return options.filter(
|
|
1550
|
+
(o) => normalize(o.label).includes(q) || (o.description ? normalize(o.description).includes(q) : false)
|
|
1551
|
+
);
|
|
1552
|
+
}, [options, query]);
|
|
1553
|
+
const groups = useMemo(() => {
|
|
1554
|
+
const acc = [];
|
|
1555
|
+
for (const opt of filtered) {
|
|
1556
|
+
let group = acc.find((g) => g.label === opt.group);
|
|
1557
|
+
if (!group) {
|
|
1558
|
+
group = { label: opt.group, items: [] };
|
|
1559
|
+
acc.push(group);
|
|
1560
|
+
}
|
|
1561
|
+
group.items.push(opt);
|
|
1454
1562
|
}
|
|
1455
|
-
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1563
|
+
return acc;
|
|
1564
|
+
}, [filtered]);
|
|
1565
|
+
const showSearch = loaded && options.length > SEARCH_THRESHOLD;
|
|
1566
|
+
useEffect(() => {
|
|
1567
|
+
if (!open || !showSearch) return;
|
|
1568
|
+
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
1569
|
+
return () => cancelAnimationFrame(id);
|
|
1570
|
+
}, [open, showSearch]);
|
|
1571
|
+
useEffect(() => {
|
|
1572
|
+
listRef.current?.querySelector(`[data-index="${activeIndex}"]`)?.scrollIntoView({ block: "nearest" });
|
|
1573
|
+
}, [activeIndex]);
|
|
1574
|
+
const choose = useCallback((opt) => {
|
|
1575
|
+
setOpen(false);
|
|
1576
|
+
onSelect(opt);
|
|
1577
|
+
}, [onSelect]);
|
|
1578
|
+
const handleInputKeyDown = (e) => {
|
|
1579
|
+
if (e.key === "Escape") return;
|
|
1580
|
+
if (e.key === "ArrowDown") {
|
|
1581
|
+
e.preventDefault();
|
|
1582
|
+
setActiveIndex((i) => Math.min(filtered.length - 1, i + 1));
|
|
1583
|
+
} else if (e.key === "ArrowUp") {
|
|
1584
|
+
e.preventDefault();
|
|
1585
|
+
setActiveIndex((i) => Math.max(0, i - 1));
|
|
1586
|
+
} else if (e.key === "Enter") {
|
|
1587
|
+
e.preventDefault();
|
|
1588
|
+
const opt = filtered[activeIndex];
|
|
1589
|
+
if (opt) choose(opt);
|
|
1590
|
+
}
|
|
1591
|
+
e.stopPropagation();
|
|
1592
|
+
};
|
|
1593
|
+
let flatIndex = -1;
|
|
1594
|
+
return /* @__PURE__ */ jsxs(Dropdown, { open, onOpenChange: handleOpenChange, children: [
|
|
1458
1595
|
/* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "sm", className: "h-8", children: [
|
|
1459
1596
|
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1" }),
|
|
1460
1597
|
t("customForms.addDocument"),
|
|
1461
1598
|
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 ml-1 opacity-50" })
|
|
1462
1599
|
] }) }),
|
|
1463
|
-
/* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
/* @__PURE__ */ jsx(
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1600
|
+
/* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-72 p-0", children: [
|
|
1601
|
+
showSearch && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-2.5 py-2", children: [
|
|
1602
|
+
/* @__PURE__ */ jsx(Search, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }),
|
|
1603
|
+
/* @__PURE__ */ jsx(
|
|
1604
|
+
"input",
|
|
1605
|
+
{
|
|
1606
|
+
ref: inputRef,
|
|
1607
|
+
value: query,
|
|
1608
|
+
onChange: (e) => {
|
|
1609
|
+
setQuery(e.target.value);
|
|
1610
|
+
setActiveIndex(0);
|
|
1611
|
+
},
|
|
1612
|
+
onKeyDown: handleInputKeyDown,
|
|
1613
|
+
placeholder: t("customForms.searchDocumentTypes"),
|
|
1614
|
+
className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
1615
|
+
}
|
|
1616
|
+
)
|
|
1617
|
+
] }),
|
|
1618
|
+
/* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-[min(60vh,20rem)] overflow-y-auto overscroll-contain p-1", children: [
|
|
1619
|
+
groups.map((group, gi) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
|
|
1620
|
+
gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
1621
|
+
/* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
|
|
1622
|
+
group.items.map((opt) => {
|
|
1623
|
+
const Icon = opt.icon;
|
|
1624
|
+
flatIndex += 1;
|
|
1625
|
+
const index = flatIndex;
|
|
1626
|
+
return /* @__PURE__ */ jsxs(
|
|
1627
|
+
"button",
|
|
1628
|
+
{
|
|
1629
|
+
type: "button",
|
|
1630
|
+
"data-index": index,
|
|
1631
|
+
onClick: () => choose(opt),
|
|
1632
|
+
onMouseEnter: () => setActiveIndex(index),
|
|
1633
|
+
className: `relative flex w-full cursor-default select-none items-center gap-2 rounded-button px-2 py-1.5 text-left text-sm outline-none transition-colors ${index === activeIndex ? "bg-muted" : ""}`,
|
|
1634
|
+
children: [
|
|
1635
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1636
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
1637
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm truncate", children: opt.label }),
|
|
1638
|
+
opt.description && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground truncate", children: opt.description })
|
|
1639
|
+
] })
|
|
1640
|
+
]
|
|
1641
|
+
},
|
|
1642
|
+
opt.id
|
|
1643
|
+
);
|
|
1644
|
+
})
|
|
1645
|
+
] }, group.label)),
|
|
1646
|
+
!loaded && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-3", children: /* @__PURE__ */ jsx("div", { className: "h-4 w-4 border-2 border-primary border-t-transparent animate-spin rounded-full" }) }),
|
|
1647
|
+
loaded && options.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: t("customForms.noTemplates") }),
|
|
1648
|
+
loaded && options.length > 0 && filtered.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: t("customForms.noResults") })
|
|
1649
|
+
] })
|
|
1480
1650
|
] })
|
|
1481
1651
|
] });
|
|
1482
1652
|
}
|
|
@@ -1671,6 +1841,89 @@ function registerFormsTimelineSource() {
|
|
|
1671
1841
|
}
|
|
1672
1842
|
});
|
|
1673
1843
|
}
|
|
1844
|
+
var SUBMISSIONS = "plg_forms_rep_submissions";
|
|
1845
|
+
var CAT = tl("Forms", "Formul\xE1rios");
|
|
1846
|
+
var STATUS_LABELS = {
|
|
1847
|
+
draft: tl("Draft", "Rascunho"),
|
|
1848
|
+
completed: tl("Completed", "Conclu\xEDdo"),
|
|
1849
|
+
signed: tl("Signed", "Assinado"),
|
|
1850
|
+
archived: tl("Archived", "Arquivado")
|
|
1851
|
+
};
|
|
1852
|
+
function buildFormsAnalyticsCards() {
|
|
1853
|
+
const base = { domain: "custom_forms", compare: true, canonicalOwner: "public.documents" };
|
|
1854
|
+
return [
|
|
1855
|
+
{
|
|
1856
|
+
...base,
|
|
1857
|
+
id: "forms.submissions",
|
|
1858
|
+
name: tl("Form submissions", "Formul\xE1rios preenchidos"),
|
|
1859
|
+
description: tl(
|
|
1860
|
+
"Forms filled in during the period, whoever filled them.",
|
|
1861
|
+
"Formul\xE1rios preenchidos no per\xEDodo, por quem quer que seja."
|
|
1862
|
+
),
|
|
1863
|
+
icon: "FileText",
|
|
1864
|
+
category: CAT,
|
|
1865
|
+
grain: "submission",
|
|
1866
|
+
query: {
|
|
1867
|
+
source: { kind: "view", name: SUBMISSIONS },
|
|
1868
|
+
dimensions: [{ key: "created_at", label: tl("Date", "Data"), grain: "auto" }],
|
|
1869
|
+
measures: [{ key: "submissions", label: tl("Submissions", "Preenchimentos"), agg: "count", format: "number" }]
|
|
1870
|
+
},
|
|
1871
|
+
viz: { type: "kpi", spark: true },
|
|
1872
|
+
dashboard: { span: 1, order: 30 }
|
|
1873
|
+
},
|
|
1874
|
+
{
|
|
1875
|
+
...base,
|
|
1876
|
+
id: "forms.by-template",
|
|
1877
|
+
name: tl("Submissions by form", "Preenchimentos por formul\xE1rio"),
|
|
1878
|
+
description: tl(
|
|
1879
|
+
"Which templates are actually being used \u2014 and which one nobody answers.",
|
|
1880
|
+
"Quais modelos est\xE3o realmente em uso \u2014 e qual ningu\xE9m responde."
|
|
1881
|
+
),
|
|
1882
|
+
icon: "ClipboardList",
|
|
1883
|
+
category: CAT,
|
|
1884
|
+
grain: "submission",
|
|
1885
|
+
query: {
|
|
1886
|
+
source: { kind: "view", name: SUBMISSIONS },
|
|
1887
|
+
dimensions: [{ key: "template_name", label: tl("Form", "Formul\xE1rio") }],
|
|
1888
|
+
measures: [{ key: "submissions", label: tl("Submissions", "Preenchimentos"), agg: "count", format: "number" }],
|
|
1889
|
+
defaultSort: "submissions",
|
|
1890
|
+
defaultSortDir: "desc"
|
|
1891
|
+
},
|
|
1892
|
+
viz: { type: "breakdown", limit: 8 }
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
// The list the product never had: one row per filled form, with the
|
|
1896
|
+
// person behind it. No measures, so it is a plain select — the rows ARE
|
|
1897
|
+
// the report, and the viewer's export button turns them into a file.
|
|
1898
|
+
...base,
|
|
1899
|
+
id: "forms.submissions-list",
|
|
1900
|
+
name: tl("Submission list", "Lista de preenchimentos"),
|
|
1901
|
+
description: tl(
|
|
1902
|
+
"Every form filled in the period, with who filled it \u2014 ready to export.",
|
|
1903
|
+
"Todo formul\xE1rio preenchido no per\xEDodo, com quem preencheu \u2014 pronto para exportar."
|
|
1904
|
+
),
|
|
1905
|
+
icon: "Table",
|
|
1906
|
+
category: CAT,
|
|
1907
|
+
grain: "submission",
|
|
1908
|
+
query: {
|
|
1909
|
+
source: { kind: "view", name: SUBMISSIONS },
|
|
1910
|
+
defaultSort: "created_at",
|
|
1911
|
+
defaultSortDir: "desc"
|
|
1912
|
+
},
|
|
1913
|
+
columns: [
|
|
1914
|
+
{ key: "created_at", label: tl("Filled", "Preenchido em"), type: "date", sortable: true },
|
|
1915
|
+
{ key: "template_name", label: tl("Form", "Formul\xE1rio"), type: "text" },
|
|
1916
|
+
{ key: "person_name", label: tl("Person", "Pessoa"), type: "person", idKey: "person_id" },
|
|
1917
|
+
{ key: "person_email", label: tl("E-mail", "E-mail"), type: "email" },
|
|
1918
|
+
{ key: "person_phone", label: tl("Phone", "Telefone"), type: "text" },
|
|
1919
|
+
{ key: "status", label: tl("Status", "Situa\xE7\xE3o"), type: "text", valueMap: STATUS_LABELS },
|
|
1920
|
+
{ key: "template_category", label: tl("Category", "Categoria"), type: "text", visible: false },
|
|
1921
|
+
{ key: "signed_at", label: tl("Signed at", "Assinado em"), type: "date", visible: false }
|
|
1922
|
+
],
|
|
1923
|
+
viz: { type: "table" }
|
|
1924
|
+
}
|
|
1925
|
+
];
|
|
1926
|
+
}
|
|
1674
1927
|
function registerBuiltInProviders(formsProvider) {
|
|
1675
1928
|
registerDocumentTypeProvider({
|
|
1676
1929
|
id: "custom_forms:templates",
|
|
@@ -1747,7 +2000,6 @@ function createCustomFormsPlugin(options) {
|
|
|
1747
2000
|
const registries = [...formRegistries, ...options?.settingsRegistries ?? []];
|
|
1748
2001
|
const SettingsComponent = () => React2.createElement(PluginSettingsPanel, {
|
|
1749
2002
|
title: config.labels.settingsLabel,
|
|
1750
|
-
subtitle: config.labels.settingsSubtitle,
|
|
1751
2003
|
customTabs: [
|
|
1752
2004
|
{
|
|
1753
2005
|
id: "forms",
|
|
@@ -1765,6 +2017,7 @@ function createCustomFormsPlugin(options) {
|
|
|
1765
2017
|
PersonDocumentsWidget2.displayName = "PersonDocumentsWidget";
|
|
1766
2018
|
return {
|
|
1767
2019
|
id: "custom_forms",
|
|
2020
|
+
defaultAgentRole: "setup",
|
|
1768
2021
|
name: config.labels.pageTitle,
|
|
1769
2022
|
icon: "FileText",
|
|
1770
2023
|
version: "1.0.0",
|
|
@@ -1779,6 +2032,10 @@ function createCustomFormsPlugin(options) {
|
|
|
1779
2032
|
{ key: "form_templates", label: "Form templates", table: "plg_forms_templates" },
|
|
1780
2033
|
{ key: "documents_month", label: "Documents / month", table: "documents", period: "month" }
|
|
1781
2034
|
],
|
|
2035
|
+
// O formulário preenchido vira número e vira LISTA em /analytics — até aqui
|
|
2036
|
+
// ele só existia dentro da ficha de uma pessoa, um de cada vez. Read model
|
|
2037
|
+
// na migration 006.
|
|
2038
|
+
analyticsCards: options?.analyticsCards === false ? [] : buildFormsAnalyticsCards(),
|
|
1782
2039
|
navigation: [],
|
|
1783
2040
|
routes: [],
|
|
1784
2041
|
settings: [
|