@fayz-ai/plugin-forms 0.15.13 → 0.15.15
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/CustomFormsSettingsTab-EI4WKOYR.js +314 -0
- package/dist/CustomFormsSettingsTab-EI4WKOYR.js.map +1 -0
- package/dist/{FormBuilder-Q2Q42HAT.js → FormBuilder-7UXCXUOF.js} +7 -3
- package/dist/FormBuilder-7UXCXUOF.js.map +1 -0
- package/dist/PersonAnswerAlerts-MDBBY63K.js +98 -0
- package/dist/PersonAnswerAlerts-MDBBY63K.js.map +1 -0
- package/dist/PersonDocumentsWidget-RGN4UHS3.js +977 -0
- package/dist/PersonDocumentsWidget-RGN4UHS3.js.map +1 -0
- package/dist/chunk-5WW6FTNG.js +113 -0
- package/dist/chunk-5WW6FTNG.js.map +1 -0
- package/dist/chunk-66C3L4RE.js +17 -0
- package/dist/chunk-66C3L4RE.js.map +1 -0
- package/dist/chunk-BFXJLPKA.js +55 -0
- package/dist/chunk-BFXJLPKA.js.map +1 -0
- package/dist/chunk-RB7UYZLA.js +824 -0
- package/dist/chunk-RB7UYZLA.js.map +1 -0
- package/dist/chunk-X6CU44B4.js +223 -0
- package/dist/chunk-X6CU44B4.js.map +1 -0
- package/dist/chunk-YOFFKQD4.js +584 -0
- package/dist/chunk-YOFFKQD4.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -1507
- package/dist/index.js.map +1 -1
- package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/FormBuilder-Q2Q42HAT.js.map +0 -1
- package/dist/chunk-JRR7A5HN.js +0 -1678
- package/dist/chunk-JRR7A5HN.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { registerDocumentTypeProvider, normalizeCategory, CATEGORY_GROUP_ORDER, CATEGORY_ICONS, categoryLabel } from './chunk-5WW6FTNG.js';
|
|
2
|
+
export { registerDocumentTypeProvider } from './chunk-5WW6FTNG.js';
|
|
3
|
+
import { createOrUpdateFormTool } from './chunk-BFXJLPKA.js';
|
|
4
|
+
import { isFieldEmpty, FormRenderer, sanitizeHtml } from './chunk-RB7UYZLA.js';
|
|
5
|
+
import './chunk-YOFFKQD4.js';
|
|
6
|
+
import { CustomFormsProvider } from './chunk-66C3L4RE.js';
|
|
7
|
+
import React4 from 'react';
|
|
8
|
+
import { tl, lazyComponent, describeError, useActiveTenantId, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, getActiveTenantId, formatCurrency } from '@fayz-ai/core';
|
|
4
9
|
import { T } from '@fayz-ai/forms/data/tables';
|
|
5
|
-
import { createSafeFormsProvider, createFormsStore
|
|
6
|
-
import { defineSettingsTab, PluginSettingsPanel, registerTimelineSource, registerTabCountProvider
|
|
10
|
+
import { createSafeFormsProvider, createFormsStore } from '@fayz-ai/forms/store';
|
|
11
|
+
import { defineSettingsTab, PluginSettingsPanel, registerTimelineSource, registerTabCountProvider } from '@fayz-ai/admin';
|
|
7
12
|
import { FORMS_ZONES } from '@fayz-ai/forms/views/zones';
|
|
8
|
-
import { Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Skeleton, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, Input
|
|
13
|
+
import { Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Skeleton, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, Input } from '@fayz-ai/ui';
|
|
9
14
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
10
|
-
import { ClipboardList, QrCode, Printer, Check, CheckCircle2, Clock, Circle, X, Camera, FileText,
|
|
15
|
+
import { ClipboardList, QrCode, Printer, Check, CheckCircle2, Clock, Circle, X, Camera, FileText, Image, Paperclip } from 'lucide-react';
|
|
11
16
|
import { shownInChecklist, isOutstanding } from '@fayz-ai/forms/domain/requirement';
|
|
12
17
|
export { isOutstanding } from '@fayz-ai/forms/domain/requirement';
|
|
13
18
|
export { getPersonFormRequirements } from '@fayz-ai/forms/data/supabase';
|
|
@@ -50,15 +55,15 @@ function FormFillDialog({
|
|
|
50
55
|
recommendationShortcuts,
|
|
51
56
|
provider: given
|
|
52
57
|
}) {
|
|
53
|
-
const provider =
|
|
54
|
-
const store =
|
|
55
|
-
const config =
|
|
56
|
-
const [template, setTemplate] =
|
|
57
|
-
const [erro, setErro] =
|
|
58
|
-
const [data, setData] =
|
|
59
|
-
const [salvando, setSalvando] =
|
|
60
|
-
const [missing, setMissing] =
|
|
61
|
-
|
|
58
|
+
const provider = React4.useMemo(() => given ?? createSafeFormsProvider(), [given]);
|
|
59
|
+
const store = React4.useMemo(() => createFormsStore(provider), [provider]);
|
|
60
|
+
const config = React4.useMemo(() => resolveConfig(), []);
|
|
61
|
+
const [template, setTemplate] = React4.useState(null);
|
|
62
|
+
const [erro, setErro] = React4.useState(null);
|
|
63
|
+
const [data, setData] = React4.useState({});
|
|
64
|
+
const [salvando, setSalvando] = React4.useState(false);
|
|
65
|
+
const [missing, setMissing] = React4.useState(/* @__PURE__ */ new Set());
|
|
66
|
+
React4.useEffect(() => {
|
|
62
67
|
if (!template || missing.size === 0) return;
|
|
63
68
|
const still = new Set([...missing].filter((id) => {
|
|
64
69
|
const f = template.schema.fields.find((x) => x.id === id);
|
|
@@ -99,7 +104,7 @@ function FormFillDialog({
|
|
|
99
104
|
setSalvando(false);
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
|
-
|
|
107
|
+
React4.useEffect(() => {
|
|
103
108
|
let vivo = true;
|
|
104
109
|
(async () => {
|
|
105
110
|
try {
|
|
@@ -196,10 +201,10 @@ function Linha({ item }) {
|
|
|
196
201
|
] });
|
|
197
202
|
}
|
|
198
203
|
function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, sendLink, className, provider: given }) {
|
|
199
|
-
const provider =
|
|
204
|
+
const provider = React4.useMemo(() => given ?? createSafeFormsProvider(), [given]);
|
|
200
205
|
const tenantId2 = useActiveTenantId();
|
|
201
|
-
const [rows, setRows] =
|
|
202
|
-
|
|
206
|
+
const [rows, setRows] = React4.useState(null);
|
|
207
|
+
React4.useEffect(() => {
|
|
203
208
|
if (!personId || !tenantId2) {
|
|
204
209
|
setRows([]);
|
|
205
210
|
return;
|
|
@@ -215,7 +220,7 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, se
|
|
|
215
220
|
cancelled = true;
|
|
216
221
|
};
|
|
217
222
|
}, [personId, tenantId2, version, provider]);
|
|
218
|
-
const itens =
|
|
223
|
+
const itens = React4.useMemo(() => [
|
|
219
224
|
...(rows ?? []).filter(shownInChecklist).map((r) => {
|
|
220
225
|
const pendente = isOutstanding(r);
|
|
221
226
|
return {
|
|
@@ -233,7 +238,7 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, se
|
|
|
233
238
|
const pendentes = itens.filter((i) => i.state !== "done");
|
|
234
239
|
const faltando = pendentes.length;
|
|
235
240
|
const chave = pendentes.map((i) => i.label).join("|");
|
|
236
|
-
|
|
241
|
+
React4.useEffect(() => {
|
|
237
242
|
onPending?.(chave ? chave.split("|") : []);
|
|
238
243
|
}, [chave]);
|
|
239
244
|
if (rows === null) return /* @__PURE__ */ jsx(Skeleton, { className: `h-12 w-full ${className ?? ""}` });
|
|
@@ -265,8 +270,8 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, se
|
|
|
265
270
|
|
|
266
271
|
// src/components/formsZoneWidgets.ts
|
|
267
272
|
function formsZoneWidgets(provider) {
|
|
268
|
-
const PersonChecklist = (props) =>
|
|
269
|
-
const FormFill = (props) =>
|
|
273
|
+
const PersonChecklist = (props) => React4.createElement(PersonFormsAlert, { ...props, provider });
|
|
274
|
+
const FormFill = (props) => React4.createElement(FormFillDialog, { ...props, provider });
|
|
270
275
|
return [
|
|
271
276
|
{
|
|
272
277
|
id: "forms-person-checklist",
|
|
@@ -565,1463 +570,6 @@ var customFormsLocales = {
|
|
|
565
570
|
en,
|
|
566
571
|
"pt-BR": ptBR
|
|
567
572
|
};
|
|
568
|
-
var CATEGORY_COLORS = {
|
|
569
|
-
anamnesis: "bg-info-soft text-info-soft-foreground",
|
|
570
|
-
evaluation: "bg-info-soft text-info-soft-foreground",
|
|
571
|
-
photos: "bg-muted text-muted-foreground",
|
|
572
|
-
evolution: "bg-success-soft text-success-soft-foreground",
|
|
573
|
-
report: "bg-magic-soft text-magic-soft-foreground",
|
|
574
|
-
contract: "bg-warning-soft text-warning-soft-foreground",
|
|
575
|
-
general: "bg-muted text-muted-foreground"
|
|
576
|
-
};
|
|
577
|
-
function TemplateListView({ store, config, onEdit, onNew }) {
|
|
578
|
-
const t = useTranslation();
|
|
579
|
-
const [search, setSearch] = useState("");
|
|
580
|
-
const templates = store((s) => s.templates);
|
|
581
|
-
const loading = store((s) => s.templatesLoading);
|
|
582
|
-
const usage = store((s) => s.templateUsage);
|
|
583
|
-
const orgId = getActiveTenantId();
|
|
584
|
-
const createActions = useMemo(() => {
|
|
585
|
-
const permission = { feature: "custom_forms", action: "create" };
|
|
586
|
-
return [
|
|
587
|
-
{
|
|
588
|
-
id: "new",
|
|
589
|
-
label: t("customForms.newTemplate"),
|
|
590
|
-
description: t("customForms.newByHandHint"),
|
|
591
|
-
icon: Plus,
|
|
592
|
-
intent: "primary",
|
|
593
|
-
creates: true,
|
|
594
|
-
onSelect: onNew,
|
|
595
|
-
permission
|
|
596
|
-
},
|
|
597
|
-
{
|
|
598
|
-
id: "ai",
|
|
599
|
-
label: t("assistant.askAI"),
|
|
600
|
-
description: t("customForms.newWithAIHint"),
|
|
601
|
-
icon: WandSparkles,
|
|
602
|
-
ai: true,
|
|
603
|
-
intent: "secondary",
|
|
604
|
-
creates: true,
|
|
605
|
-
onSelect: () => askAI({
|
|
606
|
-
hint: t("customForms.askPlaceholder"),
|
|
607
|
-
intro: {
|
|
608
|
-
role: "setup",
|
|
609
|
-
persona: t("customForms.askPersona"),
|
|
610
|
-
pitch: t("customForms.askPitch"),
|
|
611
|
-
description: t("customForms.askDescription"),
|
|
612
|
-
suggestions: [0, 1, 2].map((i) => ({ label: t(`customForms.askSuggestion.${i}`) }))
|
|
613
|
-
}
|
|
614
|
-
}),
|
|
615
|
-
permission
|
|
616
|
-
}
|
|
617
|
-
];
|
|
618
|
-
}, [t, onNew]);
|
|
619
|
-
const version = useDataVersion(`${FORM_TEMPLATE_EVENT}*`);
|
|
620
|
-
useEffect(() => {
|
|
621
|
-
if (!orgId) return;
|
|
622
|
-
const timer = setTimeout(() => {
|
|
623
|
-
store.getState().fetchTemplates({ search: search || void 0 });
|
|
624
|
-
}, 50);
|
|
625
|
-
return () => clearTimeout(timer);
|
|
626
|
-
}, [orgId, search, version]);
|
|
627
|
-
useEffect(() => {
|
|
628
|
-
if (!orgId) return;
|
|
629
|
-
void store.getState().fetchTemplateUsage();
|
|
630
|
-
}, [orgId]);
|
|
631
|
-
const handleArchive = useCallback(async (template) => {
|
|
632
|
-
if (!window.confirm(t("customForms.archiveTemplateConfirm"))) return;
|
|
633
|
-
await store.getState().updateTemplate(template.id, { isActive: false });
|
|
634
|
-
}, [store, t]);
|
|
635
|
-
const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
|
|
636
|
-
const categoryLabel2 = (cat) => t(`customForms.category.${cat}`);
|
|
637
|
-
const columns = useMemo(() => [
|
|
638
|
-
{
|
|
639
|
-
id: "name",
|
|
640
|
-
header: t("customForms.template"),
|
|
641
|
-
cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
642
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: row.original.name }),
|
|
643
|
-
row.original.description && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: row.original.description })
|
|
644
|
-
] })
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
id: "category",
|
|
648
|
-
header: t("customForms.categoryColumn"),
|
|
649
|
-
cell: ({ row }) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${CATEGORY_COLORS[row.original.category]}`, children: categoryLabel2(row.original.category) })
|
|
650
|
-
},
|
|
651
|
-
{
|
|
652
|
-
id: "fields",
|
|
653
|
-
header: t("customForms.builder.fields"),
|
|
654
|
-
cell: ({ row }) => /* @__PURE__ */ jsxs("span", { className: "tabular-nums text-muted-foreground", children: [
|
|
655
|
-
row.original.schema.fields.length,
|
|
656
|
-
row.original.version > 1 && /* @__PURE__ */ jsxs("span", { className: "ml-2", children: [
|
|
657
|
-
"v",
|
|
658
|
-
row.original.version
|
|
659
|
-
] })
|
|
660
|
-
] })
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
id: "required",
|
|
664
|
-
header: "Exigido",
|
|
665
|
-
// A PORTA da migration 001. Sem ela as duas colunas novas ficam no banco
|
|
666
|
-
// e o aviso de formulário pendente nunca acende para ninguém — código
|
|
667
|
-
// pronto sem consumidor é o mesmo que código ausente.
|
|
668
|
-
cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
669
|
-
/* @__PURE__ */ jsx(
|
|
670
|
-
Switch,
|
|
671
|
-
{
|
|
672
|
-
size: "sm",
|
|
673
|
-
checked: row.original.requiredOnAttendance,
|
|
674
|
-
onCheckedChange: (on) => {
|
|
675
|
-
void store.getState().updateTemplate(row.original.id, {
|
|
676
|
-
requiredOnAttendance: on,
|
|
677
|
-
// Seis meses é o que o V1 usa na anamnese, e é o padrão que faz
|
|
678
|
-
// a validade existir sem pedir mais uma decisão na hora de
|
|
679
|
-
// ligar. Zero validade é o mesmo que "vale para sempre", que
|
|
680
|
-
// deixa uma anamnese de 2019 contando como preenchida.
|
|
681
|
-
validityDays: on ? row.original.validityDays ?? 180 : null
|
|
682
|
-
});
|
|
683
|
-
},
|
|
684
|
-
"aria-label": `Exigir ${row.original.name} de todo cliente`
|
|
685
|
-
}
|
|
686
|
-
),
|
|
687
|
-
row.original.requiredOnAttendance && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground", children: [
|
|
688
|
-
"vale ",
|
|
689
|
-
row.original.validityDays ?? "\u221E",
|
|
690
|
-
" dias"
|
|
691
|
-
] })
|
|
692
|
-
] })
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
id: "alert",
|
|
696
|
-
header: "Alerta",
|
|
697
|
-
// A SEGUNDA PERGUNTA DO V1, e ela é outra pergunta mesmo: exigir põe a
|
|
698
|
-
// bola no checklist do atendimento — trabalho a fazer com a pessoa na
|
|
699
|
-
// cadeira; alertar leva a falta para a FRENTE, na faixa âmbar do
|
|
700
|
-
// agendamento e da ficha. Uma casa exige anamnese e não quer a faixa
|
|
701
|
-
// toda vez que marca horário, e o V1 respeita isso desligado.
|
|
702
|
-
cell: ({ row }) => /* @__PURE__ */ jsx(
|
|
703
|
-
Switch,
|
|
704
|
-
{
|
|
705
|
-
size: "sm",
|
|
706
|
-
disabled: !row.original.requiredOnAttendance,
|
|
707
|
-
checked: row.original.requiredOnAttendance && row.original.alertWhenMissing,
|
|
708
|
-
onCheckedChange: (on) => {
|
|
709
|
-
void store.getState().updateTemplate(row.original.id, { alertWhenMissing: on });
|
|
710
|
-
},
|
|
711
|
-
"aria-label": `Alertar quando ${row.original.name} estiver faltando`
|
|
712
|
-
}
|
|
713
|
-
)
|
|
714
|
-
},
|
|
715
|
-
{
|
|
716
|
-
id: "usage",
|
|
717
|
-
header: t("customForms.usageColumn"),
|
|
718
|
-
// "0 preenchimentos" é informação, não ausência dela: é o que separa um
|
|
719
|
-
// formulário que o salão usa todo dia de um que sobrou de uma importação.
|
|
720
|
-
cell: ({ row }) => {
|
|
721
|
-
const u = usage[row.original.id];
|
|
722
|
-
return /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
723
|
-
t("customForms.usageCount", { count: String(u?.documentCount ?? 0) }),
|
|
724
|
-
u?.lastDocumentAt && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
725
|
-
/* @__PURE__ */ jsx("span", { className: "px-1", children: "\xB7" }),
|
|
726
|
-
t("customForms.usageLast", { date: new Date(u.lastDocumentAt).toLocaleDateString() })
|
|
727
|
-
] })
|
|
728
|
-
] });
|
|
729
|
-
}
|
|
730
|
-
},
|
|
731
|
-
{
|
|
732
|
-
id: "actions",
|
|
733
|
-
header: "",
|
|
734
|
-
// RowActions is the one renderer for what can be done to a single row —
|
|
735
|
-
// it owns the sizing, the accessible name and the rule that keeps a
|
|
736
|
-
// destructive action off the row itself.
|
|
737
|
-
cell: ({ row }) => /* @__PURE__ */ jsx(
|
|
738
|
-
RowActions,
|
|
739
|
-
{
|
|
740
|
-
actions: [
|
|
741
|
-
{
|
|
742
|
-
id: "edit",
|
|
743
|
-
label: t("common.edit"),
|
|
744
|
-
icon: Pencil,
|
|
745
|
-
onSelect: () => onEdit(row.original)
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
id: "archive",
|
|
749
|
-
label: t("customForms.archive"),
|
|
750
|
-
icon: Archive,
|
|
751
|
-
intent: "danger",
|
|
752
|
-
onSelect: () => {
|
|
753
|
-
void handleArchive(row.original);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
]
|
|
757
|
-
}
|
|
758
|
-
)
|
|
759
|
-
}
|
|
760
|
-
], [t, usage, onEdit, handleArchive]);
|
|
761
|
-
const emptyState = /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-6 text-center", children: [
|
|
762
|
-
/* @__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" }) }),
|
|
763
|
-
/* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: t("customForms.noTemplates") }),
|
|
764
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: t("customForms.noTemplatesDescription") }),
|
|
765
|
-
/* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
|
|
766
|
-
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
767
|
-
t("customForms.newTemplate")
|
|
768
|
-
] }) })
|
|
769
|
-
] });
|
|
770
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
771
|
-
/* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: createActions }) }),
|
|
772
|
-
/* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
773
|
-
/* @__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: [
|
|
774
|
-
/* @__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" }),
|
|
775
|
-
/* @__PURE__ */ jsx(
|
|
776
|
-
Input,
|
|
777
|
-
{
|
|
778
|
-
value: search,
|
|
779
|
-
onChange: (e) => setSearch(e.target.value),
|
|
780
|
-
placeholder: t("common.search"),
|
|
781
|
-
className: "h-auto rounded-none border-0 bg-transparent py-1.5 pl-8 pr-3 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
782
|
-
}
|
|
783
|
-
)
|
|
784
|
-
] }) }),
|
|
785
|
-
/* @__PURE__ */ jsx(
|
|
786
|
-
DataTable,
|
|
787
|
-
{
|
|
788
|
-
variant: "bare",
|
|
789
|
-
columns,
|
|
790
|
-
data: loading ? [] : visibleTemplates,
|
|
791
|
-
loading,
|
|
792
|
-
skeletonRows: 3,
|
|
793
|
-
emptyMessage: emptyState,
|
|
794
|
-
onRowClick: (row) => onEdit(row)
|
|
795
|
-
}
|
|
796
|
-
)
|
|
797
|
-
] })
|
|
798
|
-
] });
|
|
799
|
-
}
|
|
800
|
-
var ROUTE_BASE = "/settings/custom_forms";
|
|
801
|
-
function readBuilderIdFromHash() {
|
|
802
|
-
const hash = window.location.hash.slice(1) || "/";
|
|
803
|
-
const m = hash.match(/\/settings\/custom_forms\/templates\/([^/]+)/);
|
|
804
|
-
if (!m) return null;
|
|
805
|
-
return m[1] === "new" ? "__new__" : m[1];
|
|
806
|
-
}
|
|
807
|
-
function CustomFormsSettingsTab({ config, provider, store }) {
|
|
808
|
-
const [builderTemplateId, setBuilderTemplateId] = useState(readBuilderIdFromHash);
|
|
809
|
-
const handleNew = useCallback(() => {
|
|
810
|
-
window.location.hash = `#${ROUTE_BASE}/templates/new`;
|
|
811
|
-
}, []);
|
|
812
|
-
const handleEdit = useCallback((template) => {
|
|
813
|
-
window.location.hash = `#${ROUTE_BASE}/templates/${template.id}`;
|
|
814
|
-
}, []);
|
|
815
|
-
const handleBackFromBuilder = useCallback(() => {
|
|
816
|
-
window.location.hash = `#${ROUTE_BASE}/forms`;
|
|
817
|
-
store.getState().fetchTemplates();
|
|
818
|
-
}, [store]);
|
|
819
|
-
React13.useEffect(() => {
|
|
820
|
-
const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
|
|
821
|
-
window.addEventListener("hashchange", sync);
|
|
822
|
-
return () => window.removeEventListener("hashchange", sync);
|
|
823
|
-
}, []);
|
|
824
|
-
return builderTemplateId ? /* @__PURE__ */ jsx(
|
|
825
|
-
BuilderWrapper,
|
|
826
|
-
{
|
|
827
|
-
templateId: builderTemplateId === "__new__" ? void 0 : builderTemplateId,
|
|
828
|
-
store,
|
|
829
|
-
provider,
|
|
830
|
-
config,
|
|
831
|
-
onBack: handleBackFromBuilder
|
|
832
|
-
}
|
|
833
|
-
) : /* @__PURE__ */ jsx(
|
|
834
|
-
TemplateListView,
|
|
835
|
-
{
|
|
836
|
-
store,
|
|
837
|
-
config,
|
|
838
|
-
onEdit: handleEdit,
|
|
839
|
-
onNew: handleNew
|
|
840
|
-
}
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
function BuilderWrapper({
|
|
844
|
-
templateId,
|
|
845
|
-
store,
|
|
846
|
-
provider,
|
|
847
|
-
config,
|
|
848
|
-
onBack
|
|
849
|
-
}) {
|
|
850
|
-
const FormBuilderLazy = React13.lazy(
|
|
851
|
-
() => import('./FormBuilder-Q2Q42HAT.js').then((m) => ({ default: m.FormBuilder }))
|
|
852
|
-
);
|
|
853
|
-
return /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
|
|
854
|
-
React13.Suspense,
|
|
855
|
-
{
|
|
856
|
-
fallback: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx("div", { className: "h-6 w-6 border-2 border-primary border-t-transparent animate-spin rounded-full" }) }),
|
|
857
|
-
children: /* @__PURE__ */ jsx(
|
|
858
|
-
FormBuilderLazy,
|
|
859
|
-
{
|
|
860
|
-
templateId,
|
|
861
|
-
store,
|
|
862
|
-
provider,
|
|
863
|
-
config,
|
|
864
|
-
onBack
|
|
865
|
-
}
|
|
866
|
-
)
|
|
867
|
-
}
|
|
868
|
-
) });
|
|
869
|
-
}
|
|
870
|
-
var MAX_FIELDS = 8;
|
|
871
|
-
var MAX_CHARS = 280;
|
|
872
|
-
function isEmpty(value) {
|
|
873
|
-
if (value == null) return true;
|
|
874
|
-
if (typeof value === "string") return value.trim() === "";
|
|
875
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
876
|
-
return false;
|
|
877
|
-
}
|
|
878
|
-
function asText(value) {
|
|
879
|
-
if (Array.isArray(value)) return value.map((v) => String(v)).join(", ");
|
|
880
|
-
if (typeof value === "boolean") return value ? "Sim" : "N\xE3o";
|
|
881
|
-
return htmlToPlainText(String(value)) ?? String(value);
|
|
882
|
-
}
|
|
883
|
-
function PreviewMedia({ items, provider }) {
|
|
884
|
-
const [urls, setUrls] = useState([]);
|
|
885
|
-
useEffect(() => {
|
|
886
|
-
let alive = true;
|
|
887
|
-
Promise.all(items.map((m) => Promise.resolve(provider.mediaUrl?.(m.path) ?? null).catch(() => null))).then((u) => {
|
|
888
|
-
if (alive) setUrls(u);
|
|
889
|
-
});
|
|
890
|
-
return () => {
|
|
891
|
-
alive = false;
|
|
892
|
-
};
|
|
893
|
-
}, [items.map((m) => m.path).join("|"), provider]);
|
|
894
|
-
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 pt-1", children: items.map((m, i) => urls[i] ? m.type === "video" ? /* @__PURE__ */ jsx("video", { src: urls[i], className: "h-20 w-20 rounded-md border object-cover", preload: "metadata" }, m.path) : /* @__PURE__ */ jsx("img", { src: urls[i], alt: "", className: `h-20 rounded-md border object-cover ${m.type === "signature" ? "bg-white" : "w-16"}` }, m.path) : /* @__PURE__ */ jsx("div", { className: "h-20 w-16 animate-pulse rounded-md bg-muted" }, m.path)) });
|
|
895
|
-
}
|
|
896
|
-
function mediaOf(field, value) {
|
|
897
|
-
if (field.type === "stencil_photo" || field.type === "signature") return isMedia(value) ? [value] : null;
|
|
898
|
-
if (field.type === "gallery") return Array.isArray(value) ? value.filter(isMedia) : null;
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
function fieldText(field, value) {
|
|
902
|
-
if (field.type === "yes_no" || field.type === "yes_no_text") {
|
|
903
|
-
const a = yesNoAnswer(value);
|
|
904
|
-
const extra = value && typeof value === "object" ? value.text?.trim() : "";
|
|
905
|
-
return `${a === "sim" ? "Sim" : a === "nao" ? "N\xE3o" : ""}${extra ? ` \u2014 ${extra}` : ""}`;
|
|
906
|
-
}
|
|
907
|
-
if (field.type === "recommendation" && Array.isArray(value)) {
|
|
908
|
-
return value.map((l) => `${l.quantity > 1 ? `${l.quantity}\xD7 ` : ""}${l.name}`).join(" \xB7 ");
|
|
909
|
-
}
|
|
910
|
-
return asText(value);
|
|
911
|
-
}
|
|
912
|
-
function truncate(text) {
|
|
913
|
-
return text.length > MAX_CHARS ? `${text.slice(0, MAX_CHARS).trimEnd()}\u2026` : text;
|
|
914
|
-
}
|
|
915
|
-
function DocumentPreview({
|
|
916
|
-
document: doc,
|
|
917
|
-
provider,
|
|
918
|
-
onOpen
|
|
919
|
-
}) {
|
|
920
|
-
const t = useTranslation();
|
|
921
|
-
const [template, setTemplate] = useState(null);
|
|
922
|
-
const [loading, setLoading] = useState(true);
|
|
923
|
-
useEffect(() => {
|
|
924
|
-
let alive = true;
|
|
925
|
-
if (!doc.templateId) {
|
|
926
|
-
setLoading(false);
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
void provider.getTemplateById(doc.templateId).then((found) => {
|
|
930
|
-
if (alive) {
|
|
931
|
-
setTemplate(found);
|
|
932
|
-
setLoading(false);
|
|
933
|
-
}
|
|
934
|
-
}).catch(() => {
|
|
935
|
-
if (alive) setLoading(false);
|
|
936
|
-
});
|
|
937
|
-
return () => {
|
|
938
|
-
alive = false;
|
|
939
|
-
};
|
|
940
|
-
}, [doc.templateId, provider]);
|
|
941
|
-
if (doc.kind === "image" && doc.fileUrl) {
|
|
942
|
-
return /* @__PURE__ */ jsx(
|
|
943
|
-
"img",
|
|
944
|
-
{
|
|
945
|
-
src: doc.fileUrl,
|
|
946
|
-
alt: doc.fileName ?? "",
|
|
947
|
-
className: "max-h-64 rounded-lg object-contain"
|
|
948
|
-
}
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
if (doc.kind === "attachment") {
|
|
952
|
-
return /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
953
|
-
doc.fileName ?? t("customForms.documentFallbackTitle"),
|
|
954
|
-
doc.fileSize ? ` \xB7 ${(doc.fileSize / 1024).toFixed(1)} KB` : ""
|
|
955
|
-
] });
|
|
956
|
-
}
|
|
957
|
-
if (loading) {
|
|
958
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
959
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-1/3 animate-pulse rounded bg-muted" }),
|
|
960
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-2/3 animate-pulse rounded bg-muted" })
|
|
961
|
-
] });
|
|
962
|
-
}
|
|
963
|
-
const fields = template?.schema?.fields ?? [];
|
|
964
|
-
const answered = fields.filter((f) => f.type !== "title" && !isEmpty(doc.data?.[f.id])).sort((a, b) => a.row - b.row || a.col - b.col);
|
|
965
|
-
if (answered.length === 0) {
|
|
966
|
-
return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("customForms.previewEmpty") });
|
|
967
|
-
}
|
|
968
|
-
const shown = answered.slice(0, MAX_FIELDS);
|
|
969
|
-
const hidden = answered.length - shown.length;
|
|
970
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
971
|
-
/* @__PURE__ */ jsx("dl", { className: "grid gap-x-6 gap-y-2 sm:grid-cols-2", children: shown.map((field) => {
|
|
972
|
-
const media = mediaOf(field, doc.data[field.id]);
|
|
973
|
-
const text = media ? "" : truncate(fieldText(field, doc.data[field.id]));
|
|
974
|
-
const wide = field.type === "memo" || field.type === "richtext" || field.type === "gallery" || text.length > 60;
|
|
975
|
-
return /* @__PURE__ */ jsxs("div", { className: `min-w-0 ${wide ? "sm:col-span-2" : ""}`, children: [
|
|
976
|
-
/* @__PURE__ */ jsx("dt", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: htmlToPlainText(field.label) ?? field.label }),
|
|
977
|
-
/* @__PURE__ */ jsx("dd", { className: "whitespace-pre-wrap text-sm leading-6 text-foreground", children: media ? /* @__PURE__ */ jsx(PreviewMedia, { items: media, provider }) : text })
|
|
978
|
-
] }, field.id);
|
|
979
|
-
}) }),
|
|
980
|
-
hidden > 0 && /* @__PURE__ */ jsx(
|
|
981
|
-
Button,
|
|
982
|
-
{
|
|
983
|
-
variant: "bare",
|
|
984
|
-
size: "bare",
|
|
985
|
-
type: "button",
|
|
986
|
-
onClick: onOpen,
|
|
987
|
-
className: "text-xs font-medium text-primary hover:underline",
|
|
988
|
-
children: t("customForms.previewMore", { count: String(hidden) })
|
|
989
|
-
}
|
|
990
|
-
)
|
|
991
|
-
] });
|
|
992
|
-
}
|
|
993
|
-
var CATEGORY_ORDER = [
|
|
994
|
-
"anamnesis",
|
|
995
|
-
"evaluation",
|
|
996
|
-
"photos",
|
|
997
|
-
"evolution",
|
|
998
|
-
"report",
|
|
999
|
-
"contract",
|
|
1000
|
-
"general"
|
|
1001
|
-
];
|
|
1002
|
-
var CATEGORY_GROUP_ORDER = {
|
|
1003
|
-
anamnesis: 10,
|
|
1004
|
-
evaluation: 12,
|
|
1005
|
-
photos: 14,
|
|
1006
|
-
evolution: 20,
|
|
1007
|
-
report: 30,
|
|
1008
|
-
contract: 40,
|
|
1009
|
-
general: 50
|
|
1010
|
-
};
|
|
1011
|
-
var CATEGORY_ICONS = {
|
|
1012
|
-
anamnesis: ClipboardList,
|
|
1013
|
-
evaluation: Stethoscope,
|
|
1014
|
-
photos: Camera,
|
|
1015
|
-
evolution: Activity,
|
|
1016
|
-
report: FileCheck2,
|
|
1017
|
-
contract: FileSignature,
|
|
1018
|
-
general: FileText
|
|
1019
|
-
};
|
|
1020
|
-
function categoryLabel(category) {
|
|
1021
|
-
switch (category) {
|
|
1022
|
-
case "anamnesis":
|
|
1023
|
-
return tl("Anamnesis", "Anamnese");
|
|
1024
|
-
case "evaluation":
|
|
1025
|
-
return tl("Evaluation", "Avalia\xE7\xE3o");
|
|
1026
|
-
case "photos":
|
|
1027
|
-
return tl("Photos", "Fotos");
|
|
1028
|
-
case "evolution":
|
|
1029
|
-
return tl("Evolution", "Evolu\xE7\xE3o");
|
|
1030
|
-
case "report":
|
|
1031
|
-
return tl("Report", "Laudo");
|
|
1032
|
-
case "contract":
|
|
1033
|
-
return tl("Contract", "Contrato");
|
|
1034
|
-
default:
|
|
1035
|
-
return tl("General", "Geral");
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
function normalizeCategory(value) {
|
|
1039
|
-
return CATEGORY_ORDER.includes(value) ? value : "general";
|
|
1040
|
-
}
|
|
1041
|
-
var CATEGORY_TIMELINE_ICON = {
|
|
1042
|
-
anamnesis: "ClipboardList",
|
|
1043
|
-
evaluation: "Stethoscope",
|
|
1044
|
-
photos: "Camera",
|
|
1045
|
-
evolution: "Activity",
|
|
1046
|
-
report: "FileCheck2",
|
|
1047
|
-
contract: "FileSignature",
|
|
1048
|
-
general: "FileText"
|
|
1049
|
-
};
|
|
1050
|
-
var CATEGORY_TONE = {
|
|
1051
|
-
anamnesis: "primary",
|
|
1052
|
-
evaluation: "primary",
|
|
1053
|
-
photos: "info",
|
|
1054
|
-
evolution: "info",
|
|
1055
|
-
report: "success",
|
|
1056
|
-
contract: "warning",
|
|
1057
|
-
general: "neutral"
|
|
1058
|
-
};
|
|
1059
|
-
function bucketOf(doc) {
|
|
1060
|
-
if (doc.kind === "image" || doc.kind === "attachment") return "file";
|
|
1061
|
-
return normalizeCategory(doc.templateCategory);
|
|
1062
|
-
}
|
|
1063
|
-
function subtitleFor(doc) {
|
|
1064
|
-
const title = doc.title?.trim();
|
|
1065
|
-
if (!title) return void 0;
|
|
1066
|
-
const model = doc.templateName?.trim();
|
|
1067
|
-
if (!model) return title;
|
|
1068
|
-
if (title === model) return void 0;
|
|
1069
|
-
return title.toLowerCase().startsWith(model.toLowerCase()) ? void 0 : title;
|
|
1070
|
-
}
|
|
1071
|
-
function documentToTimelineItem(doc, fallbackTitle) {
|
|
1072
|
-
const b = bucketOf(doc);
|
|
1073
|
-
const secondary = subtitleFor(doc);
|
|
1074
|
-
return {
|
|
1075
|
-
id: doc.id,
|
|
1076
|
-
sourceId: b,
|
|
1077
|
-
occurredAt: doc.createdAt,
|
|
1078
|
-
title: doc.templateName ?? doc.title ?? fallbackTitle,
|
|
1079
|
-
description: secondary,
|
|
1080
|
-
badge: b === "file" ? void 0 : categoryLabel(b),
|
|
1081
|
-
icon: b === "file" ? doc.kind === "image" ? "Image" : "Paperclip" : CATEGORY_TIMELINE_ICON[b],
|
|
1082
|
-
tone: b === "file" ? "neutral" : CATEGORY_TONE[b]
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
var ACTIVE_STATUSES = ["draft", "completed", "signed"];
|
|
1086
|
-
var EXPAND_KEY = "fayz.forms.documents.expandAll";
|
|
1087
|
-
function DocumentList({ personId, provider, store, onView, onFileDrop, kinds }) {
|
|
1088
|
-
const acceptsFiles = Boolean(onFileDrop) && (!kinds || kinds.includes("attachment"));
|
|
1089
|
-
const t = useTranslation();
|
|
1090
|
-
const [documents, setDocuments] = useState([]);
|
|
1091
|
-
const [total, setTotal] = useState(0);
|
|
1092
|
-
const [page, setPage] = useState(1);
|
|
1093
|
-
const [loading, setLoading] = useState(true);
|
|
1094
|
-
const [loadingMore, setLoadingMore] = useState(false);
|
|
1095
|
-
const [counts, setCounts] = useState({});
|
|
1096
|
-
const [bucket, setBucket] = useState(void 0);
|
|
1097
|
-
const [isDragOver, setIsDragOver] = useState(false);
|
|
1098
|
-
const [expandAll, setExpandAll] = useState(() => {
|
|
1099
|
-
try {
|
|
1100
|
-
return localStorage.getItem(EXPAND_KEY) === "1";
|
|
1101
|
-
} catch {
|
|
1102
|
-
return false;
|
|
1103
|
-
}
|
|
1104
|
-
});
|
|
1105
|
-
const toggleExpandAll = useCallback(() => {
|
|
1106
|
-
setExpandAll((prev) => {
|
|
1107
|
-
try {
|
|
1108
|
-
localStorage.setItem(EXPAND_KEY, prev ? "0" : "1");
|
|
1109
|
-
} catch {
|
|
1110
|
-
}
|
|
1111
|
-
return !prev;
|
|
1112
|
-
});
|
|
1113
|
-
}, []);
|
|
1114
|
-
const [openIds, setOpenIds] = useState(/* @__PURE__ */ new Set());
|
|
1115
|
-
const dragCounter = useRef(0);
|
|
1116
|
-
const PAGE_SIZE = 20;
|
|
1117
|
-
const queryFor = useCallback((nextPage) => ({
|
|
1118
|
-
personId,
|
|
1119
|
-
kinds,
|
|
1120
|
-
templateCategory: bucket && bucket !== "file" ? bucket : void 0,
|
|
1121
|
-
...bucket === "file" ? { kinds: ["image", "attachment"] } : {},
|
|
1122
|
-
status: ACTIVE_STATUSES,
|
|
1123
|
-
page: nextPage,
|
|
1124
|
-
pageSize: PAGE_SIZE
|
|
1125
|
-
}), [personId, kinds, bucket]);
|
|
1126
|
-
const fetchDocs = useCallback(async () => {
|
|
1127
|
-
setLoading(true);
|
|
1128
|
-
try {
|
|
1129
|
-
const result = await provider.getDocuments(queryFor(1));
|
|
1130
|
-
setDocuments(result.data);
|
|
1131
|
-
setTotal(result.total);
|
|
1132
|
-
setPage(1);
|
|
1133
|
-
} finally {
|
|
1134
|
-
setLoading(false);
|
|
1135
|
-
}
|
|
1136
|
-
}, [provider, queryFor]);
|
|
1137
|
-
const fetchCounts = useCallback(async () => {
|
|
1138
|
-
try {
|
|
1139
|
-
setCounts(await provider.getDocumentCounts({ personId, kinds }));
|
|
1140
|
-
} catch {
|
|
1141
|
-
setCounts({});
|
|
1142
|
-
}
|
|
1143
|
-
}, [provider, personId, kinds]);
|
|
1144
|
-
const loadMore = useCallback(async () => {
|
|
1145
|
-
setLoadingMore(true);
|
|
1146
|
-
try {
|
|
1147
|
-
const next = page + 1;
|
|
1148
|
-
const result = await provider.getDocuments(queryFor(next));
|
|
1149
|
-
setDocuments((prev) => {
|
|
1150
|
-
const seen = new Set(prev.map((d) => d.id));
|
|
1151
|
-
return [...prev, ...result.data.filter((d) => !seen.has(d.id))];
|
|
1152
|
-
});
|
|
1153
|
-
setTotal(result.total);
|
|
1154
|
-
setPage(next);
|
|
1155
|
-
} finally {
|
|
1156
|
-
setLoadingMore(false);
|
|
1157
|
-
}
|
|
1158
|
-
}, [provider, queryFor, page]);
|
|
1159
|
-
useEffect(() => {
|
|
1160
|
-
const timer = setTimeout(() => fetchDocs(), 50);
|
|
1161
|
-
return () => clearTimeout(timer);
|
|
1162
|
-
}, [fetchDocs]);
|
|
1163
|
-
useEffect(() => {
|
|
1164
|
-
void fetchCounts();
|
|
1165
|
-
}, [fetchCounts]);
|
|
1166
|
-
useEffect(() => {
|
|
1167
|
-
const unsub = store.subscribe(() => {
|
|
1168
|
-
void fetchDocs();
|
|
1169
|
-
void fetchCounts();
|
|
1170
|
-
});
|
|
1171
|
-
return unsub;
|
|
1172
|
-
}, [store, fetchDocs, fetchCounts]);
|
|
1173
|
-
const [archiving, setArchiving] = useState(null);
|
|
1174
|
-
const handleArchive = useCallback(async () => {
|
|
1175
|
-
const doc = archiving;
|
|
1176
|
-
setArchiving(null);
|
|
1177
|
-
if (!doc) return;
|
|
1178
|
-
await provider.updateDocument(doc.id, { status: "archived" });
|
|
1179
|
-
setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
|
|
1180
|
-
setTotal((n) => Math.max(0, n - 1));
|
|
1181
|
-
void fetchCounts();
|
|
1182
|
-
invalidateTabCounts();
|
|
1183
|
-
}, [provider, archiving, fetchCounts]);
|
|
1184
|
-
const handleDragEnter = useCallback((e) => {
|
|
1185
|
-
e.preventDefault();
|
|
1186
|
-
dragCounter.current++;
|
|
1187
|
-
setIsDragOver(true);
|
|
1188
|
-
}, []);
|
|
1189
|
-
const handleDragLeave = useCallback((e) => {
|
|
1190
|
-
e.preventDefault();
|
|
1191
|
-
dragCounter.current--;
|
|
1192
|
-
if (dragCounter.current === 0) setIsDragOver(false);
|
|
1193
|
-
}, []);
|
|
1194
|
-
const handleDragOver = useCallback((e) => {
|
|
1195
|
-
e.preventDefault();
|
|
1196
|
-
}, []);
|
|
1197
|
-
const handleDrop = useCallback((e) => {
|
|
1198
|
-
e.preventDefault();
|
|
1199
|
-
dragCounter.current = 0;
|
|
1200
|
-
setIsDragOver(false);
|
|
1201
|
-
const files = Array.from(e.dataTransfer.files);
|
|
1202
|
-
if (files.length > 0 && onFileDrop) {
|
|
1203
|
-
onFileDrop(files);
|
|
1204
|
-
}
|
|
1205
|
-
}, [onFileDrop]);
|
|
1206
|
-
const buckets = useMemo(() => {
|
|
1207
|
-
const order = [...CATEGORY_ORDER, "file"];
|
|
1208
|
-
return order.filter((b) => (counts[b] ?? 0) > 0).map((b) => ({
|
|
1209
|
-
id: b,
|
|
1210
|
-
label: b === "file" ? t("customForms.bucket.file") : categoryLabel(b),
|
|
1211
|
-
count: counts[b] ?? 0
|
|
1212
|
-
}));
|
|
1213
|
-
}, [counts, t]);
|
|
1214
|
-
const totalAll = useMemo(
|
|
1215
|
-
() => Object.values(counts).reduce((sum, n) => sum + n, 0),
|
|
1216
|
-
[counts]
|
|
1217
|
-
);
|
|
1218
|
-
useEffect(() => {
|
|
1219
|
-
if (bucket && !buckets.some((b) => b.id === bucket)) setBucket(void 0);
|
|
1220
|
-
}, [buckets, bucket]);
|
|
1221
|
-
const visible = documents;
|
|
1222
|
-
const items = useMemo(
|
|
1223
|
-
() => visible.map((doc) => documentToTimelineItem(doc, t("customForms.documentFallbackTitle"))),
|
|
1224
|
-
[visible, t]
|
|
1225
|
-
);
|
|
1226
|
-
const byId = useMemo(() => new Map(visible.map((d) => [d.id, d])), [visible]);
|
|
1227
|
-
const expandedIds = useMemo(() => {
|
|
1228
|
-
if (!expandAll) return openIds;
|
|
1229
|
-
const open = new Set(visible.map((d) => d.id));
|
|
1230
|
-
for (const id of openIds) open.delete(id);
|
|
1231
|
-
return open;
|
|
1232
|
-
}, [expandAll, openIds, visible]);
|
|
1233
|
-
const toggleOne = useCallback((id) => {
|
|
1234
|
-
setOpenIds((prev) => {
|
|
1235
|
-
const next = new Set(prev);
|
|
1236
|
-
if (next.has(id)) next.delete(id);
|
|
1237
|
-
else next.add(id);
|
|
1238
|
-
return next;
|
|
1239
|
-
});
|
|
1240
|
-
}, []);
|
|
1241
|
-
useEffect(() => {
|
|
1242
|
-
setOpenIds(/* @__PURE__ */ new Set());
|
|
1243
|
-
}, [expandAll]);
|
|
1244
|
-
if (loading) {
|
|
1245
|
-
return /* @__PURE__ */ jsx("div", { className: "space-y-4 py-4", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1246
|
-
/* @__PURE__ */ jsx("div", { className: "h-9 w-9 rounded-lg bg-muted animate-pulse shrink-0" }),
|
|
1247
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1.5", children: [
|
|
1248
|
-
/* @__PURE__ */ jsx("div", { className: "h-3.5 w-2/3 bg-muted animate-pulse rounded" }),
|
|
1249
|
-
/* @__PURE__ */ jsx("div", { className: "h-2.5 w-1/3 bg-muted animate-pulse rounded" })
|
|
1250
|
-
] })
|
|
1251
|
-
] }, i)) });
|
|
1252
|
-
}
|
|
1253
|
-
const filters = (buckets.length > 1 || totalAll > 0) && /* @__PURE__ */ jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-1.5", children: [
|
|
1254
|
-
buckets.length > 1 && [{ id: void 0, label: t("customForms.bucket.all"), count: totalAll }, ...buckets].map((chip) => {
|
|
1255
|
-
const active = chip.id === bucket;
|
|
1256
|
-
return /* @__PURE__ */ jsxs(
|
|
1257
|
-
Button,
|
|
1258
|
-
{
|
|
1259
|
-
variant: "bare",
|
|
1260
|
-
size: "bare",
|
|
1261
|
-
type: "button",
|
|
1262
|
-
onClick: () => setBucket(chip.id),
|
|
1263
|
-
className: `gap-0 font-normal rounded-full border px-2.5 py-1 text-xs transition-colors ${active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
1264
|
-
children: [
|
|
1265
|
-
chip.label,
|
|
1266
|
-
/* @__PURE__ */ jsx("span", { className: "ml-1.5 tabular-nums opacity-60", children: chip.count })
|
|
1267
|
-
]
|
|
1268
|
-
},
|
|
1269
|
-
chip.id ?? "__all__"
|
|
1270
|
-
);
|
|
1271
|
-
}),
|
|
1272
|
-
/* @__PURE__ */ jsxs(
|
|
1273
|
-
Button,
|
|
1274
|
-
{
|
|
1275
|
-
variant: "bare",
|
|
1276
|
-
size: "bare",
|
|
1277
|
-
type: "button",
|
|
1278
|
-
onClick: toggleExpandAll,
|
|
1279
|
-
"aria-pressed": expandAll,
|
|
1280
|
-
title: t(expandAll ? "customForms.hideContent" : "customForms.showContent"),
|
|
1281
|
-
className: `font-normal ml-auto flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors ${expandAll ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
1282
|
-
children: [
|
|
1283
|
-
expandAll ? /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" }),
|
|
1284
|
-
t(expandAll ? "customForms.hideContent" : "customForms.showContent")
|
|
1285
|
-
]
|
|
1286
|
-
}
|
|
1287
|
-
)
|
|
1288
|
-
] });
|
|
1289
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1290
|
-
filters,
|
|
1291
|
-
/* @__PURE__ */ jsxs(
|
|
1292
|
-
"div",
|
|
1293
|
-
{
|
|
1294
|
-
className: `relative min-h-[200px] rounded-xl border-2 transition-colors ${isDragOver ? "border-dashed border-primary bg-primary/5" : documents.length === 0 && acceptsFiles ? "border-dashed border-border" : "border-transparent"}`,
|
|
1295
|
-
...acceptsFiles ? {
|
|
1296
|
-
onDragEnter: handleDragEnter,
|
|
1297
|
-
onDragLeave: handleDragLeave,
|
|
1298
|
-
onDragOver: handleDragOver,
|
|
1299
|
-
onDrop: handleDrop
|
|
1300
|
-
} : {},
|
|
1301
|
-
children: [
|
|
1302
|
-
isDragOver && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 z-10 flex flex-col items-center justify-center rounded-xl bg-primary/5", children: [
|
|
1303
|
-
/* @__PURE__ */ jsx(Upload, { className: "h-8 w-8 text-primary/50 mb-2" }),
|
|
1304
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-primary/70", children: t("customForms.dropFilesHere") })
|
|
1305
|
-
] }),
|
|
1306
|
-
documents.length > 0 && visible.length === 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("customForms.noDocumentsInFilter") }) }),
|
|
1307
|
-
documents.length === 0 && !isDragOver && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [
|
|
1308
|
-
/* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-3", children: acceptsFiles ? /* @__PURE__ */ jsx(Paperclip, { className: "h-5 w-5 text-muted-foreground" }) : /* @__PURE__ */ jsx(ClipboardList, { className: "h-5 w-5 text-muted-foreground" }) }),
|
|
1309
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t(acceptsFiles ? "customForms.noFiles" : "customForms.noRecords") }),
|
|
1310
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1 max-w-xs", children: t(acceptsFiles ? "customForms.noFilesDescription" : "customForms.noRecordsDescription") }),
|
|
1311
|
-
acceptsFiles && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground mt-3 flex items-center gap-1", children: [
|
|
1312
|
-
/* @__PURE__ */ jsx(Upload, { className: "h-3 w-3" }),
|
|
1313
|
-
t("customForms.dragFilesHere")
|
|
1314
|
-
] })
|
|
1315
|
-
] }),
|
|
1316
|
-
visible.length > 0 && /* @__PURE__ */ jsx(
|
|
1317
|
-
TimelineList,
|
|
1318
|
-
{
|
|
1319
|
-
items,
|
|
1320
|
-
expandedIds,
|
|
1321
|
-
renderPreview: (it) => {
|
|
1322
|
-
const doc = byId.get(it.id);
|
|
1323
|
-
return doc ? /* @__PURE__ */ jsx(DocumentPreview, { document: doc, provider, onOpen: () => onView(doc) }) : null;
|
|
1324
|
-
},
|
|
1325
|
-
onSelect: (it) => {
|
|
1326
|
-
const doc = byId.get(it.id);
|
|
1327
|
-
if (doc) onView(doc);
|
|
1328
|
-
},
|
|
1329
|
-
renderActions: (it) => {
|
|
1330
|
-
const doc = byId.get(it.id);
|
|
1331
|
-
if (!doc) return null;
|
|
1332
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1333
|
-
/* @__PURE__ */ jsx(
|
|
1334
|
-
Button,
|
|
1335
|
-
{
|
|
1336
|
-
variant: "bare",
|
|
1337
|
-
size: "bare",
|
|
1338
|
-
type: "button",
|
|
1339
|
-
onClick: (e) => {
|
|
1340
|
-
e.stopPropagation();
|
|
1341
|
-
toggleOne(doc.id);
|
|
1342
|
-
},
|
|
1343
|
-
className: "rounded-md p-1.5 hover:bg-muted",
|
|
1344
|
-
"aria-label": t(expandedIds.has(doc.id) ? "customForms.hideContent" : "customForms.showContent"),
|
|
1345
|
-
"aria-expanded": expandedIds.has(doc.id),
|
|
1346
|
-
children: expandedIds.has(doc.id) ? /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5 text-muted-foreground" }) : /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
1347
|
-
}
|
|
1348
|
-
),
|
|
1349
|
-
/* @__PURE__ */ jsx(
|
|
1350
|
-
Button,
|
|
1351
|
-
{
|
|
1352
|
-
variant: "bare",
|
|
1353
|
-
size: "bare",
|
|
1354
|
-
type: "button",
|
|
1355
|
-
onClick: (e) => {
|
|
1356
|
-
e.stopPropagation();
|
|
1357
|
-
setArchiving(doc);
|
|
1358
|
-
},
|
|
1359
|
-
className: "rounded-md p-1.5 hover:bg-muted",
|
|
1360
|
-
"aria-label": t("customForms.archive"),
|
|
1361
|
-
title: t("customForms.archive"),
|
|
1362
|
-
children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
1363
|
-
}
|
|
1364
|
-
)
|
|
1365
|
-
] });
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
),
|
|
1369
|
-
visible.length < total && /* @__PURE__ */ jsx("div", { className: "flex justify-center pt-4", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: loadingMore, onClick: () => void loadMore(), children: loadingMore ? t("customForms.loadingMore") : t("customForms.loadMore", { count: String(total - visible.length) }) }) })
|
|
1370
|
-
]
|
|
1371
|
-
}
|
|
1372
|
-
),
|
|
1373
|
-
/* @__PURE__ */ jsx(
|
|
1374
|
-
ConfirmDialog,
|
|
1375
|
-
{
|
|
1376
|
-
open: archiving !== null,
|
|
1377
|
-
title: t("customForms.archiveTitle"),
|
|
1378
|
-
description: t("customForms.archiveDescription", {
|
|
1379
|
-
name: archiving?.templateName ?? archiving?.title ?? t("customForms.documentFallbackTitle")
|
|
1380
|
-
}),
|
|
1381
|
-
confirmLabel: t("customForms.archive"),
|
|
1382
|
-
cancelLabel: t("common.cancel"),
|
|
1383
|
-
onConfirm: () => void handleArchive(),
|
|
1384
|
-
onCancel: () => setArchiving(null)
|
|
1385
|
-
}
|
|
1386
|
-
)
|
|
1387
|
-
] });
|
|
1388
|
-
}
|
|
1389
|
-
function DocumentBodySkeleton({ rows = 5 }) {
|
|
1390
|
-
return /* @__PURE__ */ jsx("div", { className: "space-y-4 rounded-xl border p-4", "aria-hidden": true, children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
1391
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-28 animate-pulse rounded bg-muted" }),
|
|
1392
|
-
/* @__PURE__ */ jsx("div", { className: "h-9 w-full animate-pulse rounded-md bg-muted" })
|
|
1393
|
-
] }, i)) });
|
|
1394
|
-
}
|
|
1395
|
-
function DocumentHeaderSkeleton() {
|
|
1396
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", "aria-hidden": true, children: [
|
|
1397
|
-
/* @__PURE__ */ jsx("div", { className: "h-3.5 w-3.5 shrink-0 animate-pulse rounded bg-muted" }),
|
|
1398
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
|
|
1399
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 w-48 max-w-full animate-pulse rounded bg-muted" }),
|
|
1400
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-24 animate-pulse rounded bg-muted" })
|
|
1401
|
-
] })
|
|
1402
|
-
] });
|
|
1403
|
-
}
|
|
1404
|
-
function generateTitle(templateName, personName) {
|
|
1405
|
-
const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
1406
|
-
return personName ? `${templateName} \u2014 ${personName} \u2014 ${date}` : `${templateName} \u2014 ${date}`;
|
|
1407
|
-
}
|
|
1408
|
-
function DocumentFormDialog({
|
|
1409
|
-
template: initialTemplate,
|
|
1410
|
-
existingDocument,
|
|
1411
|
-
personId,
|
|
1412
|
-
personName,
|
|
1413
|
-
person,
|
|
1414
|
-
provider,
|
|
1415
|
-
store,
|
|
1416
|
-
onSaved,
|
|
1417
|
-
onBack
|
|
1418
|
-
}) {
|
|
1419
|
-
const t = useTranslation();
|
|
1420
|
-
const guardDocuments = useLimitGuard("documents_month");
|
|
1421
|
-
const [template, setTemplate] = useState(initialTemplate);
|
|
1422
|
-
const [data, setData] = useState(
|
|
1423
|
-
() => existingDocument ? existingDocument.data : prefillFromPerson(initialTemplate?.schema, person)
|
|
1424
|
-
);
|
|
1425
|
-
const [saving, setSaving] = useState(false);
|
|
1426
|
-
useEffect(() => {
|
|
1427
|
-
if (existingDocument && !template) {
|
|
1428
|
-
provider.getTemplateById(existingDocument.templateId).then(setTemplate);
|
|
1429
|
-
}
|
|
1430
|
-
}, [existingDocument, template, provider]);
|
|
1431
|
-
const prefilled = React13.useRef(!!initialTemplate || !!existingDocument);
|
|
1432
|
-
useEffect(() => {
|
|
1433
|
-
if (prefilled.current || !template || existingDocument) return;
|
|
1434
|
-
prefilled.current = true;
|
|
1435
|
-
setData((prev) => prefillFromPerson(template.schema, person, prev));
|
|
1436
|
-
}, [template, existingDocument, person]);
|
|
1437
|
-
const handleSave = useCallback(
|
|
1438
|
-
async (status) => {
|
|
1439
|
-
if (!template) return;
|
|
1440
|
-
if (!existingDocument && await guardDocuments() === "blocked") return;
|
|
1441
|
-
setSaving(true);
|
|
1442
|
-
try {
|
|
1443
|
-
const autoTitle = generateTitle(template.name, personName);
|
|
1444
|
-
if (existingDocument) {
|
|
1445
|
-
await store.getState().updateDocument(existingDocument.id, { data, status });
|
|
1446
|
-
} else {
|
|
1447
|
-
await store.getState().createDocument({
|
|
1448
|
-
templateId: template.id,
|
|
1449
|
-
personId,
|
|
1450
|
-
title: autoTitle,
|
|
1451
|
-
data,
|
|
1452
|
-
status
|
|
1453
|
-
});
|
|
1454
|
-
invalidateLimit("documents_month");
|
|
1455
|
-
}
|
|
1456
|
-
onSaved();
|
|
1457
|
-
} finally {
|
|
1458
|
-
setSaving(false);
|
|
1459
|
-
}
|
|
1460
|
-
},
|
|
1461
|
-
[template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
|
|
1462
|
-
);
|
|
1463
|
-
const snapshot = React13.useRef(null);
|
|
1464
|
-
useEffect(() => {
|
|
1465
|
-
if (template && snapshot.current === null) {
|
|
1466
|
-
snapshot.current = JSON.stringify(existingDocument?.data ?? {});
|
|
1467
|
-
}
|
|
1468
|
-
}, [template, existingDocument]);
|
|
1469
|
-
const dirty = snapshot.current !== null && JSON.stringify(data) !== snapshot.current;
|
|
1470
|
-
useSaveBar({
|
|
1471
|
-
dirty,
|
|
1472
|
-
saving,
|
|
1473
|
-
onSave: () => {
|
|
1474
|
-
void handleSave("completed");
|
|
1475
|
-
},
|
|
1476
|
-
onDiscard: () => onBack(),
|
|
1477
|
-
saveLabel: t("customForms.saveAndComplete")
|
|
1478
|
-
});
|
|
1479
|
-
if (!template) {
|
|
1480
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1481
|
-
existingDocument?.title ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1482
|
-
/* @__PURE__ */ jsx(
|
|
1483
|
-
Button,
|
|
1484
|
-
{
|
|
1485
|
-
variant: "bare",
|
|
1486
|
-
size: "bare",
|
|
1487
|
-
onClick: onBack,
|
|
1488
|
-
className: "flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground",
|
|
1489
|
-
children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
|
|
1490
|
-
}
|
|
1491
|
-
),
|
|
1492
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1493
|
-
/* @__PURE__ */ jsx("h3", { className: "truncate text-sm font-semibold", children: existingDocument.title }),
|
|
1494
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: new Date(existingDocument.createdAt).toLocaleDateString() })
|
|
1495
|
-
] })
|
|
1496
|
-
] }) : /* @__PURE__ */ jsx(DocumentHeaderSkeleton, {}),
|
|
1497
|
-
/* @__PURE__ */ jsx(
|
|
1498
|
-
DocumentBodySkeleton,
|
|
1499
|
-
{
|
|
1500
|
-
rows: Math.min(Math.max(Object.keys(existingDocument?.data ?? {}).length, 3), 8)
|
|
1501
|
-
}
|
|
1502
|
-
)
|
|
1503
|
-
] });
|
|
1504
|
-
}
|
|
1505
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1506
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1507
|
-
/* @__PURE__ */ jsx(
|
|
1508
|
-
Button,
|
|
1509
|
-
{
|
|
1510
|
-
variant: "bare",
|
|
1511
|
-
size: "bare",
|
|
1512
|
-
onClick: onBack,
|
|
1513
|
-
className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
1514
|
-
children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
|
|
1515
|
-
}
|
|
1516
|
-
),
|
|
1517
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
1518
|
-
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm", children: template.name }),
|
|
1519
|
-
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
1520
|
-
personName && /* @__PURE__ */ jsxs("span", { children: [
|
|
1521
|
-
personName,
|
|
1522
|
-
" \xB7 "
|
|
1523
|
-
] }),
|
|
1524
|
-
(/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1525
|
-
] })
|
|
1526
|
-
] })
|
|
1527
|
-
] }),
|
|
1528
|
-
/* @__PURE__ */ jsx("div", { className: "rounded-xl border p-4", children: /* @__PURE__ */ jsx(
|
|
1529
|
-
FormRenderer,
|
|
1530
|
-
{
|
|
1531
|
-
schema: template.schema,
|
|
1532
|
-
data,
|
|
1533
|
-
onChange: setData
|
|
1534
|
-
}
|
|
1535
|
-
) })
|
|
1536
|
-
] });
|
|
1537
|
-
}
|
|
1538
|
-
var STATUS_COLORS = {
|
|
1539
|
-
draft: "bg-warning-soft text-warning-soft-foreground",
|
|
1540
|
-
completed: "bg-success-soft text-success-soft-foreground",
|
|
1541
|
-
signed: "bg-info-soft text-info-soft-foreground",
|
|
1542
|
-
archived: "bg-muted text-muted-foreground"
|
|
1543
|
-
};
|
|
1544
|
-
function FormViewer({ document: doc, provider, onBack, onEdit }) {
|
|
1545
|
-
const t = useTranslation();
|
|
1546
|
-
const [template, setTemplate] = useState(null);
|
|
1547
|
-
useEffect(() => {
|
|
1548
|
-
if (!doc.templateId) return;
|
|
1549
|
-
const timer = setTimeout(() => {
|
|
1550
|
-
provider.getTemplateById(doc.templateId).then(setTemplate);
|
|
1551
|
-
}, 50);
|
|
1552
|
-
return () => clearTimeout(timer);
|
|
1553
|
-
}, [doc.templateId, provider]);
|
|
1554
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1555
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1556
|
-
/* @__PURE__ */ jsx(
|
|
1557
|
-
Button,
|
|
1558
|
-
{
|
|
1559
|
-
variant: "bare",
|
|
1560
|
-
size: "bare",
|
|
1561
|
-
onClick: onBack,
|
|
1562
|
-
className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
1563
|
-
children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
|
|
1564
|
-
}
|
|
1565
|
-
),
|
|
1566
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
1567
|
-
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm truncate", children: doc.title ?? template?.name ?? "" }),
|
|
1568
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: new Date(doc.createdAt).toLocaleDateString() })
|
|
1569
|
-
] }),
|
|
1570
|
-
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${STATUS_COLORS[doc.status] ?? ""}`, children: t(`customForms.status.${doc.status}`) }),
|
|
1571
|
-
/* @__PURE__ */ jsxs(
|
|
1572
|
-
Button,
|
|
1573
|
-
{
|
|
1574
|
-
variant: "outline",
|
|
1575
|
-
size: "sm",
|
|
1576
|
-
className: "h-8",
|
|
1577
|
-
disabled: !template,
|
|
1578
|
-
onClick: () => template && printDocument(template, doc),
|
|
1579
|
-
children: [
|
|
1580
|
-
/* @__PURE__ */ jsx(Printer, { className: "h-3 w-3 mr-1" }),
|
|
1581
|
-
t("customForms.printPdf")
|
|
1582
|
-
]
|
|
1583
|
-
}
|
|
1584
|
-
),
|
|
1585
|
-
onEdit && doc.status === "draft" && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => onEdit(doc), children: [
|
|
1586
|
-
/* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 mr-1" }),
|
|
1587
|
-
t("common.edit")
|
|
1588
|
-
] })
|
|
1589
|
-
] }),
|
|
1590
|
-
template ? /* @__PURE__ */ jsx("div", { className: "rounded-xl border p-4", children: /* @__PURE__ */ jsx(
|
|
1591
|
-
FormRenderer,
|
|
1592
|
-
{
|
|
1593
|
-
schema: template.schema,
|
|
1594
|
-
data: doc.data,
|
|
1595
|
-
onChange: () => {
|
|
1596
|
-
},
|
|
1597
|
-
readOnly: true
|
|
1598
|
-
}
|
|
1599
|
-
) }) : (
|
|
1600
|
-
// O documento já diz quantos campos tem: o esqueleto sai da altura certa
|
|
1601
|
-
// em vez de chutar, e a chegada do conteúdo não mexe no scroll.
|
|
1602
|
-
/* @__PURE__ */ jsx(DocumentBodySkeleton, { rows: Math.min(Math.max(Object.keys(doc.data ?? {}).length, 3), 8) })
|
|
1603
|
-
)
|
|
1604
|
-
] });
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
// src/lib/document-types.ts
|
|
1608
|
-
var providers = [];
|
|
1609
|
-
function registerDocumentTypeProvider(provider) {
|
|
1610
|
-
if (providers.find((p) => p.id === provider.id)) return;
|
|
1611
|
-
providers.push(provider);
|
|
1612
|
-
}
|
|
1613
|
-
async function getAllDocumentTypes(personId) {
|
|
1614
|
-
const results = await Promise.all(
|
|
1615
|
-
providers.map((p) => Promise.resolve(p.getTypes(personId)))
|
|
1616
|
-
);
|
|
1617
|
-
const all = results.flat();
|
|
1618
|
-
all.sort((a, b) => (a.groupOrder ?? 50) - (b.groupOrder ?? 50) || (a.order ?? 0) - (b.order ?? 0));
|
|
1619
|
-
return all;
|
|
1620
|
-
}
|
|
1621
|
-
var SEARCH_THRESHOLD = 8;
|
|
1622
|
-
var normalize = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
1623
|
-
function AddDocumentDropdown({ personId, onSelect, optionFilter, label }) {
|
|
1624
|
-
const t = useTranslation();
|
|
1625
|
-
const [open, setOpen] = useState(false);
|
|
1626
|
-
const [options, setOptions] = useState([]);
|
|
1627
|
-
const [loaded, setLoaded] = useState(false);
|
|
1628
|
-
const [query, setQuery] = useState("");
|
|
1629
|
-
const [activeIndex, setActiveIndex] = useState(0);
|
|
1630
|
-
const loadingRef = useRef(false);
|
|
1631
|
-
const inputRef = useRef(null);
|
|
1632
|
-
const listRef = useRef(null);
|
|
1633
|
-
const handleOpenChange = useCallback((next) => {
|
|
1634
|
-
setOpen(next);
|
|
1635
|
-
if (!next) return;
|
|
1636
|
-
setQuery("");
|
|
1637
|
-
setActiveIndex(0);
|
|
1638
|
-
if (loaded || loadingRef.current) return;
|
|
1639
|
-
loadingRef.current = true;
|
|
1640
|
-
getAllDocumentTypes(personId).then((opts) => {
|
|
1641
|
-
setOptions(optionFilter ? opts.filter(optionFilter) : opts);
|
|
1642
|
-
setLoaded(true);
|
|
1643
|
-
loadingRef.current = false;
|
|
1644
|
-
});
|
|
1645
|
-
}, [personId, loaded, optionFilter]);
|
|
1646
|
-
const filtered = useMemo(() => {
|
|
1647
|
-
const q = normalize(query.trim());
|
|
1648
|
-
if (!q) return options;
|
|
1649
|
-
return options.filter(
|
|
1650
|
-
(o) => normalize(o.label).includes(q) || (o.description ? normalize(o.description).includes(q) : false)
|
|
1651
|
-
);
|
|
1652
|
-
}, [options, query]);
|
|
1653
|
-
const groups = useMemo(() => {
|
|
1654
|
-
const acc = [];
|
|
1655
|
-
for (const opt of filtered) {
|
|
1656
|
-
let group = acc.find((g) => g.label === opt.group);
|
|
1657
|
-
if (!group) {
|
|
1658
|
-
group = { label: opt.group, items: [] };
|
|
1659
|
-
acc.push(group);
|
|
1660
|
-
}
|
|
1661
|
-
group.items.push(opt);
|
|
1662
|
-
}
|
|
1663
|
-
return acc;
|
|
1664
|
-
}, [filtered]);
|
|
1665
|
-
const showSearch = loaded && options.length > SEARCH_THRESHOLD;
|
|
1666
|
-
useEffect(() => {
|
|
1667
|
-
if (!open || !showSearch) return;
|
|
1668
|
-
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
1669
|
-
return () => cancelAnimationFrame(id);
|
|
1670
|
-
}, [open, showSearch]);
|
|
1671
|
-
useEffect(() => {
|
|
1672
|
-
listRef.current?.querySelector(`[data-index="${activeIndex}"]`)?.scrollIntoView({ block: "nearest" });
|
|
1673
|
-
}, [activeIndex]);
|
|
1674
|
-
const choose = useCallback((opt) => {
|
|
1675
|
-
setOpen(false);
|
|
1676
|
-
onSelect(opt);
|
|
1677
|
-
}, [onSelect]);
|
|
1678
|
-
const handleInputKeyDown = (e) => {
|
|
1679
|
-
if (e.key === "Escape") return;
|
|
1680
|
-
if (e.key === "ArrowDown") {
|
|
1681
|
-
e.preventDefault();
|
|
1682
|
-
setActiveIndex((i) => Math.min(filtered.length - 1, i + 1));
|
|
1683
|
-
} else if (e.key === "ArrowUp") {
|
|
1684
|
-
e.preventDefault();
|
|
1685
|
-
setActiveIndex((i) => Math.max(0, i - 1));
|
|
1686
|
-
} else if (e.key === "Enter") {
|
|
1687
|
-
e.preventDefault();
|
|
1688
|
-
const opt = filtered[activeIndex];
|
|
1689
|
-
if (opt) choose(opt);
|
|
1690
|
-
}
|
|
1691
|
-
e.stopPropagation();
|
|
1692
|
-
};
|
|
1693
|
-
let flatIndex = -1;
|
|
1694
|
-
return /* @__PURE__ */ jsxs(Dropdown, { open, onOpenChange: handleOpenChange, children: [
|
|
1695
|
-
/* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "sm", className: "h-8", children: [
|
|
1696
|
-
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1" }),
|
|
1697
|
-
label ?? t("customForms.addDocument"),
|
|
1698
|
-
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 ml-1 opacity-50" })
|
|
1699
|
-
] }) }),
|
|
1700
|
-
/* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-72 p-0", children: [
|
|
1701
|
-
showSearch && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-2.5 py-2", children: [
|
|
1702
|
-
/* @__PURE__ */ jsx(Search, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }),
|
|
1703
|
-
/* @__PURE__ */ jsx(
|
|
1704
|
-
Input,
|
|
1705
|
-
{
|
|
1706
|
-
ref: inputRef,
|
|
1707
|
-
value: query,
|
|
1708
|
-
onChange: (e) => {
|
|
1709
|
-
setQuery(e.target.value);
|
|
1710
|
-
setActiveIndex(0);
|
|
1711
|
-
},
|
|
1712
|
-
onKeyDown: handleInputKeyDown,
|
|
1713
|
-
placeholder: t("customForms.searchDocumentTypes"),
|
|
1714
|
-
className: "h-auto rounded-none border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
1715
|
-
}
|
|
1716
|
-
)
|
|
1717
|
-
] }),
|
|
1718
|
-
/* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-[min(60vh,20rem)] overflow-y-auto overscroll-contain p-1", children: [
|
|
1719
|
-
groups.map((group, gi) => /* @__PURE__ */ jsxs(React13.Fragment, { children: [
|
|
1720
|
-
gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
1721
|
-
/* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
|
|
1722
|
-
group.items.map((opt) => {
|
|
1723
|
-
const Icon = opt.icon;
|
|
1724
|
-
flatIndex += 1;
|
|
1725
|
-
const index = flatIndex;
|
|
1726
|
-
return /* @__PURE__ */ jsxs(
|
|
1727
|
-
Button,
|
|
1728
|
-
{
|
|
1729
|
-
variant: "bare",
|
|
1730
|
-
size: "bare",
|
|
1731
|
-
type: "button",
|
|
1732
|
-
"data-index": index,
|
|
1733
|
-
onClick: () => choose(opt),
|
|
1734
|
-
onMouseEnter: () => setActiveIndex(index),
|
|
1735
|
-
className: `font-normal justify-start 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" : ""}`,
|
|
1736
|
-
children: [
|
|
1737
|
-
/* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1738
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
1739
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm truncate", children: opt.label }),
|
|
1740
|
-
opt.description && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground truncate", children: opt.description })
|
|
1741
|
-
] })
|
|
1742
|
-
]
|
|
1743
|
-
},
|
|
1744
|
-
opt.id
|
|
1745
|
-
);
|
|
1746
|
-
})
|
|
1747
|
-
] }, group.label)),
|
|
1748
|
-
!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" }) }),
|
|
1749
|
-
loaded && options.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: t("customForms.noTemplates") }),
|
|
1750
|
-
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") })
|
|
1751
|
-
] })
|
|
1752
|
-
] })
|
|
1753
|
-
] });
|
|
1754
|
-
}
|
|
1755
|
-
var SURFACE_KINDS = {
|
|
1756
|
-
records: ["form"],
|
|
1757
|
-
files: ["image", "attachment"]
|
|
1758
|
-
};
|
|
1759
|
-
function PersonDocumentsWidget({ item, config, provider, store, surface = "records" }) {
|
|
1760
|
-
const t = useTranslation();
|
|
1761
|
-
const kinds = SURFACE_KINDS[surface];
|
|
1762
|
-
const optionFilter = useCallback(
|
|
1763
|
-
(o) => surface === "records" ? o.id.startsWith("template:") : o.id === "core:image" || o.id === "core:attachment",
|
|
1764
|
-
[surface]
|
|
1765
|
-
);
|
|
1766
|
-
const guardDocuments = useLimitGuard("documents_month");
|
|
1767
|
-
const [view, setView] = useState({ type: "list" });
|
|
1768
|
-
const fileInputRef = useRef(null);
|
|
1769
|
-
const [pendingFileAccept, setPendingFileAccept] = useState("*/*");
|
|
1770
|
-
const handleDocTypeSelect = useCallback(async (option) => {
|
|
1771
|
-
if (option.id.startsWith("template:")) {
|
|
1772
|
-
const templateId = option.id.replace("template:", "");
|
|
1773
|
-
const template = await provider.getTemplateById(templateId);
|
|
1774
|
-
if (template) {
|
|
1775
|
-
setView({ type: "fill", template });
|
|
1776
|
-
}
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
if (option.id === "core:image") {
|
|
1780
|
-
setPendingFileAccept("image/*");
|
|
1781
|
-
setTimeout(() => fileInputRef.current?.click(), 0);
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
if (option.id === "core:attachment") {
|
|
1785
|
-
setPendingFileAccept("*/*");
|
|
1786
|
-
setTimeout(() => fileInputRef.current?.click(), 0);
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
}, [provider]);
|
|
1790
|
-
const handleFiles = useCallback(async (files) => {
|
|
1791
|
-
if (await guardDocuments(files.length) === "blocked") return;
|
|
1792
|
-
for (const file of files) {
|
|
1793
|
-
const isImage = file.type.startsWith("image/");
|
|
1794
|
-
await store.getState().createDocument({
|
|
1795
|
-
kind: isImage ? "image" : "attachment",
|
|
1796
|
-
personId: item.id,
|
|
1797
|
-
title: file.name,
|
|
1798
|
-
fileName: file.name,
|
|
1799
|
-
fileSize: file.size,
|
|
1800
|
-
mimeType: file.type,
|
|
1801
|
-
status: "completed"
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1804
|
-
invalidateLimit("documents_month");
|
|
1805
|
-
}, [store, item.id, guardDocuments]);
|
|
1806
|
-
const handleFileInputChange = useCallback((e) => {
|
|
1807
|
-
const files = Array.from(e.target.files ?? []);
|
|
1808
|
-
if (files.length > 0) handleFiles(files);
|
|
1809
|
-
e.target.value = "";
|
|
1810
|
-
}, [handleFiles]);
|
|
1811
|
-
const handleViewDoc = useCallback((doc) => {
|
|
1812
|
-
setView({ type: "view", document: doc });
|
|
1813
|
-
}, []);
|
|
1814
|
-
const handleEditDoc = useCallback((doc) => {
|
|
1815
|
-
setView({ type: "edit", document: doc });
|
|
1816
|
-
}, []);
|
|
1817
|
-
const handleBack = useCallback(() => {
|
|
1818
|
-
setView({ type: "list" });
|
|
1819
|
-
}, []);
|
|
1820
|
-
const handleSaved = useCallback(() => {
|
|
1821
|
-
setView({ type: "list" });
|
|
1822
|
-
}, []);
|
|
1823
|
-
if (view.type === "fill") {
|
|
1824
|
-
return /* @__PURE__ */ jsx(
|
|
1825
|
-
DocumentFormDialog,
|
|
1826
|
-
{
|
|
1827
|
-
template: view.template,
|
|
1828
|
-
personId: item.id,
|
|
1829
|
-
personName: item.name,
|
|
1830
|
-
person: item,
|
|
1831
|
-
provider,
|
|
1832
|
-
store,
|
|
1833
|
-
onSaved: handleSaved,
|
|
1834
|
-
onBack: handleBack
|
|
1835
|
-
}
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1838
|
-
if (view.type === "view") {
|
|
1839
|
-
const doc = view.document;
|
|
1840
|
-
const isForm = doc.kind === "form" || !doc.kind && doc.templateId;
|
|
1841
|
-
if (!isForm) {
|
|
1842
|
-
return /* @__PURE__ */ jsx(FileDocumentViewer, { document: doc, onBack: handleBack });
|
|
1843
|
-
}
|
|
1844
|
-
return /* @__PURE__ */ jsx(
|
|
1845
|
-
FormViewer,
|
|
1846
|
-
{
|
|
1847
|
-
document: doc,
|
|
1848
|
-
provider,
|
|
1849
|
-
onBack: handleBack,
|
|
1850
|
-
onEdit: handleEditDoc
|
|
1851
|
-
}
|
|
1852
|
-
);
|
|
1853
|
-
}
|
|
1854
|
-
if (view.type === "edit") {
|
|
1855
|
-
return /* @__PURE__ */ jsx(
|
|
1856
|
-
DocumentFormDialog,
|
|
1857
|
-
{
|
|
1858
|
-
template: null,
|
|
1859
|
-
existingDocument: view.document,
|
|
1860
|
-
personId: item.id,
|
|
1861
|
-
personName: item.name,
|
|
1862
|
-
person: item,
|
|
1863
|
-
provider,
|
|
1864
|
-
store,
|
|
1865
|
-
onSaved: handleSaved,
|
|
1866
|
-
onBack: handleBack
|
|
1867
|
-
}
|
|
1868
|
-
);
|
|
1869
|
-
}
|
|
1870
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1871
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1872
|
-
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t(surface === "files" ? "customForms.filesTitle" : "customForms.recordsTitle") }),
|
|
1873
|
-
/* @__PURE__ */ jsx(
|
|
1874
|
-
AddDocumentDropdown,
|
|
1875
|
-
{
|
|
1876
|
-
personId: item.id,
|
|
1877
|
-
onSelect: handleDocTypeSelect,
|
|
1878
|
-
optionFilter,
|
|
1879
|
-
label: t(surface === "files" ? "customForms.addFile" : "customForms.addRecord")
|
|
1880
|
-
}
|
|
1881
|
-
)
|
|
1882
|
-
] }),
|
|
1883
|
-
/* @__PURE__ */ jsx(
|
|
1884
|
-
DocumentList,
|
|
1885
|
-
{
|
|
1886
|
-
personId: item.id,
|
|
1887
|
-
provider,
|
|
1888
|
-
store,
|
|
1889
|
-
kinds,
|
|
1890
|
-
onView: handleViewDoc,
|
|
1891
|
-
onFileDrop: handleFiles
|
|
1892
|
-
}
|
|
1893
|
-
),
|
|
1894
|
-
/* @__PURE__ */ jsx(
|
|
1895
|
-
"input",
|
|
1896
|
-
{
|
|
1897
|
-
ref: fileInputRef,
|
|
1898
|
-
type: "file",
|
|
1899
|
-
className: "hidden",
|
|
1900
|
-
accept: pendingFileAccept,
|
|
1901
|
-
multiple: true,
|
|
1902
|
-
onChange: handleFileInputChange
|
|
1903
|
-
}
|
|
1904
|
-
)
|
|
1905
|
-
] });
|
|
1906
|
-
}
|
|
1907
|
-
function FileDocumentViewer({ document: doc, onBack }) {
|
|
1908
|
-
const isImage = doc.kind === "image" || doc.mimeType?.startsWith("image/");
|
|
1909
|
-
const Icon = isImage ? Image : Paperclip;
|
|
1910
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1911
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1912
|
-
/* @__PURE__ */ jsx(
|
|
1913
|
-
Button,
|
|
1914
|
-
{
|
|
1915
|
-
variant: "bare",
|
|
1916
|
-
size: "bare",
|
|
1917
|
-
onClick: onBack,
|
|
1918
|
-
className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
1919
|
-
children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
|
|
1920
|
-
}
|
|
1921
|
-
),
|
|
1922
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
1923
|
-
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm truncate", children: doc.title ?? doc.fileName ?? "Document" }),
|
|
1924
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: new Date(doc.createdAt).toLocaleDateString() })
|
|
1925
|
-
] }),
|
|
1926
|
-
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-[10px] bg-success/15 text-success", children: doc.status === "completed" ? "Finalizado" : doc.status })
|
|
1927
|
-
] }),
|
|
1928
|
-
/* @__PURE__ */ jsx("div", { className: "rounded-xl border p-6 flex flex-col items-center gap-4", children: isImage && doc.fileUrl ? /* @__PURE__ */ jsx("img", { src: doc.fileUrl, alt: doc.fileName ?? "", className: "max-w-full max-h-[500px] rounded-lg object-contain" }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3 py-8", children: [
|
|
1929
|
-
/* @__PURE__ */ jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(Icon, { className: "h-7 w-7 text-muted-foreground" }) }),
|
|
1930
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: doc.fileName ?? doc.title ?? "Arquivo" }),
|
|
1931
|
-
doc.fileSize && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
1932
|
-
(doc.fileSize / 1024).toFixed(1),
|
|
1933
|
-
" KB"
|
|
1934
|
-
] })
|
|
1935
|
-
] }) })
|
|
1936
|
-
] });
|
|
1937
|
-
}
|
|
1938
|
-
var RANK = { danger: 3, warning: 2, info: 1 };
|
|
1939
|
-
var TONE = {
|
|
1940
|
-
danger: { wrap: "border-destructive/40 bg-destructive/5", chip: "bg-destructive text-destructive-foreground", dot: "bg-destructive" },
|
|
1941
|
-
warning: { wrap: "border-warning/50 bg-warning-soft", chip: "bg-warning text-warning-foreground", dot: "bg-warning" },
|
|
1942
|
-
info: { wrap: "border-info/40 bg-info-soft", chip: "bg-info text-white", dot: "bg-info" }
|
|
1943
|
-
};
|
|
1944
|
-
function PersonAnswerAlerts({ personId, provider }) {
|
|
1945
|
-
const [alerts, setAlerts] = React13.useState([]);
|
|
1946
|
-
const [open, setOpen] = React13.useState(false);
|
|
1947
|
-
React13.useEffect(() => {
|
|
1948
|
-
if (!personId || !provider.getLatestAnswers) return;
|
|
1949
|
-
let alive = true;
|
|
1950
|
-
(async () => {
|
|
1951
|
-
try {
|
|
1952
|
-
const { data: templates } = await provider.getTemplates({ pageSize: 200 });
|
|
1953
|
-
const withAlerts = templates.filter((t) => t.isActive && t.schema.fields.some((f) => alertOf(f)));
|
|
1954
|
-
if (!withAlerts.length) return;
|
|
1955
|
-
const answers = await provider.getLatestAnswers(personId, withAlerts.map((t) => t.id));
|
|
1956
|
-
const out = [];
|
|
1957
|
-
for (const a of answers) {
|
|
1958
|
-
const t = withAlerts.find((x) => x.id === a.templateId);
|
|
1959
|
-
if (!t) continue;
|
|
1960
|
-
for (const f of t.schema.fields) {
|
|
1961
|
-
const hit = answerAlerts(f, a.data[f.id]);
|
|
1962
|
-
if (!hit) continue;
|
|
1963
|
-
out.push({
|
|
1964
|
-
key: `${t.id}:${f.id}`,
|
|
1965
|
-
label: hit.message || f.label,
|
|
1966
|
-
severity: hit.severity ?? "danger",
|
|
1967
|
-
answer: yesNoAnswer(a.data[f.id]),
|
|
1968
|
-
templateName: t.name
|
|
1969
|
-
});
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
out.sort((x, y) => RANK[y.severity] - RANK[x.severity]);
|
|
1973
|
-
if (alive) setAlerts(out);
|
|
1974
|
-
} catch {
|
|
1975
|
-
}
|
|
1976
|
-
})();
|
|
1977
|
-
return () => {
|
|
1978
|
-
alive = false;
|
|
1979
|
-
};
|
|
1980
|
-
}, [personId, provider]);
|
|
1981
|
-
if (!alerts.length) return null;
|
|
1982
|
-
const tone = TONE[alerts[0].severity];
|
|
1983
|
-
return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-lg border-2 ${tone.wrap}`, children: [
|
|
1984
|
-
/* @__PURE__ */ jsxs(
|
|
1985
|
-
Button,
|
|
1986
|
-
{
|
|
1987
|
-
variant: "bare",
|
|
1988
|
-
size: "bare",
|
|
1989
|
-
type: "button",
|
|
1990
|
-
onClick: () => setOpen((o) => !o),
|
|
1991
|
-
"aria-expanded": open,
|
|
1992
|
-
className: "font-normal whitespace-normal justify-start flex w-full items-center gap-2 px-3 py-2 text-left",
|
|
1993
|
-
children: [
|
|
1994
|
-
/* @__PURE__ */ jsx("span", { className: `flex h-7 w-7 shrink-0 items-center justify-center rounded-full ${tone.chip}`, children: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4" }) }),
|
|
1995
|
-
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
1996
|
-
/* @__PURE__ */ jsxs("span", { className: "block text-sm font-semibold leading-tight", children: [
|
|
1997
|
-
alerts.length,
|
|
1998
|
-
" alerta",
|
|
1999
|
-
alerts.length > 1 ? "s" : "",
|
|
2000
|
-
" no hist\xF3rico do cliente"
|
|
2001
|
-
] }),
|
|
2002
|
-
/* @__PURE__ */ jsxs("span", { className: "block truncate text-xs text-muted-foreground", children: [
|
|
2003
|
-
alerts.slice(0, 3).map((a) => a.label).join(" \xB7 "),
|
|
2004
|
-
alerts.length > 3 ? "\u2026" : ""
|
|
2005
|
-
] })
|
|
2006
|
-
] }),
|
|
2007
|
-
open ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4 text-muted-foreground" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })
|
|
2008
|
-
]
|
|
2009
|
-
}
|
|
2010
|
-
),
|
|
2011
|
-
open && /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/40 border-t px-3", children: alerts.map((a) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2 py-2", children: [
|
|
2012
|
-
/* @__PURE__ */ jsx("span", { className: `mt-1.5 h-2 w-2 shrink-0 rounded-full ${TONE[a.severity].dot}` }),
|
|
2013
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
2014
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium leading-tight", children: a.label }),
|
|
2015
|
-
/* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-[11px] leading-tight text-muted-foreground", children: [
|
|
2016
|
-
"Respondeu ",
|
|
2017
|
-
/* @__PURE__ */ jsx("strong", { className: "uppercase", children: a.answer === "sim" ? "Sim" : "N\xE3o" }),
|
|
2018
|
-
" \xB7 ",
|
|
2019
|
-
a.templateName
|
|
2020
|
-
] })
|
|
2021
|
-
] })
|
|
2022
|
-
] }, a.key)) })
|
|
2023
|
-
] });
|
|
2024
|
-
}
|
|
2025
573
|
function registerFormsTimelineSource() {
|
|
2026
574
|
registerTimelineSource({
|
|
2027
575
|
id: "documents",
|
|
@@ -2448,8 +996,8 @@ var CONTRACT_TEXT_CLASS = [
|
|
|
2448
996
|
"[&_th]:border [&_th]:border-border [&_th]:bg-muted/50 [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:font-medium"
|
|
2449
997
|
].join(" ");
|
|
2450
998
|
function useSigned(path) {
|
|
2451
|
-
const [url, setUrl] =
|
|
2452
|
-
|
|
999
|
+
const [url, setUrl] = React4.useState(null);
|
|
1000
|
+
React4.useEffect(() => {
|
|
2453
1001
|
let alive = true;
|
|
2454
1002
|
contractMediaUrl(path).then((u) => {
|
|
2455
1003
|
if (alive) setUrl(u);
|
|
@@ -2462,9 +1010,9 @@ function useSigned(path) {
|
|
|
2462
1010
|
return url;
|
|
2463
1011
|
}
|
|
2464
1012
|
function SignaturePad({ onChange }) {
|
|
2465
|
-
const ref =
|
|
2466
|
-
const drawing =
|
|
2467
|
-
|
|
1013
|
+
const ref = React4.useRef(null);
|
|
1014
|
+
const drawing = React4.useRef(false);
|
|
1015
|
+
React4.useEffect(() => {
|
|
2468
1016
|
const c = ref.current;
|
|
2469
1017
|
if (!c) return;
|
|
2470
1018
|
const r = window.devicePixelRatio || 1;
|
|
@@ -2528,17 +1076,17 @@ function SignaturePad({ onChange }) {
|
|
|
2528
1076
|
] });
|
|
2529
1077
|
}
|
|
2530
1078
|
function SelfieCapture({ value, onChange }) {
|
|
2531
|
-
const video =
|
|
2532
|
-
const stream =
|
|
2533
|
-
const [on, setOn] =
|
|
2534
|
-
const [erro, setErro] =
|
|
2535
|
-
const preview =
|
|
1079
|
+
const video = React4.useRef(null);
|
|
1080
|
+
const stream = React4.useRef(null);
|
|
1081
|
+
const [on, setOn] = React4.useState(false);
|
|
1082
|
+
const [erro, setErro] = React4.useState(null);
|
|
1083
|
+
const preview = React4.useMemo(() => value ? URL.createObjectURL(value) : null, [value]);
|
|
2536
1084
|
const stop = () => {
|
|
2537
1085
|
stream.current?.getTracks().forEach((t) => t.stop());
|
|
2538
1086
|
stream.current = null;
|
|
2539
1087
|
setOn(false);
|
|
2540
1088
|
};
|
|
2541
|
-
|
|
1089
|
+
React4.useEffect(() => () => stop(), []);
|
|
2542
1090
|
async function start() {
|
|
2543
1091
|
setErro(null);
|
|
2544
1092
|
try {
|
|
@@ -2613,16 +1161,16 @@ function printContract(contract, signatureUrl) {
|
|
|
2613
1161
|
setTimeout(() => w.print(), 300);
|
|
2614
1162
|
}
|
|
2615
1163
|
function ContractSignPane({ contract, defaultSignerName, onSigned, onClose }) {
|
|
2616
|
-
const [signer, setSigner] =
|
|
2617
|
-
const [signature, setSignature] =
|
|
2618
|
-
const [selfie, setSelfie] =
|
|
2619
|
-
const [saving, setSaving] =
|
|
2620
|
-
const [erro, setErro] =
|
|
1164
|
+
const [signer, setSigner] = React4.useState(defaultSignerName ?? "");
|
|
1165
|
+
const [signature, setSignature] = React4.useState(null);
|
|
1166
|
+
const [selfie, setSelfie] = React4.useState(null);
|
|
1167
|
+
const [saving, setSaving] = React4.useState(false);
|
|
1168
|
+
const [erro, setErro] = React4.useState(null);
|
|
2621
1169
|
const signatureUrl = useSigned(contract.signaturePath);
|
|
2622
1170
|
const selfieUrl = useSigned(contract.selfiePath);
|
|
2623
1171
|
const signed = contract.status === "signed";
|
|
2624
|
-
const html =
|
|
2625
|
-
|
|
1172
|
+
const html = React4.useMemo(() => sanitizeHtml(contract.html), [contract.html]);
|
|
1173
|
+
React4.useEffect(() => {
|
|
2626
1174
|
setErro(null);
|
|
2627
1175
|
}, [signature, selfie, signer]);
|
|
2628
1176
|
const missing = contract.requireSignature && !signature ? "Desenhe a assinatura para confirmar." : contract.requireSelfie && !selfie ? "Tire a selfie para confirmar." : !signer.trim() ? "Informe o nome de quem assina." : null;
|
|
@@ -2747,6 +1295,9 @@ function registerBuiltInProviders(formsProvider) {
|
|
|
2747
1295
|
}
|
|
2748
1296
|
});
|
|
2749
1297
|
}
|
|
1298
|
+
var PersonDocumentsWidgetImpl = lazyComponent(() => import('./PersonDocumentsWidget-RGN4UHS3.js').then((m) => m.PersonDocumentsWidget));
|
|
1299
|
+
var CustomFormsSettingsTab = lazyComponent(() => import('./CustomFormsSettingsTab-EI4WKOYR.js').then((m) => m.CustomFormsSettingsTab));
|
|
1300
|
+
var PersonAnswerAlerts = lazyComponent(() => import('./PersonAnswerAlerts-MDBBY63K.js').then((m) => m.PersonAnswerAlerts));
|
|
2750
1301
|
function createCustomFormsPlugin(options) {
|
|
2751
1302
|
registerTranslations(customFormsLocales);
|
|
2752
1303
|
const config = resolveConfig(options);
|
|
@@ -2865,14 +1416,14 @@ function createCustomFormsPlugin(options) {
|
|
|
2865
1416
|
}
|
|
2866
1417
|
];
|
|
2867
1418
|
const registries = [...formRegistries, ...options?.settingsRegistries ?? []];
|
|
2868
|
-
const SettingsComponent = () =>
|
|
1419
|
+
const SettingsComponent = () => React4.createElement(PluginSettingsPanel, {
|
|
2869
1420
|
title: config.labels.settingsLabel,
|
|
2870
1421
|
customTabs: [
|
|
2871
1422
|
{
|
|
2872
1423
|
id: "forms",
|
|
2873
1424
|
label: config.labels.pageTitle,
|
|
2874
1425
|
icon: "FileText",
|
|
2875
|
-
content:
|
|
1426
|
+
content: React4.createElement(CustomFormsSettingsTab, { config, provider, store })
|
|
2876
1427
|
}
|
|
2877
1428
|
],
|
|
2878
1429
|
registries,
|
|
@@ -2880,11 +1431,11 @@ function createCustomFormsPlugin(options) {
|
|
|
2880
1431
|
hostPluginId: "custom_forms"
|
|
2881
1432
|
});
|
|
2882
1433
|
SettingsComponent.displayName = "CustomFormsSettingsPanel";
|
|
2883
|
-
const
|
|
2884
|
-
|
|
2885
|
-
const PersonFilesWidget = (props) =>
|
|
1434
|
+
const PersonDocumentsWidget = (props) => React4.createElement(PersonDocumentsWidgetImpl, { ...props, config, provider, store, surface: "records" });
|
|
1435
|
+
PersonDocumentsWidget.displayName = "PersonDocumentsWidget";
|
|
1436
|
+
const PersonFilesWidget = (props) => React4.createElement(PersonDocumentsWidgetImpl, { ...props, config, provider, store, surface: "files" });
|
|
2886
1437
|
PersonFilesWidget.displayName = "PersonFilesWidget";
|
|
2887
|
-
const PersonAnswerAlertsWidget = (props) =>
|
|
1438
|
+
const PersonAnswerAlertsWidget = (props) => React4.createElement(PersonAnswerAlerts, { personId: props.item?.id, provider });
|
|
2888
1439
|
PersonAnswerAlertsWidget.displayName = "PersonAnswerAlertsWidget";
|
|
2889
1440
|
return {
|
|
2890
1441
|
id: "custom_forms",
|
|
@@ -2923,7 +1474,7 @@ function createCustomFormsPlugin(options) {
|
|
|
2923
1474
|
{
|
|
2924
1475
|
id: "person-documents-tab",
|
|
2925
1476
|
zone: "person.detail.documents",
|
|
2926
|
-
component:
|
|
1477
|
+
component: PersonDocumentsWidget,
|
|
2927
1478
|
order: 0
|
|
2928
1479
|
},
|
|
2929
1480
|
{
|
|
@@ -3005,6 +1556,6 @@ function createCustomFormsPlugin(options) {
|
|
|
3005
1556
|
};
|
|
3006
1557
|
}
|
|
3007
1558
|
|
|
3008
|
-
export { ContractSignPane, FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, ensureAttendanceContracts, listAttendanceContracts, recommendationsOf
|
|
1559
|
+
export { ContractSignPane, FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, ensureAttendanceContracts, listAttendanceContracts, recommendationsOf };
|
|
3009
1560
|
//# sourceMappingURL=index.js.map
|
|
3010
1561
|
//# sourceMappingURL=index.js.map
|