@fayz-ai/plugin-forms 0.2.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{FormBuilder-KDANEL6Z.js → FormBuilder-7SWZQKFN.js} +130 -23
- package/dist/FormBuilder-7SWZQKFN.js.map +1 -0
- package/dist/{FormBuilder-4VKYVZAC.cjs → FormBuilder-YB75LEEH.cjs} +129 -22
- package/dist/FormBuilder-YB75LEEH.cjs.map +1 -0
- package/dist/agent-tools.d.ts +4 -0
- package/dist/agent-tools.d.ts.map +1 -0
- package/dist/chunk-QM2AF3DZ.cjs +446 -0
- package/dist/chunk-QM2AF3DZ.cjs.map +1 -0
- package/dist/chunk-W2ZNJGQC.js +438 -0
- package/dist/chunk-W2ZNJGQC.js.map +1 -0
- package/dist/components/DocumentFormDialog.d.ts +3 -1
- package/dist/components/DocumentFormDialog.d.ts.map +1 -1
- package/dist/components/DocumentList.d.ts.map +1 -1
- package/dist/components/FieldConfigDialog.d.ts.map +1 -1
- package/dist/components/FormBuilder.d.ts.map +1 -1
- package/dist/components/FormViewer.d.ts.map +1 -1
- package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +7 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/index.cjs +168 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +168 -72
- package/dist/index.js.map +1 -1
- package/dist/lib/person-fields.d.ts +16 -0
- package/dist/lib/person-fields.d.ts.map +1 -0
- package/dist/lib/print.d.ts +17 -0
- package/dist/lib/print.d.ts.map +1 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +9 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/types.d.ts +15 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
- package/dist/views/TemplateListView.d.ts.map +1 -1
- package/package.json +10 -4
- package/src/agent-tools.ts +73 -0
- package/src/components/DocumentFormDialog.tsx +37 -16
- package/src/components/DocumentList.tsx +12 -8
- package/src/components/FieldConfigDialog.tsx +40 -0
- package/src/components/FormBuilder.tsx +104 -22
- package/src/components/FormViewer.tsx +11 -1
- package/src/components/PersonDocumentsWidget.tsx +10 -1
- package/src/data/supabase.ts +22 -19
- package/src/data/tables.ts +7 -0
- package/src/index.ts +23 -1
- package/src/lib/person-fields.ts +67 -0
- package/src/lib/print.ts +207 -0
- package/src/lib/tenant.ts +6 -2
- package/src/locales/en.ts +20 -0
- package/src/locales/pt-BR.ts +20 -0
- package/src/migrations/000_plg_rename.sql +21 -0
- package/src/migrations/001_frm_base.sql +92 -56
- package/src/migrations/002_document_archetype.sql +130 -42
- package/src/migrations/003_agent_rpcs.sql +174 -0
- package/src/migrations/index.ts +610 -0
- package/src/types.ts +18 -4
- package/src/views/CustomFormsSettingsTab.tsx +23 -16
- package/src/views/TemplateListView.tsx +29 -18
- package/dist/FormBuilder-4VKYVZAC.cjs.map +0 -1
- package/dist/FormBuilder-KDANEL6Z.js.map +0 -1
- package/dist/chunk-32I43T37.js +0 -198
- package/dist/chunk-32I43T37.js.map +0 -1
- package/dist/chunk-XLUULIVL.cjs +0 -200
- package/dist/chunk-XLUULIVL.cjs.map +0 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAUnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAA;AA0CxD,YAAY,EAAE,wBAAwB,EAAE,CAAA;AACxC,OAAO,EAAE,4BAA4B,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAEnH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAiL1F"}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { FormRenderer } from './chunk-
|
|
1
|
+
import { createOrUpdateFormTool, prefillFromPerson, FormRenderer, printDocument } from './chunk-W2ZNJGQC.js';
|
|
2
2
|
import React2, { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
3
|
import { registerTranslations, createSafeDataProvider, useTranslation, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
|
|
4
|
-
import { PluginSettingsPanel } from '@fayz-ai/saas';
|
|
4
|
+
import { PluginSettingsPanel, useLimitGuard, invalidateLimit, PermissionGate } from '@fayz-ai/saas';
|
|
5
5
|
import { create } from 'zustand';
|
|
6
|
-
import { FileText, Image, Paperclip, Search, Plus, Pencil,
|
|
6
|
+
import { FileText, Image, Paperclip, Search, Plus, Pencil, Archive, ArrowLeft, Printer, ChevronDown, Upload, Eye } from 'lucide-react';
|
|
7
7
|
import { Input, Button, Card, CardContent, Badge, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, DropdownItem } from '@fayz-ai/ui';
|
|
8
8
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
9
9
|
|
|
10
|
+
// src/data/tables.ts
|
|
11
|
+
var T = {
|
|
12
|
+
documents: "plg_forms_documents",
|
|
13
|
+
templates: "plg_forms_templates",
|
|
14
|
+
documentFiles: "plg_forms_document_files",
|
|
15
|
+
categories: "plg_forms_categories"
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
// src/data/mock.ts
|
|
11
19
|
function uuid() {
|
|
12
20
|
return crypto.randomUUID();
|
|
@@ -170,14 +178,11 @@ function createMockFormsProvider() {
|
|
|
170
178
|
}
|
|
171
179
|
};
|
|
172
180
|
}
|
|
173
|
-
|
|
174
|
-
// src/lib/tenant.ts
|
|
175
|
-
var currentTenantId;
|
|
176
181
|
function getFormsTenantId() {
|
|
177
|
-
return
|
|
182
|
+
return getActiveTenantId();
|
|
178
183
|
}
|
|
179
184
|
function getTenantId() {
|
|
180
|
-
return getActiveTenantId() ?? "";
|
|
185
|
+
return getFormsTenantId() ?? getActiveTenantId() ?? "";
|
|
181
186
|
}
|
|
182
187
|
function getClient() {
|
|
183
188
|
const supabase = getSupabaseClientOptional();
|
|
@@ -255,7 +260,7 @@ function createSupabaseFormsProvider() {
|
|
|
255
260
|
// ── Templates ──────────────────────────────────────────
|
|
256
261
|
async getTemplates(query) {
|
|
257
262
|
const db = getClient();
|
|
258
|
-
let qb = db.from(
|
|
263
|
+
let qb = db.from(T.templates).select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_current", true).eq("is_deleted", false).eq("is_active", true);
|
|
259
264
|
if (query.category) qb = qb.eq("category", query.category);
|
|
260
265
|
if (query.search) qb = qb.ilike("name", `%${query.search}%`);
|
|
261
266
|
const page = query.page ?? 1;
|
|
@@ -267,14 +272,14 @@ function createSupabaseFormsProvider() {
|
|
|
267
272
|
},
|
|
268
273
|
async getTemplateById(id) {
|
|
269
274
|
const db = getClient();
|
|
270
|
-
const { data, error } = await db.from(
|
|
275
|
+
const { data, error } = await db.from(T.templates).select("*").eq("id", id).eq("is_deleted", false).single();
|
|
271
276
|
if (error || !data) return null;
|
|
272
277
|
return mapTemplate(data);
|
|
273
278
|
},
|
|
274
279
|
async createTemplate(input) {
|
|
275
280
|
const db = getClient();
|
|
276
281
|
const tenantId = getTenantId();
|
|
277
|
-
const { data, error } = await db.from(
|
|
282
|
+
const { data, error } = await db.from(T.templates).insert({
|
|
278
283
|
tenant_id: tenantId,
|
|
279
284
|
name: input.name,
|
|
280
285
|
description: input.description,
|
|
@@ -290,12 +295,12 @@ function createSupabaseFormsProvider() {
|
|
|
290
295
|
async updateTemplate(id, input) {
|
|
291
296
|
const db = getClient();
|
|
292
297
|
const tenantId = getTenantId();
|
|
293
|
-
const { count } = await db.from(
|
|
298
|
+
const { count } = await db.from(T.documents).select("document_id", { count: "exact", head: true }).eq("template_id", id);
|
|
294
299
|
const hasDocuments = (count ?? 0) > 0;
|
|
295
300
|
if (hasDocuments && input.schema) {
|
|
296
301
|
const existing = await this.getTemplateById(id);
|
|
297
302
|
if (!existing) throw new Error("Template not found");
|
|
298
|
-
const { data: newRow, error: insertError } = await db.from(
|
|
303
|
+
const { data: newRow, error: insertError } = await db.from(T.templates).insert({
|
|
299
304
|
tenant_id: tenantId,
|
|
300
305
|
name: input.name ?? existing.name,
|
|
301
306
|
description: input.description ?? existing.description,
|
|
@@ -310,7 +315,7 @@ function createSupabaseFormsProvider() {
|
|
|
310
315
|
parent_id: existing.parentId ?? existing.id
|
|
311
316
|
}).select().single();
|
|
312
317
|
if (insertError) throw new Error(insertError.message);
|
|
313
|
-
await db.from(
|
|
318
|
+
await db.from(T.templates).update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
314
319
|
return mapTemplate(newRow);
|
|
315
320
|
}
|
|
316
321
|
const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -322,16 +327,16 @@ function createSupabaseFormsProvider() {
|
|
|
322
327
|
if (input.tags !== void 0) updates.tags = input.tags;
|
|
323
328
|
if (input.metadata !== void 0) updates.metadata = input.metadata;
|
|
324
329
|
if (input.isActive !== void 0) updates.is_active = input.isActive;
|
|
325
|
-
const { data, error } = await db.from(
|
|
330
|
+
const { data, error } = await db.from(T.templates).update(updates).eq("id", id).select().single();
|
|
326
331
|
if (error) throw new Error(error.message);
|
|
327
332
|
return mapTemplate(data);
|
|
328
333
|
},
|
|
329
334
|
async deleteTemplate(id) {
|
|
330
335
|
const db = getClient();
|
|
331
|
-
const { error } = await db.from(
|
|
336
|
+
const { error } = await db.from(T.templates).update({ is_deleted: true, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
332
337
|
if (error) throw new Error(error.message);
|
|
333
338
|
},
|
|
334
|
-
// ── Documents (
|
|
339
|
+
// ── Documents (public.documents + plg_forms_documents extension) ──
|
|
335
340
|
async getDocuments(query) {
|
|
336
341
|
const db = getClient();
|
|
337
342
|
let qb = db.from("v_documents").select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_active", true);
|
|
@@ -358,7 +363,7 @@ function createSupabaseFormsProvider() {
|
|
|
358
363
|
async createDocument(input) {
|
|
359
364
|
const tenantId = getTenantId();
|
|
360
365
|
const isForm = !!input.templateId;
|
|
361
|
-
const { data: coreDoc, error: coreError } = await getClient().from("
|
|
366
|
+
const { data: coreDoc, error: coreError } = await getClient().from("documents").insert({
|
|
362
367
|
tenant_id: tenantId,
|
|
363
368
|
kind: input.kind ?? (isForm ? "form" : "attachment"),
|
|
364
369
|
person_id: input.personId,
|
|
@@ -373,14 +378,14 @@ function createSupabaseFormsProvider() {
|
|
|
373
378
|
if (coreError) throw new Error(coreError.message);
|
|
374
379
|
if (isForm && input.templateId) {
|
|
375
380
|
const db = getClient();
|
|
376
|
-
const { error: extError } = await db.from(
|
|
381
|
+
const { error: extError } = await db.from(T.documents).insert({
|
|
377
382
|
document_id: coreDoc.id,
|
|
378
383
|
tenant_id: tenantId,
|
|
379
384
|
template_id: input.templateId,
|
|
380
385
|
data: input.data ?? {}
|
|
381
386
|
});
|
|
382
387
|
if (extError) {
|
|
383
|
-
await getClient().from("
|
|
388
|
+
await getClient().from("documents").delete().eq("id", coreDoc.id);
|
|
384
389
|
throw new Error(extError.message);
|
|
385
390
|
}
|
|
386
391
|
}
|
|
@@ -393,23 +398,23 @@ function createSupabaseFormsProvider() {
|
|
|
393
398
|
if (input.status !== void 0) coreUpdates.status = input.status;
|
|
394
399
|
if (input.title !== void 0) coreUpdates.title = input.title;
|
|
395
400
|
if (input.notes !== void 0) coreUpdates.notes = input.notes;
|
|
396
|
-
const { error: coreError } = await getClient().from("
|
|
401
|
+
const { error: coreError } = await getClient().from("documents").update(coreUpdates).eq("id", id);
|
|
397
402
|
if (coreError) throw new Error(coreError.message);
|
|
398
403
|
if (input.data !== void 0) {
|
|
399
404
|
const db = getClient();
|
|
400
|
-
await db.from(
|
|
405
|
+
await db.from(T.documents).update({ data: input.data, updated_at: now2 }).eq("document_id", id);
|
|
401
406
|
}
|
|
402
407
|
const doc = await this.getDocumentById(id);
|
|
403
408
|
return doc;
|
|
404
409
|
},
|
|
405
410
|
async deleteDocument(id) {
|
|
406
|
-
const { error } = await getClient().from("
|
|
411
|
+
const { error } = await getClient().from("documents").update({ is_active: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
407
412
|
if (error) throw new Error(error.message);
|
|
408
413
|
},
|
|
409
414
|
// ── Files ──────────────────────────────────────────────
|
|
410
415
|
async getDocumentFiles(documentId) {
|
|
411
416
|
const db = getClient();
|
|
412
|
-
const { data, error } = await db.from(
|
|
417
|
+
const { data, error } = await db.from(T.documentFiles).select("*").eq("document_id", documentId).order("sort_order");
|
|
413
418
|
if (error) throw new Error(error.message);
|
|
414
419
|
return (data ?? []).map(mapFile);
|
|
415
420
|
},
|
|
@@ -421,7 +426,7 @@ function createSupabaseFormsProvider() {
|
|
|
421
426
|
const { error: uploadError } = await db.storage.from("documents").upload(storagePath, file, { contentType: file.type });
|
|
422
427
|
if (uploadError) throw new Error(uploadError.message);
|
|
423
428
|
const { data: urlData } = db.storage.from("documents").getPublicUrl(storagePath);
|
|
424
|
-
const { data, error } = await db.from(
|
|
429
|
+
const { data, error } = await db.from(T.documentFiles).insert({
|
|
425
430
|
tenant_id: tenantId,
|
|
426
431
|
document_id: documentId,
|
|
427
432
|
field_key: fieldKey,
|
|
@@ -435,7 +440,7 @@ function createSupabaseFormsProvider() {
|
|
|
435
440
|
},
|
|
436
441
|
async deleteFile(fileId) {
|
|
437
442
|
const db = getClient();
|
|
438
|
-
const { error } = await db.from(
|
|
443
|
+
const { error } = await db.from(T.documentFiles).delete().eq("id", fileId);
|
|
439
444
|
if (error) throw new Error(error.message);
|
|
440
445
|
}
|
|
441
446
|
};
|
|
@@ -561,6 +566,7 @@ var en = {
|
|
|
561
566
|
"customForms.noTemplates": "No form templates yet",
|
|
562
567
|
"customForms.noTemplatesDescription": "Create your first custom form to start collecting structured data.",
|
|
563
568
|
"customForms.deleteTemplateConfirm": "Delete this form template? Existing documents will be preserved.",
|
|
569
|
+
"customForms.archiveTemplateConfirm": "Archive this form template? It leaves the list and the document picker, but is kept and existing documents are preserved.",
|
|
564
570
|
// Categories
|
|
565
571
|
"customForms.category.anamnesis": "Anamnesis",
|
|
566
572
|
"customForms.category.evolution": "Evolution Note",
|
|
@@ -582,6 +588,21 @@ var en = {
|
|
|
582
588
|
"customForms.builder.addOption": "Add option",
|
|
583
589
|
"customForms.builder.colSpan": "Column span",
|
|
584
590
|
"customForms.builder.deleteField": "Remove field",
|
|
591
|
+
"customForms.builder.mapToPerson": "Auto-fill from record",
|
|
592
|
+
"customForms.builder.mapNone": "Don't auto-fill",
|
|
593
|
+
"customForms.builder.mapHint": "When creating the document from a person's profile, this field is pre-filled with the chosen value (still editable).",
|
|
594
|
+
"customForms.preview.editor": "Editor",
|
|
595
|
+
"customForms.preview.print": "Print",
|
|
596
|
+
// Person field mappings (auto-fill)
|
|
597
|
+
"customForms.personField.name": "Name",
|
|
598
|
+
"customForms.personField.documentNumber": "ID / Document",
|
|
599
|
+
"customForms.personField.email": "Email",
|
|
600
|
+
"customForms.personField.phone": "Phone",
|
|
601
|
+
"customForms.personField.dateOfBirth": "Date of Birth",
|
|
602
|
+
"customForms.personField.guardianName": "Guardian",
|
|
603
|
+
"customForms.personField.address": "Address",
|
|
604
|
+
"customForms.personField.city": "City",
|
|
605
|
+
"customForms.personField.state": "State",
|
|
585
606
|
// Field types
|
|
586
607
|
"customForms.fieldType.title": "Title",
|
|
587
608
|
"customForms.fieldType.text": "Simple Text",
|
|
@@ -605,6 +626,9 @@ var en = {
|
|
|
605
626
|
"customForms.saveAsDraft": "Save as Draft",
|
|
606
627
|
"customForms.saveAndComplete": "Save & Complete",
|
|
607
628
|
"customForms.deleteDocumentConfirm": "Delete this document?",
|
|
629
|
+
"customForms.archive": "Archive",
|
|
630
|
+
"customForms.archiveDocumentConfirm": "Archive this document? It leaves the active list but is kept on file.",
|
|
631
|
+
"customForms.printPdf": "Print / PDF",
|
|
608
632
|
// Registry
|
|
609
633
|
"registry.form-categories": "Categories",
|
|
610
634
|
"registry.form-categories.description": "Categories for organizing form templates",
|
|
@@ -631,6 +655,7 @@ var ptBR = {
|
|
|
631
655
|
"customForms.noTemplates": "Nenhum modelo criado",
|
|
632
656
|
"customForms.noTemplatesDescription": "Crie seu primeiro formul\xE1rio personalizado para come\xE7ar a coletar dados estruturados.",
|
|
633
657
|
"customForms.deleteTemplateConfirm": "Excluir este modelo de formul\xE1rio? Documentos existentes ser\xE3o preservados.",
|
|
658
|
+
"customForms.archiveTemplateConfirm": "Arquivar este modelo? Ele sai da lista e do seletor de documentos, mas fica guardado e os documentos existentes s\xE3o preservados.",
|
|
634
659
|
// Categories
|
|
635
660
|
"customForms.category.anamnesis": "Anamnese",
|
|
636
661
|
"customForms.category.evolution": "Evolu\xE7\xE3o",
|
|
@@ -652,6 +677,21 @@ var ptBR = {
|
|
|
652
677
|
"customForms.builder.addOption": "Adicionar op\xE7\xE3o",
|
|
653
678
|
"customForms.builder.colSpan": "Largura em colunas",
|
|
654
679
|
"customForms.builder.deleteField": "Remover campo",
|
|
680
|
+
"customForms.builder.mapToPerson": "Preencher com dado da ficha",
|
|
681
|
+
"customForms.builder.mapNone": "N\xE3o preencher",
|
|
682
|
+
"customForms.builder.mapHint": "Ao criar o documento na ficha da pessoa, este campo j\xE1 vem preenchido com o dado escolhido (edit\xE1vel).",
|
|
683
|
+
"customForms.preview.editor": "Editor",
|
|
684
|
+
"customForms.preview.print": "Impress\xE3o",
|
|
685
|
+
// Person field mappings (auto-fill)
|
|
686
|
+
"customForms.personField.name": "Nome",
|
|
687
|
+
"customForms.personField.documentNumber": "CPF / Documento",
|
|
688
|
+
"customForms.personField.email": "E-mail",
|
|
689
|
+
"customForms.personField.phone": "Telefone",
|
|
690
|
+
"customForms.personField.dateOfBirth": "Data de Nascimento",
|
|
691
|
+
"customForms.personField.guardianName": "Respons\xE1vel",
|
|
692
|
+
"customForms.personField.address": "Endere\xE7o",
|
|
693
|
+
"customForms.personField.city": "Cidade",
|
|
694
|
+
"customForms.personField.state": "Estado",
|
|
655
695
|
// Field types
|
|
656
696
|
"customForms.fieldType.title": "T\xEDtulo",
|
|
657
697
|
"customForms.fieldType.text": "Texto Simples",
|
|
@@ -675,6 +715,9 @@ var ptBR = {
|
|
|
675
715
|
"customForms.saveAsDraft": "Salvar Rascunho",
|
|
676
716
|
"customForms.saveAndComplete": "Salvar e Finalizar",
|
|
677
717
|
"customForms.deleteDocumentConfirm": "Excluir este documento?",
|
|
718
|
+
"customForms.archive": "Arquivar",
|
|
719
|
+
"customForms.archiveDocumentConfirm": "Arquivar este documento? Ele sai da lista ativa mas fica guardado.",
|
|
720
|
+
"customForms.printPdf": "Imprimir / PDF",
|
|
678
721
|
// Registry
|
|
679
722
|
"registry.form-categories": "Categorias",
|
|
680
723
|
"registry.form-categories.description": "Categorias para organizar modelos de formul\xE1rios",
|
|
@@ -720,12 +763,17 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
720
763
|
const loading = store((s) => s.templatesLoading);
|
|
721
764
|
const orgId = getFormsTenantId();
|
|
722
765
|
useEffect(() => {
|
|
723
|
-
return;
|
|
766
|
+
if (!orgId) return;
|
|
767
|
+
const timer = setTimeout(() => {
|
|
768
|
+
store.getState().fetchTemplates({ search: search || void 0 });
|
|
769
|
+
}, 50);
|
|
770
|
+
return () => clearTimeout(timer);
|
|
724
771
|
}, [orgId, search]);
|
|
725
|
-
const
|
|
726
|
-
if (!window.confirm(t("customForms.
|
|
727
|
-
await store.getState().
|
|
772
|
+
const handleArchive = useCallback(async (template) => {
|
|
773
|
+
if (!window.confirm(t("customForms.archiveTemplateConfirm"))) return;
|
|
774
|
+
await store.getState().updateTemplate(template.id, { isActive: false });
|
|
728
775
|
}, [store, t]);
|
|
776
|
+
const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
|
|
729
777
|
const categoryLabel = (cat) => t(`customForms.category.${cat}`);
|
|
730
778
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
731
779
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
@@ -741,21 +789,21 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
741
789
|
}
|
|
742
790
|
)
|
|
743
791
|
] }),
|
|
744
|
-
/* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "h-9", children: [
|
|
792
|
+
/* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "h-9", children: [
|
|
745
793
|
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
|
|
746
794
|
t("customForms.newTemplate")
|
|
747
|
-
] })
|
|
795
|
+
] }) })
|
|
748
796
|
] }),
|
|
749
|
-
!loading &&
|
|
797
|
+
!loading && visibleTemplates.length === 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-center justify-center py-16 text-center", children: [
|
|
750
798
|
/* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4", children: /* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 text-muted-foreground" }) }),
|
|
751
799
|
/* @__PURE__ */ jsx("p", { className: "font-medium", children: t("customForms.noTemplates") }),
|
|
752
800
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-sm", children: t("customForms.noTemplatesDescription") }),
|
|
753
|
-
/* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
|
|
801
|
+
/* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
|
|
754
802
|
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
|
|
755
803
|
t("customForms.newTemplate")
|
|
756
|
-
] })
|
|
804
|
+
] }) })
|
|
757
805
|
] }) }),
|
|
758
|
-
|
|
806
|
+
visibleTemplates.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: visibleTemplates.map((template) => /* @__PURE__ */ jsx(
|
|
759
807
|
Card,
|
|
760
808
|
{
|
|
761
809
|
className: "group cursor-pointer hover:shadow-md transition-shadow",
|
|
@@ -783,10 +831,11 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
783
831
|
{
|
|
784
832
|
onClick: (e) => {
|
|
785
833
|
e.stopPropagation();
|
|
786
|
-
|
|
834
|
+
handleArchive(template);
|
|
787
835
|
},
|
|
788
|
-
className: "p-1.5 rounded-md hover:bg-
|
|
789
|
-
|
|
836
|
+
className: "p-1.5 rounded-md hover:bg-muted",
|
|
837
|
+
title: t("customForms.archive"),
|
|
838
|
+
children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
790
839
|
}
|
|
791
840
|
)
|
|
792
841
|
] })
|
|
@@ -817,29 +866,29 @@ function TemplateListView({ store, config, onEdit, onNew }) {
|
|
|
817
866
|
] }) }, i)) })
|
|
818
867
|
] });
|
|
819
868
|
}
|
|
869
|
+
var ROUTE_BASE = "/settings/custom_forms";
|
|
870
|
+
function readBuilderIdFromHash() {
|
|
871
|
+
const hash = window.location.hash.slice(1) || "/";
|
|
872
|
+
const m = hash.match(/\/settings\/custom_forms\/templates\/([^/]+)/);
|
|
873
|
+
if (!m) return null;
|
|
874
|
+
return m[1] === "new" ? "__new__" : m[1];
|
|
875
|
+
}
|
|
820
876
|
function CustomFormsSettingsTab({ config, provider, store }) {
|
|
821
|
-
const [builderTemplateId, setBuilderTemplateId] = useState(
|
|
877
|
+
const [builderTemplateId, setBuilderTemplateId] = useState(readBuilderIdFromHash);
|
|
822
878
|
const handleNew = useCallback(() => {
|
|
823
|
-
|
|
879
|
+
window.location.hash = `#${ROUTE_BASE}/templates/new`;
|
|
824
880
|
}, []);
|
|
825
881
|
const handleEdit = useCallback((template) => {
|
|
826
|
-
|
|
882
|
+
window.location.hash = `#${ROUTE_BASE}/templates/${template.id}`;
|
|
827
883
|
}, []);
|
|
828
884
|
const handleBackFromBuilder = useCallback(() => {
|
|
829
|
-
|
|
885
|
+
window.location.hash = `#${ROUTE_BASE}/forms`;
|
|
830
886
|
store.getState().fetchTemplates();
|
|
831
887
|
}, [store]);
|
|
832
888
|
React2.useEffect(() => {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (builderMatch && builderMatch[1] !== "new") {
|
|
837
|
-
setBuilderTemplateId(builderMatch[1]);
|
|
838
|
-
} else if (builderMatch && builderMatch[1] === "new") {
|
|
839
|
-
setBuilderTemplateId("__new__");
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
checkHash();
|
|
889
|
+
const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
|
|
890
|
+
window.addEventListener("hashchange", sync);
|
|
891
|
+
return () => window.removeEventListener("hashchange", sync);
|
|
843
892
|
}, []);
|
|
844
893
|
return builderTemplateId ? /* @__PURE__ */ jsx(
|
|
845
894
|
BuilderWrapper,
|
|
@@ -868,7 +917,7 @@ function BuilderWrapper({
|
|
|
868
917
|
onBack
|
|
869
918
|
}) {
|
|
870
919
|
const FormBuilderLazy = React2.lazy(
|
|
871
|
-
() => import('./FormBuilder-
|
|
920
|
+
() => import('./FormBuilder-7SWZQKFN.js').then((m) => ({ default: m.FormBuilder }))
|
|
872
921
|
);
|
|
873
922
|
return /* @__PURE__ */ jsx(
|
|
874
923
|
React2.Suspense,
|
|
@@ -934,7 +983,7 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
|
|
|
934
983
|
setLoading(true);
|
|
935
984
|
try {
|
|
936
985
|
const result = await provider.getDocuments({ personId });
|
|
937
|
-
setDocuments(result.data);
|
|
986
|
+
setDocuments(result.data.filter((d) => d.status !== "archived"));
|
|
938
987
|
} finally {
|
|
939
988
|
setLoading(false);
|
|
940
989
|
}
|
|
@@ -947,9 +996,9 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
|
|
|
947
996
|
const unsub = store.subscribe(() => fetchDocs());
|
|
948
997
|
return unsub;
|
|
949
998
|
}, [store, fetchDocs]);
|
|
950
|
-
const
|
|
951
|
-
if (!window.confirm(t("customForms.
|
|
952
|
-
await provider.
|
|
999
|
+
const handleArchive = useCallback(async (doc) => {
|
|
1000
|
+
if (!window.confirm(t("customForms.archiveDocumentConfirm"))) return;
|
|
1001
|
+
await provider.updateDocument(doc.id, { status: "archived" });
|
|
953
1002
|
setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
|
|
954
1003
|
}, [provider, t]);
|
|
955
1004
|
const handleDragEnter = useCallback((e) => {
|
|
@@ -1047,10 +1096,11 @@ function DocumentList({ personId, provider, store, onView, onFileDrop }) {
|
|
|
1047
1096
|
{
|
|
1048
1097
|
onClick: (e) => {
|
|
1049
1098
|
e.stopPropagation();
|
|
1050
|
-
|
|
1099
|
+
handleArchive(doc);
|
|
1051
1100
|
},
|
|
1052
|
-
className: "p-1.5 rounded-md hover:bg-
|
|
1053
|
-
|
|
1101
|
+
className: "p-1.5 rounded-md hover:bg-muted",
|
|
1102
|
+
title: t("customForms.archive"),
|
|
1103
|
+
children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
|
|
1054
1104
|
}
|
|
1055
1105
|
)
|
|
1056
1106
|
] })
|
|
@@ -1073,23 +1123,34 @@ function DocumentFormDialog({
|
|
|
1073
1123
|
existingDocument,
|
|
1074
1124
|
personId,
|
|
1075
1125
|
personName,
|
|
1126
|
+
person,
|
|
1076
1127
|
provider,
|
|
1077
1128
|
store,
|
|
1078
1129
|
onSaved,
|
|
1079
1130
|
onBack
|
|
1080
1131
|
}) {
|
|
1081
1132
|
const t = useTranslation();
|
|
1133
|
+
const guardDocuments = useLimitGuard("documents_month");
|
|
1082
1134
|
const [template, setTemplate] = useState(initialTemplate);
|
|
1083
|
-
const [data, setData] = useState(
|
|
1135
|
+
const [data, setData] = useState(
|
|
1136
|
+
() => existingDocument ? existingDocument.data : prefillFromPerson(initialTemplate?.schema, person)
|
|
1137
|
+
);
|
|
1084
1138
|
const [saving, setSaving] = useState(false);
|
|
1085
1139
|
useEffect(() => {
|
|
1086
1140
|
if (existingDocument && !template) {
|
|
1087
1141
|
provider.getTemplateById(existingDocument.templateId).then(setTemplate);
|
|
1088
1142
|
}
|
|
1089
1143
|
}, [existingDocument, template, provider]);
|
|
1144
|
+
const prefilled = React2.useRef(!!initialTemplate || !!existingDocument);
|
|
1145
|
+
useEffect(() => {
|
|
1146
|
+
if (prefilled.current || !template || existingDocument) return;
|
|
1147
|
+
prefilled.current = true;
|
|
1148
|
+
setData((prev) => prefillFromPerson(template.schema, person, prev));
|
|
1149
|
+
}, [template, existingDocument, person]);
|
|
1090
1150
|
const handleSave = useCallback(
|
|
1091
1151
|
async (status) => {
|
|
1092
1152
|
if (!template) return;
|
|
1153
|
+
if (!existingDocument && await guardDocuments() === "blocked") return;
|
|
1093
1154
|
setSaving(true);
|
|
1094
1155
|
try {
|
|
1095
1156
|
const autoTitle = generateTitle(template.name, personName);
|
|
@@ -1103,18 +1164,21 @@ function DocumentFormDialog({
|
|
|
1103
1164
|
data,
|
|
1104
1165
|
status
|
|
1105
1166
|
});
|
|
1167
|
+
invalidateLimit("documents_month");
|
|
1106
1168
|
}
|
|
1107
1169
|
onSaved();
|
|
1108
1170
|
} finally {
|
|
1109
1171
|
setSaving(false);
|
|
1110
1172
|
}
|
|
1111
1173
|
},
|
|
1112
|
-
[template, existingDocument, data, personId, personName, store, onSaved]
|
|
1174
|
+
[template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
|
|
1113
1175
|
);
|
|
1114
1176
|
const snapshot = React2.useRef(null);
|
|
1115
1177
|
useEffect(() => {
|
|
1116
|
-
if (template && snapshot.current === null)
|
|
1117
|
-
|
|
1178
|
+
if (template && snapshot.current === null) {
|
|
1179
|
+
snapshot.current = JSON.stringify(existingDocument?.data ?? {});
|
|
1180
|
+
}
|
|
1181
|
+
}, [template, existingDocument]);
|
|
1118
1182
|
const dirty = snapshot.current !== null && JSON.stringify(data) !== snapshot.current;
|
|
1119
1183
|
useSaveBar({
|
|
1120
1184
|
dirty,
|
|
@@ -1156,11 +1220,7 @@ function DocumentFormDialog({
|
|
|
1156
1220
|
data,
|
|
1157
1221
|
onChange: setData
|
|
1158
1222
|
}
|
|
1159
|
-
) })
|
|
1160
|
-
/* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => handleSave("draft"), disabled: saving, children: [
|
|
1161
|
-
saving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 mr-1.5 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3.5 w-3.5 mr-1.5" }),
|
|
1162
|
-
t("customForms.saveAsDraft")
|
|
1163
|
-
] }) })
|
|
1223
|
+
) })
|
|
1164
1224
|
] });
|
|
1165
1225
|
}
|
|
1166
1226
|
var STATUS_COLORS2 = {
|
|
@@ -1197,6 +1257,19 @@ function FormViewer({ document: doc, provider, onBack, onEdit }) {
|
|
|
1197
1257
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: new Date(doc.createdAt).toLocaleDateString() })
|
|
1198
1258
|
] }),
|
|
1199
1259
|
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${STATUS_COLORS2[doc.status] ?? ""}`, children: t(`customForms.status.${doc.status}`) }),
|
|
1260
|
+
/* @__PURE__ */ jsxs(
|
|
1261
|
+
Button,
|
|
1262
|
+
{
|
|
1263
|
+
variant: "outline",
|
|
1264
|
+
size: "sm",
|
|
1265
|
+
className: "h-8",
|
|
1266
|
+
onClick: () => printDocument(template, doc),
|
|
1267
|
+
children: [
|
|
1268
|
+
/* @__PURE__ */ jsx(Printer, { className: "h-3 w-3 mr-1" }),
|
|
1269
|
+
t("customForms.printPdf")
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
),
|
|
1200
1273
|
onEdit && doc.status === "draft" && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => onEdit(doc), children: [
|
|
1201
1274
|
/* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 mr-1" }),
|
|
1202
1275
|
t("common.edit")
|
|
@@ -1280,6 +1353,7 @@ function AddDocumentDropdown({ personId, onSelect }) {
|
|
|
1280
1353
|
}
|
|
1281
1354
|
function PersonDocumentsWidget({ item, config, provider, store }) {
|
|
1282
1355
|
const t = useTranslation();
|
|
1356
|
+
const guardDocuments = useLimitGuard("documents_month");
|
|
1283
1357
|
const [view, setView] = useState({ type: "list" });
|
|
1284
1358
|
const fileInputRef = useRef(null);
|
|
1285
1359
|
const [pendingFileAccept, setPendingFileAccept] = useState("*/*");
|
|
@@ -1304,6 +1378,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
|
|
|
1304
1378
|
}
|
|
1305
1379
|
}, [provider]);
|
|
1306
1380
|
const handleFiles = useCallback(async (files) => {
|
|
1381
|
+
if (await guardDocuments(files.length) === "blocked") return;
|
|
1307
1382
|
for (const file of files) {
|
|
1308
1383
|
const isImage = file.type.startsWith("image/");
|
|
1309
1384
|
await store.getState().createDocument({
|
|
@@ -1316,7 +1391,8 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
|
|
|
1316
1391
|
status: "completed"
|
|
1317
1392
|
});
|
|
1318
1393
|
}
|
|
1319
|
-
|
|
1394
|
+
invalidateLimit("documents_month");
|
|
1395
|
+
}, [store, item.id, guardDocuments]);
|
|
1320
1396
|
const handleFileInputChange = useCallback((e) => {
|
|
1321
1397
|
const files = Array.from(e.target.files ?? []);
|
|
1322
1398
|
if (files.length > 0) handleFiles(files);
|
|
@@ -1341,6 +1417,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
|
|
|
1341
1417
|
template: view.template,
|
|
1342
1418
|
personId: item.id,
|
|
1343
1419
|
personName: item.name,
|
|
1420
|
+
person: item,
|
|
1344
1421
|
provider,
|
|
1345
1422
|
store,
|
|
1346
1423
|
onSaved: handleSaved,
|
|
@@ -1372,6 +1449,7 @@ function PersonDocumentsWidget({ item, config, provider, store }) {
|
|
|
1372
1449
|
existingDocument: view.document,
|
|
1373
1450
|
personId: item.id,
|
|
1374
1451
|
personName: item.name,
|
|
1452
|
+
person: item,
|
|
1375
1453
|
provider,
|
|
1376
1454
|
store,
|
|
1377
1455
|
onSaved: handleSaved,
|
|
@@ -1493,7 +1571,7 @@ function createCustomFormsPlugin(options) {
|
|
|
1493
1571
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
|
|
1494
1572
|
],
|
|
1495
1573
|
data: {
|
|
1496
|
-
table:
|
|
1574
|
+
table: T.categories,
|
|
1497
1575
|
tenantScoped: true
|
|
1498
1576
|
}
|
|
1499
1577
|
},
|
|
@@ -1512,6 +1590,9 @@ function createCustomFormsPlugin(options) {
|
|
|
1512
1590
|
const SettingsComponent = () => React2.createElement(PluginSettingsPanel, {
|
|
1513
1591
|
title: config.labels.settingsLabel,
|
|
1514
1592
|
subtitle: config.labels.settingsSubtitle,
|
|
1593
|
+
// The settings shell already renders the section label as the page H1;
|
|
1594
|
+
// hide the panel's own duplicate title (keeps the subtitle).
|
|
1595
|
+
hideTitle: true,
|
|
1515
1596
|
customTabs: [
|
|
1516
1597
|
{
|
|
1517
1598
|
id: "forms",
|
|
@@ -1539,6 +1620,10 @@ function createCustomFormsPlugin(options) {
|
|
|
1539
1620
|
declaredFeatures: [
|
|
1540
1621
|
{ id: "custom_forms", label: config.labels.pageTitle, group: config.labels.pageTitle }
|
|
1541
1622
|
],
|
|
1623
|
+
declaredLimits: [
|
|
1624
|
+
{ key: "form_templates", label: "Form templates", table: "plg_forms_templates" },
|
|
1625
|
+
{ key: "documents_month", label: "Documents / month", table: "documents", period: "month" }
|
|
1626
|
+
],
|
|
1542
1627
|
navigation: [],
|
|
1543
1628
|
routes: [],
|
|
1544
1629
|
settings: [
|
|
@@ -1603,6 +1688,17 @@ function createCustomFormsPlugin(options) {
|
|
|
1603
1688
|
{ label: "Show this client's documents" }
|
|
1604
1689
|
],
|
|
1605
1690
|
permission: { feature: "custom_forms", action: "read" }
|
|
1691
|
+
},
|
|
1692
|
+
// Server-plane write: builds/edits a form template via the pool RPC, so
|
|
1693
|
+
// the assistant can montar formulários from any page or channel.
|
|
1694
|
+
createOrUpdateFormTool
|
|
1695
|
+
],
|
|
1696
|
+
declaredRpcs: [
|
|
1697
|
+
{
|
|
1698
|
+
name: "agent_forms_upsert_template",
|
|
1699
|
+
kind: "write",
|
|
1700
|
+
description: "Guarded form-template upsert: agent_guard (role\u2192plan\u2192form_templates cap), grid layout computed server-side, create or edit by id, audited.",
|
|
1701
|
+
audits: true
|
|
1606
1702
|
}
|
|
1607
1703
|
],
|
|
1608
1704
|
locales: customFormsLocales
|