@fayz-ai/plugin-forms 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{FormBuilder-YHJHF6HZ.js → FormBuilder-AO6V2YOT.js} +3 -3
- package/dist/{FormBuilder-YHJHF6HZ.js.map → FormBuilder-AO6V2YOT.js.map} +1 -1
- package/dist/{chunk-RBLXIKWB.js → chunk-2AIKIPGU.js} +51 -7
- package/dist/chunk-2AIKIPGU.js.map +1 -0
- package/dist/components/AddDocumentDropdown.d.ts +12 -1
- package/dist/components/AddDocumentDropdown.d.ts.map +1 -1
- package/dist/components/DocumentList.d.ts +4 -1
- package/dist/components/DocumentList.d.ts.map +1 -1
- package/dist/components/DocumentPreview.d.ts +10 -0
- package/dist/components/DocumentPreview.d.ts.map +1 -0
- package/dist/components/FormRenderer.d.ts.map +1 -1
- package/dist/components/PersonDocumentsWidget.d.ts +13 -1
- package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
- package/dist/components/PersonFormsAlert.d.ts.map +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +11 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +722 -181
- package/dist/index.js.map +1 -1
- package/dist/lib/categories.d.ts +69 -0
- package/dist/lib/categories.d.ts.map +1 -0
- package/dist/lib/person-requirements.d.ts +9 -0
- package/dist/lib/person-requirements.d.ts.map +1 -1
- package/dist/lib/plain-text.d.ts +9 -0
- package/dist/lib/plain-text.d.ts.map +1 -0
- package/dist/lib/tab-counts.d.ts +12 -0
- package/dist/lib/tab-counts.d.ts.map +1 -0
- 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/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/TemplateListView.d.ts.map +1 -1
- package/package.json +6 -5
- package/dist/chunk-RBLXIKWB.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { FormRenderer, createOrUpdateFormTool, prefillFromPerson, printDocument } from './chunk-
|
|
2
|
-
import
|
|
3
|
-
import { tl, createSafeDataProvider, registerTranslations, getSupabaseClientOptional, useTranslation, getActiveTenantId, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
|
|
4
|
-
import { getFayzSupabaseClient, useOrganizationStore, PluginSettingsPanel, registerTimelineSource, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions } from '@fayz-ai/admin';
|
|
1
|
+
import { FormRenderer, createOrUpdateFormTool, prefillFromPerson, printDocument, htmlToPlainText } from './chunk-2AIKIPGU.js';
|
|
2
|
+
import React11, { createContext, useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
|
3
|
+
import { tl, createSafeDataProvider, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, useTranslation, getActiveTenantId, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
|
|
4
|
+
import { getFayzSupabaseClient, useOrganizationStore, PluginSettingsPanel, registerTimelineSource, registerTabCountProvider, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions, invalidateTabCounts, TimelineList } from '@fayz-ai/admin';
|
|
5
5
|
import { create } from 'zustand';
|
|
6
|
-
import { ClipboardList, CheckCircle2, Clock, Circle, FileText, Image, Paperclip, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft, Printer, ChevronDown,
|
|
7
|
-
import { Skeleton, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Badge, Switch, RowActions, Button, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel } from '@fayz-ai/ui';
|
|
6
|
+
import { ClipboardList, CheckCircle2, Clock, Circle, FileText, FileSignature, FileCheck2, Activity, Image, Paperclip, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft, Printer, ChevronDown, EyeOff, Eye, Upload } from 'lucide-react';
|
|
7
|
+
import { Skeleton, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Badge, Switch, RowActions, Button, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, ConfirmDialog } from '@fayz-ai/ui';
|
|
8
8
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
9
9
|
|
|
10
10
|
// src/data/tables.ts
|
|
@@ -61,6 +61,7 @@ function createMockFormsProvider() {
|
|
|
61
61
|
isActive: true,
|
|
62
62
|
isDeleted: false,
|
|
63
63
|
requiredOnAttendance: false,
|
|
64
|
+
alertWhenMissing: false,
|
|
64
65
|
validityDays: null,
|
|
65
66
|
createdAt: now(),
|
|
66
67
|
updatedAt: now()
|
|
@@ -119,12 +120,29 @@ function createMockFormsProvider() {
|
|
|
119
120
|
(d) => d.title?.toLowerCase().includes(s) || d.templateName?.toLowerCase().includes(s)
|
|
120
121
|
);
|
|
121
122
|
}
|
|
123
|
+
if (query.kinds?.length) {
|
|
124
|
+
filtered = filtered.filter((d) => query.kinds.includes(d.kind ?? "form"));
|
|
125
|
+
}
|
|
126
|
+
if (query.templateCategory) {
|
|
127
|
+
filtered = filtered.filter((d) => (d.templateCategory ?? "general") === query.templateCategory);
|
|
128
|
+
}
|
|
122
129
|
filtered.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
123
130
|
const page = query.page ?? 1;
|
|
124
131
|
const pageSize = query.pageSize ?? 25;
|
|
125
132
|
const start = (page - 1) * pageSize;
|
|
126
133
|
return { data: filtered.slice(start, start + pageSize), total: filtered.length };
|
|
127
134
|
},
|
|
135
|
+
async getDocumentCounts(query) {
|
|
136
|
+
const counts = {};
|
|
137
|
+
for (const d of documents) {
|
|
138
|
+
if (d.isDeleted || d.status === "archived") continue;
|
|
139
|
+
if (d.personId !== query.personId) continue;
|
|
140
|
+
if (query.kinds?.length && !query.kinds.includes(d.kind ?? "form")) continue;
|
|
141
|
+
const bucket = d.kind === "image" || d.kind === "attachment" ? "file" : d.templateCategory ?? "general";
|
|
142
|
+
counts[bucket] = (counts[bucket] ?? 0) + 1;
|
|
143
|
+
}
|
|
144
|
+
return counts;
|
|
145
|
+
},
|
|
128
146
|
async getDocumentById(id) {
|
|
129
147
|
return documents.find((d) => d.id === id && !d.isDeleted) ?? null;
|
|
130
148
|
},
|
|
@@ -228,6 +246,7 @@ function mapTemplate(r) {
|
|
|
228
246
|
return {
|
|
229
247
|
requiredOnAttendance: r.required_on_attendance ?? false,
|
|
230
248
|
validityDays: r.validity_days ?? null,
|
|
249
|
+
alertWhenMissing: r.alert_when_missing ?? false,
|
|
231
250
|
id: r.id,
|
|
232
251
|
tenantId: r.tenant_id,
|
|
233
252
|
name: r.name,
|
|
@@ -349,15 +368,17 @@ function createSupabaseFormsProvider() {
|
|
|
349
368
|
is_active: input.isActive ?? existing.isActive,
|
|
350
369
|
version: existing.version + 1,
|
|
351
370
|
is_current: true,
|
|
352
|
-
parent_id: existing.parentId ?? existing.id
|
|
371
|
+
parent_id: existing.parentId ?? existing.id,
|
|
372
|
+
// A EXIGÊNCIA É DO MODELO, NÃO DA VERSÃO. Elas eram gravadas na
|
|
373
|
+
// linha que estava sendo ARQUIVADA: editar os campos de uma
|
|
374
|
+
// anamnese exigida devolvia uma versão nova que não exigia nada,
|
|
375
|
+
// e o aviso sumia sem ninguém ter desligado nada.
|
|
376
|
+
required_on_attendance: input.requiredOnAttendance ?? existing.requiredOnAttendance,
|
|
377
|
+
validity_days: input.validityDays !== void 0 ? input.validityDays : existing.validityDays,
|
|
378
|
+
alert_when_missing: input.alertWhenMissing ?? existing.alertWhenMissing
|
|
353
379
|
}).select().single();
|
|
354
380
|
if (insertError) throw new Error(insertError.message);
|
|
355
|
-
await db.from(T.templates).update({
|
|
356
|
-
...input.requiredOnAttendance !== void 0 ? { required_on_attendance: input.requiredOnAttendance } : {},
|
|
357
|
-
...input.validityDays !== void 0 ? { validity_days: input.validityDays } : {},
|
|
358
|
-
is_current: false,
|
|
359
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
360
|
-
}).eq("id", id);
|
|
381
|
+
await db.from(T.templates).update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
361
382
|
return mapTemplate(newRow);
|
|
362
383
|
}
|
|
363
384
|
const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -369,6 +390,9 @@ function createSupabaseFormsProvider() {
|
|
|
369
390
|
if (input.tags !== void 0) updates.tags = input.tags;
|
|
370
391
|
if (input.metadata !== void 0) updates.metadata = input.metadata;
|
|
371
392
|
if (input.isActive !== void 0) updates.is_active = input.isActive;
|
|
393
|
+
if (input.requiredOnAttendance !== void 0) updates.required_on_attendance = input.requiredOnAttendance;
|
|
394
|
+
if (input.validityDays !== void 0) updates.validity_days = input.validityDays;
|
|
395
|
+
if (input.alertWhenMissing !== void 0) updates.alert_when_missing = input.alertWhenMissing;
|
|
372
396
|
const { data, error } = await db.from(T.templates).update(updates).eq("id", id).select().single();
|
|
373
397
|
if (error) throw new Error(error.message);
|
|
374
398
|
return mapTemplate(data);
|
|
@@ -414,6 +438,8 @@ function createSupabaseFormsProvider() {
|
|
|
414
438
|
qb = qb.in("status", statuses);
|
|
415
439
|
}
|
|
416
440
|
if (query.search) qb = qb.or(`title.ilike.%${query.search}%,template_name.ilike.%${query.search}%`);
|
|
441
|
+
if (query.kinds?.length) qb = qb.in("kind", query.kinds);
|
|
442
|
+
if (query.templateCategory) qb = qb.eq("template_category", query.templateCategory);
|
|
417
443
|
const page = query.page ?? 1;
|
|
418
444
|
const pageSize = query.pageSize ?? 50;
|
|
419
445
|
qb = qb.order("created_at", { ascending: false }).range((page - 1) * pageSize, page * pageSize - 1);
|
|
@@ -421,6 +447,27 @@ function createSupabaseFormsProvider() {
|
|
|
421
447
|
if (error) throw new Error(error.message);
|
|
422
448
|
return { data: (data ?? []).map(mapDocument), total: count ?? 0 };
|
|
423
449
|
},
|
|
450
|
+
/**
|
|
451
|
+
* Os totais por balde, sem trazer documento nenhum.
|
|
452
|
+
*
|
|
453
|
+
* Duas colunas e nada mais: o corpo do formulário (`form_data`) é o campo
|
|
454
|
+
* pesado da view, e contar não precisa dele. Agrupa no cliente porque
|
|
455
|
+
* PostgREST não faz `group by` — e a alternativa, um `count` por balde,
|
|
456
|
+
* seriam seis idas para responder uma pergunta.
|
|
457
|
+
*/
|
|
458
|
+
async getDocumentCounts(query) {
|
|
459
|
+
const db = getClient();
|
|
460
|
+
let qb = db.from("v_documents").select("kind, template_category").eq("tenant_id", getTenantId()).eq("person_id", query.personId).eq("is_active", true).neq("status", "archived");
|
|
461
|
+
if (query.kinds?.length) qb = qb.in("kind", query.kinds);
|
|
462
|
+
const { data, error } = await qb;
|
|
463
|
+
if (error) return {};
|
|
464
|
+
const counts = {};
|
|
465
|
+
for (const row of data ?? []) {
|
|
466
|
+
const bucket = row.kind === "image" || row.kind === "attachment" ? "file" : row.template_category || "general";
|
|
467
|
+
counts[bucket] = (counts[bucket] ?? 0) + 1;
|
|
468
|
+
}
|
|
469
|
+
return counts;
|
|
470
|
+
},
|
|
424
471
|
async getDocumentById(id) {
|
|
425
472
|
const db = getClient();
|
|
426
473
|
const { data, error } = await db.from("v_documents").select("*").eq("id", id).single();
|
|
@@ -725,11 +772,34 @@ var en = {
|
|
|
725
772
|
"customForms.noResults": "No results",
|
|
726
773
|
"customForms.noDocuments": "No documents yet",
|
|
727
774
|
"customForms.noDocumentsDescription": "Add a document to start recording information.",
|
|
775
|
+
"customForms.bucket.all": "All",
|
|
776
|
+
"customForms.bucket.file": "Files",
|
|
777
|
+
"customForms.noDocumentsInFilter": "Nothing of this kind on this record.",
|
|
778
|
+
"customForms.documentFallbackTitle": "Document",
|
|
779
|
+
"customForms.recordsTitle": "Records",
|
|
780
|
+
"customForms.filesTitle": "Files",
|
|
781
|
+
"customForms.addRecord": "New",
|
|
782
|
+
"customForms.addFile": "Upload",
|
|
783
|
+
"customForms.view": "View",
|
|
784
|
+
"customForms.showContent": "Show content",
|
|
785
|
+
"customForms.hideContent": "Hide content",
|
|
786
|
+
"customForms.previewEmpty": "This record has no filled field.",
|
|
787
|
+
"customForms.previewMore": "See the other {count} fields",
|
|
788
|
+
"customForms.loadMore": "Load {count} more",
|
|
789
|
+
"customForms.loadingMore": "Loading\u2026",
|
|
790
|
+
"customForms.noRecords": "No records yet",
|
|
791
|
+
"customForms.noRecordsDescription": 'Anamnesis, evolution, report or contract \u2014 start with "New".',
|
|
792
|
+
"customForms.noFiles": "No files yet",
|
|
793
|
+
"customForms.noFilesDescription": "Keep photos, exams and scanned documents here.",
|
|
794
|
+
"customForms.dragFilesHere": "Drag files here",
|
|
795
|
+
"customForms.dropFilesHere": "Drop the files here",
|
|
728
796
|
"customForms.documentTitle": "Document title",
|
|
729
797
|
"customForms.saveAsDraft": "Save as Draft",
|
|
730
798
|
"customForms.saveAndComplete": "Save & Complete",
|
|
731
799
|
"customForms.deleteDocumentConfirm": "Delete this document?",
|
|
732
800
|
"customForms.archive": "Archive",
|
|
801
|
+
"customForms.archiveTitle": "Archive record",
|
|
802
|
+
"customForms.archiveDescription": "{name} leaves this record's list but stays stored \u2014 archiving does not delete.",
|
|
733
803
|
"customForms.archiveDocumentConfirm": "Archive this document? It leaves the active list but is kept on file.",
|
|
734
804
|
"customForms.printPdf": "Print / PDF",
|
|
735
805
|
"customForms.usageCount": "{{count}} filled in",
|
|
@@ -832,12 +902,40 @@ var ptBR = {
|
|
|
832
902
|
"customForms.noResults": "Nenhum resultado",
|
|
833
903
|
"customForms.noDocuments": "Nenhum documento",
|
|
834
904
|
"customForms.noDocumentsDescription": "Adicione um documento para come\xE7ar a registrar informa\xE7\xF5es.",
|
|
905
|
+
// Pastilhas do prontuário — o balde "file" é o que não tem modelo (foto, anexo).
|
|
906
|
+
"customForms.bucket.all": "Tudo",
|
|
907
|
+
"customForms.bucket.file": "Arquivos",
|
|
908
|
+
"customForms.noDocumentsInFilter": "Nada deste tipo nesta ficha.",
|
|
909
|
+
"customForms.documentFallbackTitle": "Documento",
|
|
910
|
+
// Prontuário × Arquivos — as duas superfícies do mesmo widget.
|
|
911
|
+
"customForms.recordsTitle": "Registros",
|
|
912
|
+
"customForms.filesTitle": "Arquivos",
|
|
913
|
+
"customForms.addRecord": "Novo",
|
|
914
|
+
"customForms.addFile": "Enviar",
|
|
915
|
+
"customForms.view": "Ver",
|
|
916
|
+
// Olhinho, prévia e paginação da linha do tempo de registros.
|
|
917
|
+
"customForms.showContent": "Mostrar conte\xFAdo",
|
|
918
|
+
"customForms.hideContent": "Ocultar conte\xFAdo",
|
|
919
|
+
"customForms.previewEmpty": "Registro sem nenhum campo preenchido.",
|
|
920
|
+
"customForms.previewMore": "Ver os outros {count} campos",
|
|
921
|
+
"customForms.loadMore": "Carregar mais {count}",
|
|
922
|
+
"customForms.loadingMore": "Carregando\u2026",
|
|
923
|
+
"customForms.noRecords": "Nenhum registro",
|
|
924
|
+
"customForms.noRecordsDescription": 'Anamnese, evolu\xE7\xE3o, laudo ou contrato \u2014 comece em "Novo".',
|
|
925
|
+
"customForms.noFiles": "Nenhum arquivo",
|
|
926
|
+
"customForms.noFilesDescription": "Guarde aqui foto, exame, documento digitalizado.",
|
|
927
|
+
"customForms.dragFilesHere": "Arraste arquivos para c\xE1",
|
|
928
|
+
"customForms.dropFilesHere": "Solte os arquivos aqui",
|
|
835
929
|
"customForms.documentTitle": "T\xEDtulo do documento",
|
|
836
930
|
"customForms.saveAsDraft": "Salvar Rascunho",
|
|
837
931
|
"customForms.saveAndComplete": "Salvar e Finalizar",
|
|
838
932
|
"customForms.deleteDocumentConfirm": "Excluir este documento?",
|
|
839
933
|
"customForms.archive": "Arquivar",
|
|
840
934
|
"customForms.archiveDocumentConfirm": "Arquivar este documento? Ele sai da lista ativa mas fica guardado.",
|
|
935
|
+
// O diálogo diz DE QUAL registro se trata: "Tem certeza?" sozinho é a
|
|
936
|
+
// pergunta que ninguém consegue responder.
|
|
937
|
+
"customForms.archiveTitle": "Arquivar registro",
|
|
938
|
+
"customForms.archiveDescription": "{name} sai da lista desta ficha, mas continua guardado \u2014 arquivar n\xE3o apaga.",
|
|
841
939
|
"customForms.printPdf": "Imprimir / PDF",
|
|
842
940
|
"customForms.usageCount": "{{count}} preenchidos",
|
|
843
941
|
"customForms.usageLast": "\xFAltimo em {{date}}",
|
|
@@ -941,7 +1039,7 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
941
1039
|
await store.getState().updateTemplate(template.id, { isActive: false });
|
|
942
1040
|
}, [store, t]);
|
|
943
1041
|
const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
|
|
944
|
-
const
|
|
1042
|
+
const categoryLabel2 = (cat) => t(`customForms.category.${cat}`);
|
|
945
1043
|
const columns = useMemo(() => [
|
|
946
1044
|
{
|
|
947
1045
|
id: "name",
|
|
@@ -954,7 +1052,7 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
954
1052
|
{
|
|
955
1053
|
id: "category",
|
|
956
1054
|
header: t("customForms.categoryColumn"),
|
|
957
|
-
cell: ({ row }) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${CATEGORY_COLORS[row.original.category]}`, children:
|
|
1055
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${CATEGORY_COLORS[row.original.category]}`, children: categoryLabel2(row.original.category) })
|
|
958
1056
|
},
|
|
959
1057
|
{
|
|
960
1058
|
id: "fields",
|
|
@@ -999,6 +1097,27 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
999
1097
|
] })
|
|
1000
1098
|
] })
|
|
1001
1099
|
},
|
|
1100
|
+
{
|
|
1101
|
+
id: "alert",
|
|
1102
|
+
header: "Alerta",
|
|
1103
|
+
// A SEGUNDA PERGUNTA DO V1, e ela é outra pergunta mesmo: exigir põe a
|
|
1104
|
+
// bola no checklist do atendimento — trabalho a fazer com a pessoa na
|
|
1105
|
+
// cadeira; alertar leva a falta para a FRENTE, na faixa âmbar do
|
|
1106
|
+
// agendamento e da ficha. Uma casa exige anamnese e não quer a faixa
|
|
1107
|
+
// toda vez que marca horário, e o V1 respeita isso desligado.
|
|
1108
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(
|
|
1109
|
+
Switch,
|
|
1110
|
+
{
|
|
1111
|
+
size: "sm",
|
|
1112
|
+
disabled: !row.original.requiredOnAttendance,
|
|
1113
|
+
checked: row.original.requiredOnAttendance && row.original.alertWhenMissing,
|
|
1114
|
+
onCheckedChange: (on) => {
|
|
1115
|
+
void store.getState().updateTemplate(row.original.id, { alertWhenMissing: on });
|
|
1116
|
+
},
|
|
1117
|
+
"aria-label": `Alertar quando ${row.original.name} estiver faltando`
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
1120
|
+
},
|
|
1002
1121
|
{
|
|
1003
1122
|
id: "usage",
|
|
1004
1123
|
header: t("customForms.usageColumn"),
|
|
@@ -1104,7 +1223,7 @@ function CustomFormsSettingsTab({ config, provider, store }) {
|
|
|
1104
1223
|
window.location.hash = `#${ROUTE_BASE}/forms`;
|
|
1105
1224
|
store.getState().fetchTemplates();
|
|
1106
1225
|
}, [store]);
|
|
1107
|
-
|
|
1226
|
+
React11.useEffect(() => {
|
|
1108
1227
|
const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
|
|
1109
1228
|
window.addEventListener("hashchange", sync);
|
|
1110
1229
|
return () => window.removeEventListener("hashchange", sync);
|
|
@@ -1135,11 +1254,11 @@ function BuilderWrapper({
|
|
|
1135
1254
|
config,
|
|
1136
1255
|
onBack
|
|
1137
1256
|
}) {
|
|
1138
|
-
const FormBuilderLazy =
|
|
1139
|
-
() => import('./FormBuilder-
|
|
1257
|
+
const FormBuilderLazy = React11.lazy(
|
|
1258
|
+
() => import('./FormBuilder-AO6V2YOT.js').then((m) => ({ default: m.FormBuilder }))
|
|
1140
1259
|
);
|
|
1141
1260
|
return /* @__PURE__ */ jsx(
|
|
1142
|
-
|
|
1261
|
+
React11.Suspense,
|
|
1143
1262
|
{
|
|
1144
1263
|
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" }) }),
|
|
1145
1264
|
children: /* @__PURE__ */ jsx(
|
|
@@ -1155,71 +1274,274 @@ function BuilderWrapper({
|
|
|
1155
1274
|
}
|
|
1156
1275
|
);
|
|
1157
1276
|
}
|
|
1158
|
-
var
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1277
|
+
var MAX_FIELDS = 8;
|
|
1278
|
+
var MAX_CHARS = 280;
|
|
1279
|
+
function isEmpty(value) {
|
|
1280
|
+
if (value == null) return true;
|
|
1281
|
+
if (typeof value === "string") return value.trim() === "";
|
|
1282
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
function asText(value) {
|
|
1286
|
+
if (Array.isArray(value)) return value.map((v) => String(v)).join(", ");
|
|
1287
|
+
if (typeof value === "boolean") return value ? "Sim" : "N\xE3o";
|
|
1288
|
+
return htmlToPlainText(String(value)) ?? String(value);
|
|
1289
|
+
}
|
|
1290
|
+
function truncate(text) {
|
|
1291
|
+
return text.length > MAX_CHARS ? `${text.slice(0, MAX_CHARS).trimEnd()}\u2026` : text;
|
|
1292
|
+
}
|
|
1293
|
+
function DocumentPreview({
|
|
1294
|
+
document: doc,
|
|
1295
|
+
provider,
|
|
1296
|
+
onOpen
|
|
1297
|
+
}) {
|
|
1298
|
+
const t = useTranslation();
|
|
1299
|
+
const [template, setTemplate] = useState(null);
|
|
1300
|
+
const [loading, setLoading] = useState(true);
|
|
1301
|
+
useEffect(() => {
|
|
1302
|
+
let alive = true;
|
|
1303
|
+
if (!doc.templateId) {
|
|
1304
|
+
setLoading(false);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
void provider.getTemplateById(doc.templateId).then((found) => {
|
|
1308
|
+
if (alive) {
|
|
1309
|
+
setTemplate(found);
|
|
1310
|
+
setLoading(false);
|
|
1311
|
+
}
|
|
1312
|
+
}).catch(() => {
|
|
1313
|
+
if (alive) setLoading(false);
|
|
1314
|
+
});
|
|
1315
|
+
return () => {
|
|
1316
|
+
alive = false;
|
|
1317
|
+
};
|
|
1318
|
+
}, [doc.templateId, provider]);
|
|
1319
|
+
if (doc.kind === "image" && doc.fileUrl) {
|
|
1320
|
+
return /* @__PURE__ */ jsx(
|
|
1321
|
+
"img",
|
|
1322
|
+
{
|
|
1323
|
+
src: doc.fileUrl,
|
|
1324
|
+
alt: doc.fileName ?? "",
|
|
1325
|
+
className: "max-h-64 rounded-lg object-contain"
|
|
1326
|
+
}
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
if (doc.kind === "attachment") {
|
|
1330
|
+
return /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
1331
|
+
doc.fileName ?? t("customForms.documentFallbackTitle"),
|
|
1332
|
+
doc.fileSize ? ` \xB7 ${(doc.fileSize / 1024).toFixed(1)} KB` : ""
|
|
1333
|
+
] });
|
|
1334
|
+
}
|
|
1335
|
+
if (loading) {
|
|
1336
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1337
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-1/3 animate-pulse rounded bg-muted" }),
|
|
1338
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-2/3 animate-pulse rounded bg-muted" })
|
|
1339
|
+
] });
|
|
1340
|
+
}
|
|
1341
|
+
const fields = template?.schema?.fields ?? [];
|
|
1342
|
+
const answered = fields.filter((f) => f.type !== "title" && !isEmpty(doc.data?.[f.id])).sort((a, b) => a.row - b.row || a.col - b.col);
|
|
1343
|
+
if (answered.length === 0) {
|
|
1344
|
+
return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("customForms.previewEmpty") });
|
|
1345
|
+
}
|
|
1346
|
+
const shown = answered.slice(0, MAX_FIELDS);
|
|
1347
|
+
const hidden = answered.length - shown.length;
|
|
1348
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1349
|
+
/* @__PURE__ */ jsx("dl", { className: "grid gap-x-6 gap-y-2 sm:grid-cols-2", children: shown.map((field) => {
|
|
1350
|
+
const text = truncate(asText(doc.data[field.id]));
|
|
1351
|
+
const wide = field.type === "memo" || field.type === "richtext" || text.length > 60;
|
|
1352
|
+
return /* @__PURE__ */ jsxs("div", { className: `min-w-0 ${wide ? "sm:col-span-2" : ""}`, children: [
|
|
1353
|
+
/* @__PURE__ */ jsx("dt", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: htmlToPlainText(field.label) ?? field.label }),
|
|
1354
|
+
/* @__PURE__ */ jsx("dd", { className: "whitespace-pre-wrap text-sm leading-6 text-foreground", children: text })
|
|
1355
|
+
] }, field.id);
|
|
1356
|
+
}) }),
|
|
1357
|
+
hidden > 0 && /* @__PURE__ */ jsx(
|
|
1358
|
+
"button",
|
|
1359
|
+
{
|
|
1360
|
+
type: "button",
|
|
1361
|
+
onClick: onOpen,
|
|
1362
|
+
className: "text-xs font-medium text-primary hover:underline",
|
|
1363
|
+
children: t("customForms.previewMore", { count: String(hidden) })
|
|
1364
|
+
}
|
|
1365
|
+
)
|
|
1366
|
+
] });
|
|
1367
|
+
}
|
|
1368
|
+
var CATEGORY_ORDER = [
|
|
1369
|
+
"anamnesis",
|
|
1370
|
+
"evolution",
|
|
1371
|
+
"report",
|
|
1372
|
+
"contract",
|
|
1373
|
+
"general"
|
|
1374
|
+
];
|
|
1375
|
+
var CATEGORY_GROUP_ORDER = {
|
|
1376
|
+
anamnesis: 10,
|
|
1377
|
+
evolution: 20,
|
|
1378
|
+
report: 30,
|
|
1379
|
+
contract: 40,
|
|
1380
|
+
general: 50
|
|
1163
1381
|
};
|
|
1164
|
-
var
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1382
|
+
var CATEGORY_ICONS = {
|
|
1383
|
+
anamnesis: ClipboardList,
|
|
1384
|
+
evolution: Activity,
|
|
1385
|
+
report: FileCheck2,
|
|
1386
|
+
contract: FileSignature,
|
|
1387
|
+
general: FileText
|
|
1170
1388
|
};
|
|
1171
|
-
function
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1184
|
-
for (const doc of docs) {
|
|
1185
|
-
const dateKey = doc.createdAt.slice(0, 10);
|
|
1186
|
-
if (!groups.has(dateKey)) groups.set(dateKey, []);
|
|
1187
|
-
groups.get(dateKey).push(doc);
|
|
1389
|
+
function categoryLabel(category) {
|
|
1390
|
+
switch (category) {
|
|
1391
|
+
case "anamnesis":
|
|
1392
|
+
return tl("Anamnesis", "Anamnese");
|
|
1393
|
+
case "evolution":
|
|
1394
|
+
return tl("Evolution", "Evolu\xE7\xE3o");
|
|
1395
|
+
case "report":
|
|
1396
|
+
return tl("Report", "Laudo");
|
|
1397
|
+
case "contract":
|
|
1398
|
+
return tl("Contract", "Contrato");
|
|
1399
|
+
default:
|
|
1400
|
+
return tl("General", "Geral");
|
|
1188
1401
|
}
|
|
1189
|
-
return Array.from(groups.entries()).sort(([a], [b]) => b.localeCompare(a)).map(([date, items]) => ({
|
|
1190
|
-
date,
|
|
1191
|
-
label: formatDateLabel(date),
|
|
1192
|
-
items
|
|
1193
|
-
}));
|
|
1194
1402
|
}
|
|
1195
|
-
function
|
|
1403
|
+
function normalizeCategory(value) {
|
|
1404
|
+
return CATEGORY_ORDER.includes(value) ? value : "general";
|
|
1405
|
+
}
|
|
1406
|
+
var CATEGORY_TIMELINE_ICON = {
|
|
1407
|
+
anamnesis: "ClipboardList",
|
|
1408
|
+
evolution: "Activity",
|
|
1409
|
+
report: "FileCheck2",
|
|
1410
|
+
contract: "FileSignature",
|
|
1411
|
+
general: "FileText"
|
|
1412
|
+
};
|
|
1413
|
+
var CATEGORY_TONE = {
|
|
1414
|
+
anamnesis: "primary",
|
|
1415
|
+
evolution: "info",
|
|
1416
|
+
report: "success",
|
|
1417
|
+
contract: "warning",
|
|
1418
|
+
general: "neutral"
|
|
1419
|
+
};
|
|
1420
|
+
function bucketOf(doc) {
|
|
1421
|
+
if (doc.kind === "image" || doc.kind === "attachment") return "file";
|
|
1422
|
+
return normalizeCategory(doc.templateCategory);
|
|
1423
|
+
}
|
|
1424
|
+
function subtitleFor(doc) {
|
|
1425
|
+
const title = doc.title?.trim();
|
|
1426
|
+
if (!title) return void 0;
|
|
1427
|
+
const model = doc.templateName?.trim();
|
|
1428
|
+
if (!model) return title;
|
|
1429
|
+
if (title === model) return void 0;
|
|
1430
|
+
return title.toLowerCase().startsWith(model.toLowerCase()) ? void 0 : title;
|
|
1431
|
+
}
|
|
1432
|
+
function documentToTimelineItem(doc, fallbackTitle) {
|
|
1433
|
+
const b = bucketOf(doc);
|
|
1434
|
+
const secondary = subtitleFor(doc);
|
|
1435
|
+
return {
|
|
1436
|
+
id: doc.id,
|
|
1437
|
+
sourceId: b,
|
|
1438
|
+
occurredAt: doc.createdAt,
|
|
1439
|
+
title: doc.templateName ?? doc.title ?? fallbackTitle,
|
|
1440
|
+
description: secondary,
|
|
1441
|
+
badge: b === "file" ? void 0 : categoryLabel(b),
|
|
1442
|
+
icon: b === "file" ? doc.kind === "image" ? "Image" : "Paperclip" : CATEGORY_TIMELINE_ICON[b],
|
|
1443
|
+
tone: b === "file" ? "neutral" : CATEGORY_TONE[b]
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
var ACTIVE_STATUSES = ["draft", "completed", "signed"];
|
|
1447
|
+
var EXPAND_KEY = "fayz.forms.documents.expandAll";
|
|
1448
|
+
function DocumentList({ personId, provider, store, onView, onFileDrop, kinds }) {
|
|
1449
|
+
const acceptsFiles = Boolean(onFileDrop) && (!kinds || kinds.includes("attachment"));
|
|
1196
1450
|
const t = useTranslation();
|
|
1197
1451
|
const [documents, setDocuments] = useState([]);
|
|
1452
|
+
const [total, setTotal] = useState(0);
|
|
1453
|
+
const [page, setPage] = useState(1);
|
|
1198
1454
|
const [loading, setLoading] = useState(true);
|
|
1455
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
1456
|
+
const [counts, setCounts] = useState({});
|
|
1457
|
+
const [bucket, setBucket] = useState(void 0);
|
|
1199
1458
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
1459
|
+
const [expandAll, setExpandAll] = useState(() => {
|
|
1460
|
+
try {
|
|
1461
|
+
return localStorage.getItem(EXPAND_KEY) === "1";
|
|
1462
|
+
} catch {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
const toggleExpandAll = useCallback(() => {
|
|
1467
|
+
setExpandAll((prev) => {
|
|
1468
|
+
try {
|
|
1469
|
+
localStorage.setItem(EXPAND_KEY, prev ? "0" : "1");
|
|
1470
|
+
} catch {
|
|
1471
|
+
}
|
|
1472
|
+
return !prev;
|
|
1473
|
+
});
|
|
1474
|
+
}, []);
|
|
1475
|
+
const [openIds, setOpenIds] = useState(/* @__PURE__ */ new Set());
|
|
1200
1476
|
const dragCounter = useRef(0);
|
|
1477
|
+
const PAGE_SIZE = 20;
|
|
1478
|
+
const queryFor = useCallback((nextPage) => ({
|
|
1479
|
+
personId,
|
|
1480
|
+
kinds,
|
|
1481
|
+
templateCategory: bucket && bucket !== "file" ? bucket : void 0,
|
|
1482
|
+
...bucket === "file" ? { kinds: ["image", "attachment"] } : {},
|
|
1483
|
+
status: ACTIVE_STATUSES,
|
|
1484
|
+
page: nextPage,
|
|
1485
|
+
pageSize: PAGE_SIZE
|
|
1486
|
+
}), [personId, kinds, bucket]);
|
|
1201
1487
|
const fetchDocs = useCallback(async () => {
|
|
1202
1488
|
setLoading(true);
|
|
1203
1489
|
try {
|
|
1204
|
-
const result = await provider.getDocuments(
|
|
1205
|
-
setDocuments(result.data
|
|
1490
|
+
const result = await provider.getDocuments(queryFor(1));
|
|
1491
|
+
setDocuments(result.data);
|
|
1492
|
+
setTotal(result.total);
|
|
1493
|
+
setPage(1);
|
|
1206
1494
|
} finally {
|
|
1207
1495
|
setLoading(false);
|
|
1208
1496
|
}
|
|
1209
|
-
}, [
|
|
1497
|
+
}, [provider, queryFor]);
|
|
1498
|
+
const fetchCounts = useCallback(async () => {
|
|
1499
|
+
try {
|
|
1500
|
+
setCounts(await provider.getDocumentCounts({ personId, kinds }));
|
|
1501
|
+
} catch {
|
|
1502
|
+
setCounts({});
|
|
1503
|
+
}
|
|
1504
|
+
}, [provider, personId, kinds]);
|
|
1505
|
+
const loadMore = useCallback(async () => {
|
|
1506
|
+
setLoadingMore(true);
|
|
1507
|
+
try {
|
|
1508
|
+
const next = page + 1;
|
|
1509
|
+
const result = await provider.getDocuments(queryFor(next));
|
|
1510
|
+
setDocuments((prev) => {
|
|
1511
|
+
const seen = new Set(prev.map((d) => d.id));
|
|
1512
|
+
return [...prev, ...result.data.filter((d) => !seen.has(d.id))];
|
|
1513
|
+
});
|
|
1514
|
+
setTotal(result.total);
|
|
1515
|
+
setPage(next);
|
|
1516
|
+
} finally {
|
|
1517
|
+
setLoadingMore(false);
|
|
1518
|
+
}
|
|
1519
|
+
}, [provider, queryFor, page]);
|
|
1210
1520
|
useEffect(() => {
|
|
1211
1521
|
const timer = setTimeout(() => fetchDocs(), 50);
|
|
1212
1522
|
return () => clearTimeout(timer);
|
|
1213
1523
|
}, [fetchDocs]);
|
|
1214
1524
|
useEffect(() => {
|
|
1215
|
-
|
|
1525
|
+
void fetchCounts();
|
|
1526
|
+
}, [fetchCounts]);
|
|
1527
|
+
useEffect(() => {
|
|
1528
|
+
const unsub = store.subscribe(() => {
|
|
1529
|
+
void fetchDocs();
|
|
1530
|
+
void fetchCounts();
|
|
1531
|
+
});
|
|
1216
1532
|
return unsub;
|
|
1217
|
-
}, [store, fetchDocs]);
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1533
|
+
}, [store, fetchDocs, fetchCounts]);
|
|
1534
|
+
const [archiving, setArchiving] = useState(null);
|
|
1535
|
+
const handleArchive = useCallback(async () => {
|
|
1536
|
+
const doc = archiving;
|
|
1537
|
+
setArchiving(null);
|
|
1538
|
+
if (!doc) return;
|
|
1220
1539
|
await provider.updateDocument(doc.id, { status: "archived" });
|
|
1221
1540
|
setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
|
|
1222
|
-
|
|
1541
|
+
setTotal((n) => Math.max(0, n - 1));
|
|
1542
|
+
void fetchCounts();
|
|
1543
|
+
invalidateTabCounts();
|
|
1544
|
+
}, [provider, archiving, fetchCounts]);
|
|
1223
1545
|
const handleDragEnter = useCallback((e) => {
|
|
1224
1546
|
e.preventDefault();
|
|
1225
1547
|
dragCounter.current++;
|
|
@@ -1242,7 +1564,44 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
|
|
|
1242
1564
|
onFileDrop(files);
|
|
1243
1565
|
}
|
|
1244
1566
|
}, [onFileDrop]);
|
|
1245
|
-
const
|
|
1567
|
+
const buckets = useMemo(() => {
|
|
1568
|
+
const order = [...CATEGORY_ORDER, "file"];
|
|
1569
|
+
return order.filter((b) => (counts[b] ?? 0) > 0).map((b) => ({
|
|
1570
|
+
id: b,
|
|
1571
|
+
label: b === "file" ? t("customForms.bucket.file") : categoryLabel(b),
|
|
1572
|
+
count: counts[b] ?? 0
|
|
1573
|
+
}));
|
|
1574
|
+
}, [counts, t]);
|
|
1575
|
+
const totalAll = useMemo(
|
|
1576
|
+
() => Object.values(counts).reduce((sum, n) => sum + n, 0),
|
|
1577
|
+
[counts]
|
|
1578
|
+
);
|
|
1579
|
+
useEffect(() => {
|
|
1580
|
+
if (bucket && !buckets.some((b) => b.id === bucket)) setBucket(void 0);
|
|
1581
|
+
}, [buckets, bucket]);
|
|
1582
|
+
const visible = documents;
|
|
1583
|
+
const items = useMemo(
|
|
1584
|
+
() => visible.map((doc) => documentToTimelineItem(doc, t("customForms.documentFallbackTitle"))),
|
|
1585
|
+
[visible, t]
|
|
1586
|
+
);
|
|
1587
|
+
const byId = useMemo(() => new Map(visible.map((d) => [d.id, d])), [visible]);
|
|
1588
|
+
const expandedIds = useMemo(() => {
|
|
1589
|
+
if (!expandAll) return openIds;
|
|
1590
|
+
const open = new Set(visible.map((d) => d.id));
|
|
1591
|
+
for (const id of openIds) open.delete(id);
|
|
1592
|
+
return open;
|
|
1593
|
+
}, [expandAll, openIds, visible]);
|
|
1594
|
+
const toggleOne = useCallback((id) => {
|
|
1595
|
+
setOpenIds((prev) => {
|
|
1596
|
+
const next = new Set(prev);
|
|
1597
|
+
if (next.has(id)) next.delete(id);
|
|
1598
|
+
else next.add(id);
|
|
1599
|
+
return next;
|
|
1600
|
+
});
|
|
1601
|
+
}, []);
|
|
1602
|
+
useEffect(() => {
|
|
1603
|
+
setOpenIds(/* @__PURE__ */ new Set());
|
|
1604
|
+
}, [expandAll]);
|
|
1246
1605
|
if (loading) {
|
|
1247
1606
|
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: [
|
|
1248
1607
|
/* @__PURE__ */ jsx("div", { className: "h-9 w-9 rounded-lg bg-muted animate-pulse shrink-0" }),
|
|
@@ -1252,86 +1611,133 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
|
|
|
1252
1611
|
] })
|
|
1253
1612
|
] }, i)) });
|
|
1254
1613
|
}
|
|
1255
|
-
|
|
1256
|
-
"
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1614
|
+
const filters = (buckets.length > 1 || totalAll > 0) && /* @__PURE__ */ jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-1.5", children: [
|
|
1615
|
+
buckets.length > 1 && [{ id: void 0, label: t("customForms.bucket.all"), count: totalAll }, ...buckets].map((chip) => {
|
|
1616
|
+
const active = chip.id === bucket;
|
|
1617
|
+
return /* @__PURE__ */ jsxs(
|
|
1618
|
+
"button",
|
|
1619
|
+
{
|
|
1620
|
+
type: "button",
|
|
1621
|
+
onClick: () => setBucket(chip.id),
|
|
1622
|
+
className: `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"}`,
|
|
1623
|
+
children: [
|
|
1624
|
+
chip.label,
|
|
1625
|
+
/* @__PURE__ */ jsx("span", { className: "ml-1.5 tabular-nums opacity-60", children: chip.count })
|
|
1626
|
+
]
|
|
1627
|
+
},
|
|
1628
|
+
chip.id ?? "__all__"
|
|
1629
|
+
);
|
|
1630
|
+
}),
|
|
1631
|
+
/* @__PURE__ */ jsxs(
|
|
1632
|
+
"button",
|
|
1633
|
+
{
|
|
1634
|
+
type: "button",
|
|
1635
|
+
onClick: toggleExpandAll,
|
|
1636
|
+
"aria-pressed": expandAll,
|
|
1637
|
+
title: t(expandAll ? "customForms.hideContent" : "customForms.showContent"),
|
|
1638
|
+
className: `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"}`,
|
|
1639
|
+
children: [
|
|
1640
|
+
expandAll ? /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" }),
|
|
1641
|
+
t(expandAll ? "customForms.hideContent" : "customForms.showContent")
|
|
1642
|
+
]
|
|
1643
|
+
}
|
|
1644
|
+
)
|
|
1645
|
+
] });
|
|
1646
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1647
|
+
filters,
|
|
1648
|
+
/* @__PURE__ */ jsxs(
|
|
1649
|
+
"div",
|
|
1650
|
+
{
|
|
1651
|
+
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"}`,
|
|
1652
|
+
...acceptsFiles ? {
|
|
1653
|
+
onDragEnter: handleDragEnter,
|
|
1654
|
+
onDragLeave: handleDragLeave,
|
|
1655
|
+
onDragOver: handleDragOver,
|
|
1656
|
+
onDrop: handleDrop
|
|
1657
|
+
} : {},
|
|
1658
|
+
children: [
|
|
1659
|
+
isDragOver && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 z-10 flex flex-col items-center justify-center rounded-xl bg-primary/5", children: [
|
|
1660
|
+
/* @__PURE__ */ jsx(Upload, { className: "h-8 w-8 text-primary/50 mb-2" }),
|
|
1661
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-primary/70", children: t("customForms.dropFilesHere") })
|
|
1282
1662
|
] }),
|
|
1283
|
-
/* @__PURE__ */ jsx("div", { className: "
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity", children: [
|
|
1302
|
-
/* @__PURE__ */ jsx(
|
|
1303
|
-
"button",
|
|
1304
|
-
{
|
|
1305
|
-
onClick: (e) => {
|
|
1306
|
-
e.stopPropagation();
|
|
1307
|
-
onView(doc);
|
|
1308
|
-
},
|
|
1309
|
-
className: "p-1.5 rounded-md hover:bg-muted",
|
|
1310
|
-
children: /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
1311
|
-
}
|
|
1312
|
-
),
|
|
1313
|
-
/* @__PURE__ */ jsx(
|
|
1314
|
-
"button",
|
|
1315
|
-
{
|
|
1316
|
-
onClick: (e) => {
|
|
1317
|
-
e.stopPropagation();
|
|
1318
|
-
handleArchive(doc);
|
|
1319
|
-
},
|
|
1320
|
-
className: "p-1.5 rounded-md hover:bg-muted",
|
|
1321
|
-
title: t("customForms.archive"),
|
|
1322
|
-
children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
1323
|
-
}
|
|
1324
|
-
)
|
|
1325
|
-
] })
|
|
1326
|
-
]
|
|
1663
|
+
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") }) }),
|
|
1664
|
+
documents.length === 0 && !isDragOver && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [
|
|
1665
|
+
/* @__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" }) }),
|
|
1666
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t(acceptsFiles ? "customForms.noFiles" : "customForms.noRecords") }),
|
|
1667
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1 max-w-xs", children: t(acceptsFiles ? "customForms.noFilesDescription" : "customForms.noRecordsDescription") }),
|
|
1668
|
+
acceptsFiles && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground mt-3 flex items-center gap-1", children: [
|
|
1669
|
+
/* @__PURE__ */ jsx(Upload, { className: "h-3 w-3" }),
|
|
1670
|
+
t("customForms.dragFilesHere")
|
|
1671
|
+
] })
|
|
1672
|
+
] }),
|
|
1673
|
+
visible.length > 0 && /* @__PURE__ */ jsx(
|
|
1674
|
+
TimelineList,
|
|
1675
|
+
{
|
|
1676
|
+
items,
|
|
1677
|
+
expandedIds,
|
|
1678
|
+
renderPreview: (it) => {
|
|
1679
|
+
const doc = byId.get(it.id);
|
|
1680
|
+
return doc ? /* @__PURE__ */ jsx(DocumentPreview, { document: doc, provider, onOpen: () => onView(doc) }) : null;
|
|
1327
1681
|
},
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1682
|
+
onSelect: (it) => {
|
|
1683
|
+
const doc = byId.get(it.id);
|
|
1684
|
+
if (doc) onView(doc);
|
|
1685
|
+
},
|
|
1686
|
+
renderActions: (it) => {
|
|
1687
|
+
const doc = byId.get(it.id);
|
|
1688
|
+
if (!doc) return null;
|
|
1689
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1690
|
+
/* @__PURE__ */ jsx(
|
|
1691
|
+
"button",
|
|
1692
|
+
{
|
|
1693
|
+
type: "button",
|
|
1694
|
+
onClick: (e) => {
|
|
1695
|
+
e.stopPropagation();
|
|
1696
|
+
toggleOne(doc.id);
|
|
1697
|
+
},
|
|
1698
|
+
className: "rounded-md p-1.5 hover:bg-muted",
|
|
1699
|
+
"aria-label": t(expandedIds.has(doc.id) ? "customForms.hideContent" : "customForms.showContent"),
|
|
1700
|
+
"aria-expanded": expandedIds.has(doc.id),
|
|
1701
|
+
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" })
|
|
1702
|
+
}
|
|
1703
|
+
),
|
|
1704
|
+
/* @__PURE__ */ jsx(
|
|
1705
|
+
"button",
|
|
1706
|
+
{
|
|
1707
|
+
type: "button",
|
|
1708
|
+
onClick: (e) => {
|
|
1709
|
+
e.stopPropagation();
|
|
1710
|
+
setArchiving(doc);
|
|
1711
|
+
},
|
|
1712
|
+
className: "rounded-md p-1.5 hover:bg-muted",
|
|
1713
|
+
"aria-label": t("customForms.archive"),
|
|
1714
|
+
title: t("customForms.archive"),
|
|
1715
|
+
children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
1716
|
+
}
|
|
1717
|
+
)
|
|
1718
|
+
] });
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
),
|
|
1722
|
+
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) }) }) })
|
|
1723
|
+
]
|
|
1724
|
+
}
|
|
1725
|
+
),
|
|
1726
|
+
/* @__PURE__ */ jsx(
|
|
1727
|
+
ConfirmDialog,
|
|
1728
|
+
{
|
|
1729
|
+
open: archiving !== null,
|
|
1730
|
+
title: t("customForms.archiveTitle"),
|
|
1731
|
+
description: t("customForms.archiveDescription", {
|
|
1732
|
+
name: archiving?.templateName ?? archiving?.title ?? t("customForms.documentFallbackTitle")
|
|
1733
|
+
}),
|
|
1734
|
+
confirmLabel: t("customForms.archive"),
|
|
1735
|
+
cancelLabel: t("common.cancel"),
|
|
1736
|
+
onConfirm: () => void handleArchive(),
|
|
1737
|
+
onCancel: () => setArchiving(null)
|
|
1738
|
+
}
|
|
1739
|
+
)
|
|
1740
|
+
] });
|
|
1335
1741
|
}
|
|
1336
1742
|
function DocumentBodySkeleton({ rows = 5 }) {
|
|
1337
1743
|
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: [
|
|
@@ -1375,7 +1781,7 @@ function DocumentFormDialog({
|
|
|
1375
1781
|
provider.getTemplateById(existingDocument.templateId).then(setTemplate);
|
|
1376
1782
|
}
|
|
1377
1783
|
}, [existingDocument, template, provider]);
|
|
1378
|
-
const prefilled =
|
|
1784
|
+
const prefilled = React11.useRef(!!initialTemplate || !!existingDocument);
|
|
1379
1785
|
useEffect(() => {
|
|
1380
1786
|
if (prefilled.current || !template || existingDocument) return;
|
|
1381
1787
|
prefilled.current = true;
|
|
@@ -1407,7 +1813,7 @@ function DocumentFormDialog({
|
|
|
1407
1813
|
},
|
|
1408
1814
|
[template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
|
|
1409
1815
|
);
|
|
1410
|
-
const snapshot =
|
|
1816
|
+
const snapshot = React11.useRef(null);
|
|
1411
1817
|
useEffect(() => {
|
|
1412
1818
|
if (template && snapshot.current === null) {
|
|
1413
1819
|
snapshot.current = JSON.stringify(existingDocument?.data ?? {});
|
|
@@ -1478,7 +1884,7 @@ function DocumentFormDialog({
|
|
|
1478
1884
|
) })
|
|
1479
1885
|
] });
|
|
1480
1886
|
}
|
|
1481
|
-
var
|
|
1887
|
+
var STATUS_COLORS = {
|
|
1482
1888
|
draft: "bg-warning-soft text-warning-soft-foreground",
|
|
1483
1889
|
completed: "bg-success-soft text-success-soft-foreground",
|
|
1484
1890
|
signed: "bg-info-soft text-info-soft-foreground",
|
|
@@ -1508,7 +1914,7 @@ function FormViewer({ document: doc, provider, onBack, onEdit }) {
|
|
|
1508
1914
|
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm truncate", children: doc.title ?? template?.name ?? "" }),
|
|
1509
1915
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: new Date(doc.createdAt).toLocaleDateString() })
|
|
1510
1916
|
] }),
|
|
1511
|
-
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${
|
|
1917
|
+
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${STATUS_COLORS[doc.status] ?? ""}`, children: t(`customForms.status.${doc.status}`) }),
|
|
1512
1918
|
/* @__PURE__ */ jsxs(
|
|
1513
1919
|
Button,
|
|
1514
1920
|
{
|
|
@@ -1561,7 +1967,7 @@ async function getAllDocumentTypes(personId) {
|
|
|
1561
1967
|
}
|
|
1562
1968
|
var SEARCH_THRESHOLD = 8;
|
|
1563
1969
|
var normalize = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
1564
|
-
function AddDocumentDropdown({ personId, onSelect }) {
|
|
1970
|
+
function AddDocumentDropdown({ personId, onSelect, optionFilter, label }) {
|
|
1565
1971
|
const t = useTranslation();
|
|
1566
1972
|
const [open, setOpen] = useState(false);
|
|
1567
1973
|
const [options, setOptions] = useState([]);
|
|
@@ -1579,11 +1985,11 @@ function AddDocumentDropdown({ personId, onSelect }) {
|
|
|
1579
1985
|
if (loaded || loadingRef.current) return;
|
|
1580
1986
|
loadingRef.current = true;
|
|
1581
1987
|
getAllDocumentTypes(personId).then((opts) => {
|
|
1582
|
-
setOptions(opts);
|
|
1988
|
+
setOptions(optionFilter ? opts.filter(optionFilter) : opts);
|
|
1583
1989
|
setLoaded(true);
|
|
1584
1990
|
loadingRef.current = false;
|
|
1585
1991
|
});
|
|
1586
|
-
}, [personId, loaded]);
|
|
1992
|
+
}, [personId, loaded, optionFilter]);
|
|
1587
1993
|
const filtered = useMemo(() => {
|
|
1588
1994
|
const q = normalize(query.trim());
|
|
1589
1995
|
if (!q) return options;
|
|
@@ -1635,7 +2041,7 @@ function AddDocumentDropdown({ personId, onSelect }) {
|
|
|
1635
2041
|
return /* @__PURE__ */ jsxs(Dropdown, { open, onOpenChange: handleOpenChange, children: [
|
|
1636
2042
|
/* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "sm", className: "h-8", children: [
|
|
1637
2043
|
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1" }),
|
|
1638
|
-
t("customForms.addDocument"),
|
|
2044
|
+
label ?? t("customForms.addDocument"),
|
|
1639
2045
|
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 ml-1 opacity-50" })
|
|
1640
2046
|
] }) }),
|
|
1641
2047
|
/* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-72 p-0", children: [
|
|
@@ -1657,7 +2063,7 @@ function AddDocumentDropdown({ personId, onSelect }) {
|
|
|
1657
2063
|
)
|
|
1658
2064
|
] }),
|
|
1659
2065
|
/* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-[min(60vh,20rem)] overflow-y-auto overscroll-contain p-1", children: [
|
|
1660
|
-
groups.map((group, gi) => /* @__PURE__ */ jsxs(
|
|
2066
|
+
groups.map((group, gi) => /* @__PURE__ */ jsxs(React11.Fragment, { children: [
|
|
1661
2067
|
gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
1662
2068
|
/* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
|
|
1663
2069
|
group.items.map((opt) => {
|
|
@@ -1691,8 +2097,17 @@ function AddDocumentDropdown({ personId, onSelect }) {
|
|
|
1691
2097
|
] })
|
|
1692
2098
|
] });
|
|
1693
2099
|
}
|
|
1694
|
-
|
|
2100
|
+
var SURFACE_KINDS = {
|
|
2101
|
+
records: ["form"],
|
|
2102
|
+
files: ["image", "attachment"]
|
|
2103
|
+
};
|
|
2104
|
+
function PersonDocumentsWidget({ item, config, provider, store, surface = "records" }) {
|
|
1695
2105
|
const t = useTranslation();
|
|
2106
|
+
const kinds = SURFACE_KINDS[surface];
|
|
2107
|
+
const optionFilter = useCallback(
|
|
2108
|
+
(o) => surface === "records" ? o.id.startsWith("template:") : o.id === "core:image" || o.id === "core:attachment",
|
|
2109
|
+
[surface]
|
|
2110
|
+
);
|
|
1696
2111
|
const guardDocuments = useLimitGuard("documents_month");
|
|
1697
2112
|
const [view, setView] = useState({ type: "list" });
|
|
1698
2113
|
const fileInputRef = useRef(null);
|
|
@@ -1799,8 +2214,16 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
|
|
|
1799
2214
|
}
|
|
1800
2215
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1801
2216
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1802
|
-
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("customForms.
|
|
1803
|
-
/* @__PURE__ */ jsx(
|
|
2217
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t(surface === "files" ? "customForms.filesTitle" : "customForms.recordsTitle") }),
|
|
2218
|
+
/* @__PURE__ */ jsx(
|
|
2219
|
+
AddDocumentDropdown,
|
|
2220
|
+
{
|
|
2221
|
+
personId: item.id,
|
|
2222
|
+
onSelect: handleDocTypeSelect,
|
|
2223
|
+
optionFilter,
|
|
2224
|
+
label: t(surface === "files" ? "customForms.addFile" : "customForms.addRecord")
|
|
2225
|
+
}
|
|
2226
|
+
)
|
|
1804
2227
|
] }),
|
|
1805
2228
|
/* @__PURE__ */ jsx(
|
|
1806
2229
|
DocumentList,
|
|
@@ -1808,6 +2231,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
|
|
|
1808
2231
|
personId: item.id,
|
|
1809
2232
|
provider,
|
|
1810
2233
|
store,
|
|
2234
|
+
kinds,
|
|
1811
2235
|
onView: handleViewDoc,
|
|
1812
2236
|
onFileDrop: handleFiles
|
|
1813
2237
|
}
|
|
@@ -1882,6 +2306,24 @@ function registerFormsTimelineSource() {
|
|
|
1882
2306
|
}
|
|
1883
2307
|
});
|
|
1884
2308
|
}
|
|
2309
|
+
function registerFormsTabCounts() {
|
|
2310
|
+
registerTabCountProvider({
|
|
2311
|
+
id: "custom_forms:documents",
|
|
2312
|
+
async fetch({ tenantId, personId }) {
|
|
2313
|
+
const db = getSupabaseClientOptional();
|
|
2314
|
+
if (!db) return {};
|
|
2315
|
+
const { data, error } = await db.from("documents").select("kind").eq("tenant_id", tenantId).eq("person_id", personId).eq("is_active", true).neq("status", "archived");
|
|
2316
|
+
if (error) return {};
|
|
2317
|
+
let records = 0;
|
|
2318
|
+
let files = 0;
|
|
2319
|
+
for (const row of data ?? []) {
|
|
2320
|
+
if (row.kind === "image" || row.kind === "attachment") files += 1;
|
|
2321
|
+
else records += 1;
|
|
2322
|
+
}
|
|
2323
|
+
return { documents: records, files };
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
1885
2327
|
var SUBMISSIONS = "plg_forms_rep_submissions";
|
|
1886
2328
|
var CAT = tl("Forms", "Formul\xE1rios");
|
|
1887
2329
|
var STATUS_LABELS = {
|
|
@@ -1977,6 +2419,7 @@ async function getPersonFormRequirements(tenantId, personId) {
|
|
|
1977
2419
|
templateName: r.template_name,
|
|
1978
2420
|
category: r.category,
|
|
1979
2421
|
validityDays: r.validity_days ?? null,
|
|
2422
|
+
alertWhenMissing: r.alert_when_missing ?? false,
|
|
1980
2423
|
lastFilledAt: r.last_filled_at ?? null,
|
|
1981
2424
|
validUntil: r.valid_until ?? null,
|
|
1982
2425
|
state: r.state
|
|
@@ -2011,8 +2454,8 @@ function Linha({ item }) {
|
|
|
2011
2454
|
}
|
|
2012
2455
|
function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, className }) {
|
|
2013
2456
|
const tenantId = useOrganizationStore((s) => s.currentOrg?.id);
|
|
2014
|
-
const [rows, setRows] =
|
|
2015
|
-
|
|
2457
|
+
const [rows, setRows] = React11.useState(null);
|
|
2458
|
+
React11.useEffect(() => {
|
|
2016
2459
|
if (!personId || !tenantId) {
|
|
2017
2460
|
setRows([]);
|
|
2018
2461
|
return;
|
|
@@ -2028,8 +2471,17 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, cl
|
|
|
2028
2471
|
cancelled = true;
|
|
2029
2472
|
};
|
|
2030
2473
|
}, [personId, tenantId, version]);
|
|
2031
|
-
const itens =
|
|
2032
|
-
|
|
2474
|
+
const itens = React11.useMemo(() => [
|
|
2475
|
+
// O QUE NÃO ALERTA NÃO INTERROMPE. Exigir um formulário põe a bola no
|
|
2476
|
+
// checklist do atendimento; ALERTAR é a segunda decisão do V1
|
|
2477
|
+
// (`alert_when_missing`, desligada por padrão) e é ela que autoriza a
|
|
2478
|
+
// faixa âmbar na frente de quem só queria marcar um horário. Sem este
|
|
2479
|
+
// recorte, uma anamnese importada com a segunda chave desligada — como a
|
|
2480
|
+
// casa já a tinha no V1 — abria "1 item pendente" em todo agendamento.
|
|
2481
|
+
//
|
|
2482
|
+
// Em dia continua aparecendo de qualquer forma: a linha verde é
|
|
2483
|
+
// confirmação, não interrupção, e é ela que faz a âmbar ter crédito.
|
|
2484
|
+
...(rows ?? []).filter((r) => r.alertWhenMissing || !isOutstanding(r)).map((r) => {
|
|
2033
2485
|
const pendente = isOutstanding(r);
|
|
2034
2486
|
return {
|
|
2035
2487
|
id: r.templateId,
|
|
@@ -2046,7 +2498,7 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, cl
|
|
|
2046
2498
|
const pendentes = itens.filter((i) => i.state !== "done");
|
|
2047
2499
|
const faltando = pendentes.length;
|
|
2048
2500
|
const chave = pendentes.map((i) => i.label).join("|");
|
|
2049
|
-
|
|
2501
|
+
React11.useEffect(() => {
|
|
2050
2502
|
onPending?.(chave ? chave.split("|") : []);
|
|
2051
2503
|
}, [chave]);
|
|
2052
2504
|
if (rows === null) return /* @__PURE__ */ jsx(Skeleton, { className: `h-12 w-full ${className ?? ""}` });
|
|
@@ -2064,19 +2516,19 @@ function CustomFormsProvider({
|
|
|
2064
2516
|
value,
|
|
2065
2517
|
children
|
|
2066
2518
|
}) {
|
|
2067
|
-
return
|
|
2519
|
+
return React11.createElement(Ctx.Provider, { value }, children);
|
|
2068
2520
|
}
|
|
2069
2521
|
function FormFillDialog({ templateId, personId, personName, onSaved, onClose, frame = "modal" }) {
|
|
2070
|
-
const provider =
|
|
2522
|
+
const provider = React11.useMemo(() => createSafeDataProvider(
|
|
2071
2523
|
() => createSupabaseFormsProvider(),
|
|
2072
2524
|
() => createMockFormsProvider()
|
|
2073
2525
|
), []);
|
|
2074
|
-
const store =
|
|
2075
|
-
const config =
|
|
2076
|
-
const [template, setTemplate] =
|
|
2077
|
-
const [erro, setErro] =
|
|
2078
|
-
const [data, setData] =
|
|
2079
|
-
const [salvando, setSalvando] =
|
|
2526
|
+
const store = React11.useMemo(() => createCustomFormsStore(provider), [provider]);
|
|
2527
|
+
const config = React11.useMemo(() => resolveConfig(), []);
|
|
2528
|
+
const [template, setTemplate] = React11.useState(null);
|
|
2529
|
+
const [erro, setErro] = React11.useState(null);
|
|
2530
|
+
const [data, setData] = React11.useState({});
|
|
2531
|
+
const [salvando, setSalvando] = React11.useState(false);
|
|
2080
2532
|
async function salvar() {
|
|
2081
2533
|
if (!template) return;
|
|
2082
2534
|
setSalvando(true);
|
|
@@ -2096,7 +2548,7 @@ function FormFillDialog({ templateId, personId, personName, onSaved, onClose, fr
|
|
|
2096
2548
|
setSalvando(false);
|
|
2097
2549
|
}
|
|
2098
2550
|
}
|
|
2099
|
-
|
|
2551
|
+
React11.useEffect(() => {
|
|
2100
2552
|
let vivo = true;
|
|
2101
2553
|
(async () => {
|
|
2102
2554
|
try {
|
|
@@ -2156,16 +2608,19 @@ function registerBuiltInProviders(formsProvider) {
|
|
|
2156
2608
|
async getTypes() {
|
|
2157
2609
|
try {
|
|
2158
2610
|
const result = await formsProvider.getTemplates({});
|
|
2159
|
-
return result.data.map((tpl) =>
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2611
|
+
return result.data.map((tpl) => {
|
|
2612
|
+
const category = normalizeCategory(tpl.category);
|
|
2613
|
+
return {
|
|
2614
|
+
id: `template:${tpl.id}`,
|
|
2615
|
+
label: tpl.name,
|
|
2616
|
+
icon: CATEGORY_ICONS[category] ?? FileText,
|
|
2617
|
+
group: categoryLabel(category),
|
|
2618
|
+
groupOrder: CATEGORY_GROUP_ORDER[category] ?? 50,
|
|
2619
|
+
order: 0,
|
|
2620
|
+
description: tpl.description,
|
|
2621
|
+
_templateId: tpl.id
|
|
2622
|
+
};
|
|
2623
|
+
});
|
|
2169
2624
|
} catch {
|
|
2170
2625
|
return [];
|
|
2171
2626
|
}
|
|
@@ -2175,8 +2630,8 @@ function registerBuiltInProviders(formsProvider) {
|
|
|
2175
2630
|
id: "core:files",
|
|
2176
2631
|
getTypes() {
|
|
2177
2632
|
return [
|
|
2178
|
-
{ id: "core:image", label: "Imagem", icon: Image, group: "Arquivos", groupOrder: 90, order: 0 },
|
|
2179
|
-
{ id: "core:attachment", label: "Anexo", icon: Paperclip, group: "Arquivos", groupOrder: 90, order: 1 }
|
|
2633
|
+
{ id: "core:image", label: tl("Image", "Imagem"), icon: Image, group: tl("Files", "Arquivos"), groupOrder: 90, order: 0 },
|
|
2634
|
+
{ id: "core:attachment", label: tl("Attachment", "Anexo"), icon: Paperclip, group: tl("Files", "Arquivos"), groupOrder: 90, order: 1 }
|
|
2180
2635
|
];
|
|
2181
2636
|
}
|
|
2182
2637
|
});
|
|
@@ -2191,7 +2646,85 @@ function createCustomFormsPlugin(options) {
|
|
|
2191
2646
|
const store = createCustomFormsStore(provider);
|
|
2192
2647
|
registerBuiltInProviders(provider);
|
|
2193
2648
|
registerFormsTimelineSource();
|
|
2649
|
+
registerFormsTabCounts();
|
|
2650
|
+
const contractTemplateEntity = {
|
|
2651
|
+
name: "Modelo de contrato",
|
|
2652
|
+
namePlural: "Modelos de contrato",
|
|
2653
|
+
icon: "FileSignature",
|
|
2654
|
+
displayField: "name",
|
|
2655
|
+
defaultSort: "name",
|
|
2656
|
+
formColumns: 2,
|
|
2657
|
+
facets: [activeFacet({ active: "Ativos", inactive: "Inativos", all: "Todos" })],
|
|
2658
|
+
fieldGroups: [
|
|
2659
|
+
{ id: "identity", label: "O contrato", columns: 1 },
|
|
2660
|
+
{
|
|
2661
|
+
id: "when",
|
|
2662
|
+
label: "Quando ele nasce",
|
|
2663
|
+
columns: 1,
|
|
2664
|
+
description: "As duas portas convivem: um or\xE7amento aprovado gera o contrato do pacote, e a execu\xE7\xE3o gera o termo daquele procedimento."
|
|
2665
|
+
},
|
|
2666
|
+
{
|
|
2667
|
+
id: "signature",
|
|
2668
|
+
label: "Assinatura",
|
|
2669
|
+
columns: 1,
|
|
2670
|
+
description: "Os dois primeiros dizem COMO se assina. O terceiro diz o que acontece se ningu\xE9m assinar \u2014 e \xE9 o \xFAnico que bloqueia."
|
|
2671
|
+
},
|
|
2672
|
+
{ ...RECORD_GROUP }
|
|
2673
|
+
],
|
|
2674
|
+
fields: [
|
|
2675
|
+
{ key: "name", label: "Nome", type: "text", required: true, showInTable: true, searchable: true, group: "identity" },
|
|
2676
|
+
// Markdown e não editor de página: quem escreve contrato precisa de
|
|
2677
|
+
// parágrafo e negrito. As variáveis ({{cliente.nome}}) são substituídas
|
|
2678
|
+
// na geração.
|
|
2679
|
+
{
|
|
2680
|
+
key: "content",
|
|
2681
|
+
label: "Texto do contrato",
|
|
2682
|
+
type: "markdown",
|
|
2683
|
+
showInTable: false,
|
|
2684
|
+
group: "identity",
|
|
2685
|
+
hint: "Use {{cliente.nome}}, {{servico.nome}} e afins \u2014 a gera\xE7\xE3o substitui."
|
|
2686
|
+
},
|
|
2687
|
+
{ key: "generateOnQuoteApproval", label: "Gerar quando um or\xE7amento for aprovado", type: "boolean", showInTable: false, defaultValue: false, group: "when" },
|
|
2688
|
+
{ key: "generateOnExecution", label: "Gerar na execu\xE7\xE3o do servi\xE7o", type: "boolean", showInTable: false, defaultValue: false, group: "when" },
|
|
2689
|
+
// Um contrato por atendimento ou um por serviço: é o que decide se a
|
|
2690
|
+
// cliente assina uma folha ou seis.
|
|
2691
|
+
{
|
|
2692
|
+
key: "groupServices",
|
|
2693
|
+
label: "Um contrato para todos os servi\xE7os do atendimento",
|
|
2694
|
+
type: "boolean",
|
|
2695
|
+
showInTable: false,
|
|
2696
|
+
defaultValue: true,
|
|
2697
|
+
group: "when",
|
|
2698
|
+
hint: "Desligado, sai um contrato por servi\xE7o."
|
|
2699
|
+
},
|
|
2700
|
+
{ key: "requireScreenSignature", label: "Assinar na tela", type: "boolean", showInTable: true, defaultValue: false, group: "signature" },
|
|
2701
|
+
{ key: "requireSelfie", label: "Pedir selfie de quem assina", type: "boolean", showInTable: false, defaultValue: false, group: "signature" },
|
|
2702
|
+
{
|
|
2703
|
+
key: "requireSignatureForExecution",
|
|
2704
|
+
label: "Sem assinatura, n\xE3o executa",
|
|
2705
|
+
type: "boolean",
|
|
2706
|
+
showInTable: true,
|
|
2707
|
+
defaultValue: false,
|
|
2708
|
+
group: "signature",
|
|
2709
|
+
hint: "O \xFAnico que bloqueia: o servi\xE7o n\xE3o pode ser executado sem o contrato assinado."
|
|
2710
|
+
},
|
|
2711
|
+
...recordRailFields({ tagsTable: "plg_forms_contract_templates", activeLabel: "Ativo" })
|
|
2712
|
+
],
|
|
2713
|
+
data: {
|
|
2714
|
+
table: "plg_forms_contract_templates",
|
|
2715
|
+
schema: "public",
|
|
2716
|
+
tenantScoped: true,
|
|
2717
|
+
searchColumns: ["name"]
|
|
2718
|
+
}
|
|
2719
|
+
};
|
|
2194
2720
|
const formRegistries = [
|
|
2721
|
+
{
|
|
2722
|
+
id: "contract-templates",
|
|
2723
|
+
entity: contractTemplateEntity,
|
|
2724
|
+
icon: "FileSignature",
|
|
2725
|
+
description: "Modelos de contrato que o cliente assina",
|
|
2726
|
+
agentHint: "O MODELO do contrato \u2014 n\xE3o \xE9 um contrato gerado nem uma assinatura."
|
|
2727
|
+
},
|
|
2195
2728
|
{
|
|
2196
2729
|
id: "form-categories",
|
|
2197
2730
|
entity: {
|
|
@@ -2224,14 +2757,14 @@ function createCustomFormsPlugin(options) {
|
|
|
2224
2757
|
}
|
|
2225
2758
|
];
|
|
2226
2759
|
const registries = [...formRegistries, ...options?.settingsRegistries ?? []];
|
|
2227
|
-
const SettingsComponent = () =>
|
|
2760
|
+
const SettingsComponent = () => React11.createElement(PluginSettingsPanel, {
|
|
2228
2761
|
title: config.labels.settingsLabel,
|
|
2229
2762
|
customTabs: [
|
|
2230
2763
|
{
|
|
2231
2764
|
id: "forms",
|
|
2232
2765
|
label: config.labels.pageTitle,
|
|
2233
2766
|
icon: "FileText",
|
|
2234
|
-
content:
|
|
2767
|
+
content: React11.createElement(CustomFormsSettingsTab, { config, provider, store })
|
|
2235
2768
|
}
|
|
2236
2769
|
],
|
|
2237
2770
|
registries,
|
|
@@ -2239,8 +2772,10 @@ function createCustomFormsPlugin(options) {
|
|
|
2239
2772
|
hostPluginId: "custom_forms"
|
|
2240
2773
|
});
|
|
2241
2774
|
SettingsComponent.displayName = "CustomFormsSettingsPanel";
|
|
2242
|
-
const PersonDocumentsWidget2 = (props) =>
|
|
2775
|
+
const PersonDocumentsWidget2 = (props) => React11.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "records" });
|
|
2243
2776
|
PersonDocumentsWidget2.displayName = "PersonDocumentsWidget";
|
|
2777
|
+
const PersonFilesWidget = (props) => React11.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "files" });
|
|
2778
|
+
PersonFilesWidget.displayName = "PersonFilesWidget";
|
|
2244
2779
|
return {
|
|
2245
2780
|
id: "custom_forms",
|
|
2246
2781
|
defaultAgentRole: "setup",
|
|
@@ -2279,6 +2814,12 @@ function createCustomFormsPlugin(options) {
|
|
|
2279
2814
|
zone: "person.detail.documents",
|
|
2280
2815
|
component: PersonDocumentsWidget2,
|
|
2281
2816
|
order: 0
|
|
2817
|
+
},
|
|
2818
|
+
{
|
|
2819
|
+
id: "person-files-tab",
|
|
2820
|
+
zone: "person.detail.files",
|
|
2821
|
+
component: PersonFilesWidget,
|
|
2822
|
+
order: 0
|
|
2282
2823
|
}
|
|
2283
2824
|
],
|
|
2284
2825
|
registries,
|