@fayz-ai/plugin-forms 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/{FormBuilder-KDANEL6Z.js → FormBuilder-7SWZQKFN.js} +130 -23
  2. package/dist/FormBuilder-7SWZQKFN.js.map +1 -0
  3. package/dist/{FormBuilder-4VKYVZAC.cjs → FormBuilder-YB75LEEH.cjs} +129 -22
  4. package/dist/FormBuilder-YB75LEEH.cjs.map +1 -0
  5. package/dist/agent-tools.d.ts +4 -0
  6. package/dist/agent-tools.d.ts.map +1 -0
  7. package/dist/chunk-QM2AF3DZ.cjs +446 -0
  8. package/dist/chunk-QM2AF3DZ.cjs.map +1 -0
  9. package/dist/chunk-W2ZNJGQC.js +438 -0
  10. package/dist/chunk-W2ZNJGQC.js.map +1 -0
  11. package/dist/components/DocumentFormDialog.d.ts +3 -1
  12. package/dist/components/DocumentFormDialog.d.ts.map +1 -1
  13. package/dist/components/DocumentList.d.ts.map +1 -1
  14. package/dist/components/FieldConfigDialog.d.ts.map +1 -1
  15. package/dist/components/FormBuilder.d.ts.map +1 -1
  16. package/dist/components/FormViewer.d.ts.map +1 -1
  17. package/dist/components/PersonDocumentsWidget.d.ts.map +1 -1
  18. package/dist/data/supabase.d.ts.map +1 -1
  19. package/dist/index.cjs +142 -54
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +142 -54
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/person-fields.d.ts +16 -0
  25. package/dist/lib/person-fields.d.ts.map +1 -0
  26. package/dist/lib/print.d.ts +17 -0
  27. package/dist/lib/print.d.ts.map +1 -0
  28. package/dist/lib/tenant.d.ts.map +1 -1
  29. package/dist/locales/en.d.ts.map +1 -1
  30. package/dist/locales/pt-BR.d.ts.map +1 -1
  31. package/dist/migrations/index.d.ts +1 -0
  32. package/dist/migrations/index.d.ts.map +1 -1
  33. package/dist/types.d.ts +14 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
  36. package/dist/views/TemplateListView.d.ts.map +1 -1
  37. package/package.json +4 -4
  38. package/src/agent-tools.ts +73 -0
  39. package/src/components/DocumentFormDialog.tsx +37 -16
  40. package/src/components/DocumentList.tsx +12 -8
  41. package/src/components/FieldConfigDialog.tsx +40 -0
  42. package/src/components/FormBuilder.tsx +104 -22
  43. package/src/components/FormViewer.tsx +11 -1
  44. package/src/components/PersonDocumentsWidget.tsx +10 -1
  45. package/src/data/supabase.ts +2 -0
  46. package/src/index.ts +21 -0
  47. package/src/lib/person-fields.ts +67 -0
  48. package/src/lib/print.ts +207 -0
  49. package/src/lib/tenant.ts +6 -2
  50. package/src/locales/en.ts +20 -0
  51. package/src/locales/pt-BR.ts +20 -0
  52. package/src/migrations/003_agent_rpcs.sql +174 -0
  53. package/src/migrations/index.ts +178 -1
  54. package/src/types.ts +14 -0
  55. package/src/views/CustomFormsSettingsTab.tsx +23 -16
  56. package/src/views/TemplateListView.tsx +29 -18
  57. package/dist/FormBuilder-4VKYVZAC.cjs.map +0 -1
  58. package/dist/FormBuilder-KDANEL6Z.js.map +0 -1
  59. package/dist/chunk-32I43T37.js +0 -198
  60. package/dist/chunk-32I43T37.js.map +0 -1
  61. package/dist/chunk-XLUULIVL.cjs +0 -200
  62. package/dist/chunk-XLUULIVL.cjs.map +0 -1
@@ -0,0 +1,16 @@
1
+ import type { FormSchema } from '../types';
2
+ /** Curated, vertical-agnostic person keys offered in the field builder. */
3
+ export declare const PERSON_FIELD_OPTIONS: Array<{
4
+ value: string;
5
+ labelKey: string;
6
+ }>;
7
+ /** Read a person field value, tolerating camelCase/snake_case key variants. */
8
+ export declare function resolvePersonValue(person: Record<string, unknown> | undefined | null, mapKey: string | undefined): unknown;
9
+ /**
10
+ * Build the initial document data for a template. For each field, applies (in
11
+ * priority order) an existing value, then the mapped person value, then the
12
+ * field's static `defaultValue`. Never overwrites a value already present in
13
+ * `existing`.
14
+ */
15
+ export declare function prefillFromPerson(schema: FormSchema | undefined, person: Record<string, unknown> | undefined | null, existing?: Record<string, unknown>): Record<string, unknown>;
16
+ //# sourceMappingURL=person-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"person-fields.d.ts","sourceRoot":"","sources":["../../src/lib/person-fields.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAU3E,CAAA;AAKD,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,EAClD,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,EAClD,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB"}
@@ -0,0 +1,17 @@
1
+ import type { FormDocument, FormTemplate } from '../types';
2
+ export interface BuildPrintOptions {
3
+ /** Heading shown at the top of the page (defaults to the document title). */
4
+ heading?: string;
5
+ /** Small line under the heading (e.g. person name). */
6
+ subheading?: string;
7
+ }
8
+ /** Build a full, self-contained printable HTML document. */
9
+ export declare function buildPrintHtml(template: FormTemplate, doc: FormDocument, opts?: BuildPrintOptions): string;
10
+ /**
11
+ * Print a prebuilt HTML document via a hidden iframe (popup-blocker safe).
12
+ * The browser print dialog handles both printing and "Save as PDF".
13
+ */
14
+ export declare function printHtml(html: string): void;
15
+ /** Convenience: build + print a document in one call. */
16
+ export declare function printDocument(template: FormTemplate, doc: FormDocument, opts?: BuildPrintOptions): void;
17
+ //# sourceMappingURL=print.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"print.d.ts","sourceRoot":"","sources":["../../src/lib/print.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAgB,YAAY,EAAE,MAAM,UAAU,CAAA;AAqDxE,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,YAAY,EACtB,GAAG,EAAE,YAAY,EACjB,IAAI,GAAE,iBAAsB,GAC3B,MAAM,CAwER;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CA6C5C;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,YAAY,EACtB,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE,iBAAiB,GACvB,IAAI,CAEN"}
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAGA,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAyB;AACvF,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAA2B"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAOA,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAyB;AACvF,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAAkD"}
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0ErC,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA8FrC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0EvC,CAAA"}
1
+ {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA8FvC,CAAA"}
@@ -1,6 +1,7 @@
1
1
  export declare const MIGRATION_000_PLG_RENAME = "-- 000_plg_rename.sql \u2014 rename legacy forms tables to plg_forms_* for pools\n-- provisioned before the industry-pool rename. Guarded: fires only when the legacy\n-- name exists and the target does not, so fresh pools skip every branch.\n-- The core `documents` archetype table is NOT renamed \u2014 it stays public.documents.\nDO $$\nBEGIN\n IF to_regclass('public.frm_documents') IS NOT NULL AND to_regclass('public.plg_forms_documents') IS NULL THEN\n ALTER TABLE public.frm_documents RENAME TO plg_forms_documents;\n END IF;\n IF to_regclass('public.frm_templates') IS NOT NULL AND to_regclass('public.plg_forms_templates') IS NULL THEN\n ALTER TABLE public.frm_templates RENAME TO plg_forms_templates;\n END IF;\n IF to_regclass('public.frm_document_files') IS NOT NULL AND to_regclass('public.plg_forms_document_files') IS NULL THEN\n ALTER TABLE public.frm_document_files RENAME TO plg_forms_document_files;\n END IF;\n -- frm_categories: registry-declared (form-template categories); no base-table\n -- DDL ships in this plugin, but rename in place if a pool created one.\n IF to_regclass('public.frm_categories') IS NOT NULL AND to_regclass('public.plg_forms_categories') IS NULL THEN\n ALTER TABLE public.frm_categories RENAME TO plg_forms_categories;\n END IF;\nEND $$;\n";
2
2
  export declare const MIGRATION_001_FRM_BASE = "-- ============================================================\n-- Custom Forms Plugin \u2014 Base Tables\n-- ============================================================\n\n-- plg_forms_templates: form template definitions (versioned)\nCREATE TABLE IF NOT EXISTS public.plg_forms_templates (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n description text,\n category text NOT NULL DEFAULT 'general',\n version integer NOT NULL DEFAULT 1,\n is_current boolean NOT NULL DEFAULT true,\n parent_id uuid REFERENCES public.plg_forms_templates(id),\n schema jsonb NOT NULL DEFAULT '{\"fields\":[],\"layout\":{\"columns\":12}}',\n specialty text,\n tags text[] DEFAULT '{}',\n metadata jsonb DEFAULT '{}',\n is_active boolean NOT NULL DEFAULT true,\n is_deleted boolean NOT NULL DEFAULT false,\n created_by uuid,\n updated_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.plg_forms_templates ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_forms_templates_tenant\n ON public.plg_forms_templates(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_forms_templates_parent\n ON public.plg_forms_templates(parent_id);\nCREATE INDEX IF NOT EXISTS idx_plg_forms_templates_category\n ON public.plg_forms_templates(tenant_id, category);\nCREATE INDEX IF NOT EXISTS idx_plg_forms_templates_current\n ON public.plg_forms_templates(tenant_id, is_current, is_active)\n WHERE is_current = true AND is_active = true AND is_deleted = false;\n\nDROP POLICY IF EXISTS \"plg_forms_templates_select\" ON public.plg_forms_templates;\nCREATE POLICY \"plg_forms_templates_select\" ON public.plg_forms_templates\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_templates_insert\" ON public.plg_forms_templates;\nCREATE POLICY \"plg_forms_templates_insert\" ON public.plg_forms_templates\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_templates_update\" ON public.plg_forms_templates;\nCREATE POLICY \"plg_forms_templates_update\" ON public.plg_forms_templates\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_templates_delete\" ON public.plg_forms_templates;\nCREATE POLICY \"plg_forms_templates_delete\" ON public.plg_forms_templates\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- plg_forms_documents: filled form instances\nCREATE TABLE IF NOT EXISTS public.plg_forms_documents (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n template_id uuid NOT NULL REFERENCES public.plg_forms_templates(id),\n person_id uuid REFERENCES public.people(id) ON DELETE SET NULL,\n title text,\n data jsonb NOT NULL DEFAULT '{}',\n status text NOT NULL DEFAULT 'draft',\n signed_at timestamptz,\n signed_by uuid,\n notes text,\n metadata jsonb DEFAULT '{}',\n is_deleted boolean NOT NULL DEFAULT false,\n created_by uuid,\n updated_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.plg_forms_documents ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_forms_documents_tenant\n ON public.plg_forms_documents(tenant_id);\n-- person_id/status only exist in the pre-archetype shape this file creates;\n-- converted pools (salon) already carry the archetype extension shape\n-- (document_id PK, no person_id) \u2014 002 owns that shape, so guard these.\nDO $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_forms_documents'\n AND column_name = 'person_id'\n ) THEN\n EXECUTE 'CREATE INDEX IF NOT EXISTS idx_plg_forms_documents_person\n ON public.plg_forms_documents(person_id)';\n EXECUTE 'CREATE INDEX IF NOT EXISTS idx_plg_forms_documents_status\n ON public.plg_forms_documents(tenant_id, status)';\n END IF;\nEND $$;\nCREATE INDEX IF NOT EXISTS idx_plg_forms_documents_template\n ON public.plg_forms_documents(template_id);\n\nDROP POLICY IF EXISTS \"plg_forms_documents_select\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_select\" ON public.plg_forms_documents\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_insert\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_insert\" ON public.plg_forms_documents\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_update\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_update\" ON public.plg_forms_documents\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_delete\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_delete\" ON public.plg_forms_documents\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- plg_forms_document_files: file attachments for image/gallery/drawing fields\nCREATE TABLE IF NOT EXISTS public.plg_forms_document_files (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n document_id uuid NOT NULL REFERENCES public.plg_forms_documents(id) ON DELETE CASCADE,\n field_key text NOT NULL,\n file_url text NOT NULL,\n file_name text,\n file_size integer,\n mime_type text,\n sort_order integer DEFAULT 0,\n metadata jsonb DEFAULT '{}',\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.plg_forms_document_files ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_forms_document_files_document\n ON public.plg_forms_document_files(document_id);\n\nDROP POLICY IF EXISTS \"plg_forms_document_files_select\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_select\" ON public.plg_forms_document_files\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_insert\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_insert\" ON public.plg_forms_document_files\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_update\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_update\" ON public.plg_forms_document_files\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_delete\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_delete\" ON public.plg_forms_document_files\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- View: pre-archetype read model (needs person_id; archetype pools get\n-- v_documents from 002 instead, which also drops this view when migrating).\nDO $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_forms_documents'\n AND column_name = 'person_id'\n ) THEN\n EXECUTE $v$\n CREATE OR REPLACE VIEW public.v_frm_documents AS\n SELECT\n d.*,\n t.name AS template_name,\n t.category AS template_category,\n p.name AS person_name\n FROM public.plg_forms_documents d\n LEFT JOIN public.plg_forms_templates t ON t.id = d.template_id\n LEFT JOIN public.people p ON p.id = d.person_id\n $v$;\n END IF;\nEND $$;\n";
3
3
  export declare const MIGRATION_002_DOCUMENT_ARCHETYPE = "-- ============================================================\n-- Document Archetype \u2014 public.documents\n-- A document is any record associated with a person (or standalone):\n-- forms, images, attachments, prescriptions, contracts, etc.\n-- The `kind` column discriminates the type.\n--\n-- DATA SAFETY (converted pools): plg_forms_documents may already hold REAL\n-- rows in its PRE-archetype shape (its own `id` PK, no `document_id` column \u2014\n-- see 001_frm_base.sql). This migration MIGRATES those rows into the new\n-- document archetype instead of dropping them:\n-- * pre-archetype + rows -> copy each legacy row into public.documents\n-- (reusing its id) + into the new extension\n-- table, migrate its files, then drop legacy.\n-- * pre-archetype + empty -> plain drop + create (fresh shape).\n-- * already new shape -> no-op.\n-- ============================================================\n\nCREATE TABLE IF NOT EXISTS public.documents (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n kind text NOT NULL DEFAULT 'attachment',\n -- kind: 'form', 'image', 'attachment', 'prescription', 'contract', etc.\n person_id uuid REFERENCES public.people(id) ON DELETE SET NULL,\n title text,\n description text,\n status text NOT NULL DEFAULT 'draft',\n -- status: 'draft', 'completed', 'signed', 'archived'\n file_url text,\n file_name text,\n file_size integer,\n mime_type text,\n tags text[] DEFAULT '{}',\n notes text,\n is_active boolean NOT NULL DEFAULT true,\n metadata jsonb DEFAULT '{}',\n created_by uuid,\n updated_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_documents_tenant ON public.documents(tenant_id, kind);\nCREATE INDEX IF NOT EXISTS idx_documents_person ON public.documents(person_id);\nCREATE INDEX IF NOT EXISTS idx_documents_status ON public.documents(tenant_id, status);\n\nDROP POLICY IF EXISTS \"documents_select\" ON public.documents;\nCREATE POLICY \"documents_select\" ON public.documents\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"documents_insert\" ON public.documents;\nCREATE POLICY \"documents_insert\" ON public.documents\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"documents_update\" ON public.documents;\nCREATE POLICY \"documents_update\" ON public.documents\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"documents_delete\" ON public.documents;\nCREATE POLICY \"documents_delete\" ON public.documents\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- ============================================================\n-- Reconcile plg_forms_documents / plg_forms_document_files with the archetype.\n-- ============================================================\n\n-- Step 1: if a PRE-archetype plg_forms_documents is present (no document_id\n-- column), rename it (and its files table + dependent view) out of the way so\n-- the new archetype-shaped tables can be created and \u2014 when it holds rows \u2014\n-- its data copied across. Fully guarded, so already-migrated pools skip this.\nDO $$\nBEGIN\n IF to_regclass('public.plg_forms_documents') IS NOT NULL\n AND NOT EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public'\n AND table_name = 'plg_forms_documents'\n AND column_name = 'document_id'\n ) THEN\n DROP VIEW IF EXISTS public.v_frm_documents;\n ALTER TABLE public.plg_forms_documents RENAME TO plg_forms_documents_legacy;\n IF to_regclass('public.plg_forms_document_files') IS NOT NULL THEN\n ALTER TABLE public.plg_forms_document_files RENAME TO plg_forms_document_files_legacy;\n END IF;\n END IF;\nEND $$;\n\n-- Step 2: create the new archetype-shaped extension tables. IF NOT EXISTS makes\n-- this a no-op on pools already migrated to the new shape; after Step 1 the\n-- names are free on pre-archetype pools.\n\n-- plg_forms_documents: extension table for form-type documents\nCREATE TABLE IF NOT EXISTS public.plg_forms_documents (\n document_id uuid PRIMARY KEY REFERENCES public.documents(id) ON DELETE CASCADE,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n template_id uuid NOT NULL REFERENCES public.plg_forms_templates(id),\n data jsonb NOT NULL DEFAULT '{}',\n signed_at timestamptz,\n signed_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.plg_forms_documents ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_forms_documents_template ON public.plg_forms_documents(template_id);\n\nDROP POLICY IF EXISTS \"plg_forms_documents_select\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_select\" ON public.plg_forms_documents\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_insert\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_insert\" ON public.plg_forms_documents\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_update\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_update\" ON public.plg_forms_documents\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_delete\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_delete\" ON public.plg_forms_documents\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- plg_forms_document_files: file attachments for form fields\nCREATE TABLE IF NOT EXISTS public.plg_forms_document_files (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n document_id uuid NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,\n field_key text NOT NULL,\n file_url text NOT NULL,\n file_name text,\n file_size integer,\n mime_type text,\n sort_order integer DEFAULT 0,\n metadata jsonb DEFAULT '{}',\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.plg_forms_document_files ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_forms_document_files_document ON public.plg_forms_document_files(document_id);\n\nDROP POLICY IF EXISTS \"plg_forms_document_files_select\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_select\" ON public.plg_forms_document_files\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_insert\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_insert\" ON public.plg_forms_document_files\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_update\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_update\" ON public.plg_forms_document_files\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_delete\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_delete\" ON public.plg_forms_document_files\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- Step 3: if legacy rows exist, migrate them, then drop the quarantined legacy\n-- tables. Reusing each legacy id as the documents.id keeps every legacy file's\n-- document_id FK valid after the copy. ON CONFLICT DO NOTHING makes a partial\n-- re-run safe.\nDO $$\nBEGIN\n IF to_regclass('public.plg_forms_documents_legacy') IS NOT NULL THEN\n -- 3a. base document (one per legacy form document; is_active mirrors NOT is_deleted)\n INSERT INTO public.documents (\n id, tenant_id, kind, person_id, title, status, notes, metadata,\n is_active, created_by, updated_by, created_at, updated_at\n )\n SELECT\n id, tenant_id, 'form', person_id, title, status, notes, metadata,\n NOT COALESCE(is_deleted, false), created_by, updated_by, created_at, updated_at\n FROM public.plg_forms_documents_legacy\n ON CONFLICT (id) DO NOTHING;\n\n -- 3b. form extension row (keyed by the reused document id)\n INSERT INTO public.plg_forms_documents (\n document_id, tenant_id, template_id, data, signed_at, signed_by, created_at, updated_at\n )\n SELECT\n id, tenant_id, template_id, data, signed_at, signed_by, created_at, updated_at\n FROM public.plg_forms_documents_legacy\n ON CONFLICT (document_id) DO NOTHING;\n\n -- 3c. file attachments (document_id already points at the reused id)\n IF to_regclass('public.plg_forms_document_files_legacy') IS NOT NULL THEN\n INSERT INTO public.plg_forms_document_files (\n id, tenant_id, document_id, field_key, file_url, file_name,\n file_size, mime_type, sort_order, metadata, created_at\n )\n SELECT\n id, tenant_id, document_id, field_key, file_url, file_name,\n file_size, mime_type, sort_order, metadata, created_at\n FROM public.plg_forms_document_files_legacy\n ON CONFLICT (id) DO NOTHING;\n END IF;\n END IF;\n\n -- Drop legacy remnants now that any data has been copied across.\n DROP TABLE IF EXISTS public.plg_forms_document_files_legacy;\n DROP TABLE IF EXISTS public.plg_forms_documents_legacy;\nEND $$;\n\n-- View: all documents for a person with form data joined when applicable\nCREATE OR REPLACE VIEW public.v_documents AS\nSELECT\n d.*,\n f.template_id,\n f.data AS form_data,\n f.signed_at,\n f.signed_by,\n t.name AS template_name,\n t.category AS template_category,\n p.name AS person_name\nFROM public.documents d\nLEFT JOIN public.plg_forms_documents f ON f.document_id = d.id\nLEFT JOIN public.plg_forms_templates t ON t.id = f.template_id\nLEFT JOIN public.people p ON p.id = d.person_id;\n";
4
+ export declare const MIGRATION_003_AGENT_RPCS = "-- ============================================================================\n-- plugin-forms 003: server-plane agent write RPC.\n--\n-- public.agent_forms_upsert_template \u2014 the assistant BUILDS or EDITS a form\n-- template from any surface or channel (in-app FAB, WhatsApp, MCP). Same\n-- contract as every agent_* RPC (agenda 005, financial 009):\n-- (p_tenant_id, p_actor_user_id, p_payload jsonb) \u2192 jsonb\n-- {ok:true, id, record:{...}} | {ok:false, denial:{...}} | {ok:false, error}\n--\n-- The grid layout is computed HERE (col 0, sequential rows, full-width span) so\n-- the model only ever sends {type,label,required?,options?} \u2014 never positions.\n--\n-- Payload:\n-- template_id? uuid \u2014 omit to CREATE, provide to EDIT\n-- name? text \u2014 required on create\n-- category? text \u2014 anamnesis|evolution|report|contract|general\n-- description? text\n-- fields? [{type,label,required?,placeholder?,options?[]}] \u2014 REPLACE all\n-- append_fields? [{...}] \u2014 APPEND to existing\n--\n-- GRANT authenticated+service_role \u2014 NEVER anon (the broker calls with the pool\n-- service key and injects tenant/actor; in-browser the signed-in user calls it\n-- directly and spine RLS/actor guards bind them to their own tenant).\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.agent_forms_upsert_template(\n p_tenant_id uuid,\n p_actor_user_id uuid,\n p_payload jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_denial jsonb;\n v_used int;\n v_template_id uuid;\n v_is_update boolean;\n v_name text;\n v_category text;\n v_description text;\n v_existing jsonb;\n v_input jsonb;\n v_merged jsonb;\n v_fields jsonb;\n v_schema jsonb;\n v_final_id uuid;\nBEGIN\n -- \u2500\u2500 payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n BEGIN\n v_template_id := NULLIF(p_payload->>'template_id','')::uuid;\n EXCEPTION WHEN OTHERS THEN\n RETURN jsonb_build_object('ok', false, 'error', 'invalid template_id');\n END;\n v_is_update := v_template_id IS NOT NULL;\n v_name := btrim(p_payload->>'name');\n v_category := lower(COALESCE(NULLIF(p_payload->>'category',''), 'general'));\n v_description := left(p_payload->>'description', 1000);\n IF v_category NOT IN ('anamnesis','evolution','report','contract','general') THEN\n v_category := 'general';\n END IF;\n\n IF NOT v_is_update AND (v_name IS NULL OR v_name = '') THEN\n RETURN jsonb_build_object('ok', false, 'error', 'name is required to create a form');\n END IF;\n\n -- \u2500\u2500 authorization: role \u2192 plan \u2192 form_templates cap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n SELECT count(*) INTO v_used FROM plg_forms_templates\n WHERE tenant_id = p_tenant_id AND is_deleted = false;\n v_denial := agent_guard(p_tenant_id, p_actor_user_id, 'custom_forms',\n CASE WHEN v_is_update THEN 'update' ELSE 'create' END,\n 'form_templates', v_used, CASE WHEN v_is_update THEN 0 ELSE 1 END);\n IF v_denial IS NOT NULL THEN\n RETURN jsonb_build_object('ok', false, 'denial', v_denial);\n END IF;\n\n -- \u2500\u2500 current fields (for APPEND / to reject an unknown id) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF v_is_update THEN\n SELECT COALESCE(schema->'fields','[]'::jsonb) INTO v_existing\n FROM plg_forms_templates\n WHERE id = v_template_id AND tenant_id = p_tenant_id AND is_deleted = false;\n IF v_existing IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'error', 'unknown template for this tenant');\n END IF;\n ELSE\n v_existing := '[]'::jsonb;\n END IF;\n\n -- \u2500\u2500 normalize the incoming fields to the builder's FormFieldDef shape \u2500\u2500\u2500\u2500\n -- (id, type, label, required, placeholder, col, colSpan, options) \u2014 row is\n -- assigned last, across the merged list.\n WITH src AS (\n SELECT CASE\n WHEN p_payload ? 'fields' THEN COALESCE(p_payload->'fields','[]'::jsonb)\n WHEN p_payload ? 'append_fields' THEN COALESCE(p_payload->'append_fields','[]'::jsonb)\n ELSE '[]'::jsonb\n END AS arr\n ),\n norm AS (\n SELECT f.ord, jsonb_strip_nulls(jsonb_build_object(\n 'id', COALESCE(NULLIF(f.value->>'id',''), gen_random_uuid()::text),\n 'type', COALESCE(NULLIF(f.value->>'type',''), 'text'),\n 'label', COALESCE(NULLIF(f.value->>'label',''), 'Campo'),\n 'required', COALESCE((f.value->>'required')::boolean, false),\n 'placeholder', NULLIF(f.value->>'placeholder',''),\n 'col', 0,\n 'colSpan', 12,\n 'options', CASE\n WHEN jsonb_typeof(f.value->'options') = 'array' AND jsonb_array_length(f.value->'options') > 0\n THEN (SELECT jsonb_agg(jsonb_build_object(\n 'label', opt,\n 'value', lower(regexp_replace(btrim(opt), '\\s+', '_', 'g'))))\n FROM jsonb_array_elements_text(f.value->'options') opt)\n ELSE NULL END\n )) AS field\n FROM src, jsonb_array_elements(src.arr) WITH ORDINALITY AS f(value, ord)\n )\n SELECT COALESCE(jsonb_agg(field ORDER BY ord), '[]'::jsonb) INTO v_input FROM norm;\n\n -- REPLACE with `fields`; otherwise APPEND onto what's already there.\n IF p_payload ? 'fields' THEN\n v_merged := v_input;\n ELSE\n v_merged := v_existing || v_input;\n END IF;\n\n -- sequential rows across the final list\n SELECT COALESCE(jsonb_agg(jsonb_set(e.value, '{row}', to_jsonb((e.ord - 1))) ORDER BY e.ord), '[]'::jsonb)\n INTO v_fields\n FROM jsonb_array_elements(v_merged) WITH ORDINALITY AS e(value, ord);\n\n v_schema := jsonb_build_object('layout', jsonb_build_object('columns', 12), 'fields', v_fields);\n\n -- \u2500\u2500 write + audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF v_is_update THEN\n UPDATE plg_forms_templates SET\n name = COALESCE(NULLIF(v_name,''), name),\n category = v_category,\n description = COALESCE(v_description, description),\n schema = v_schema,\n updated_by = p_actor_user_id,\n updated_at = now()\n WHERE id = v_template_id AND tenant_id = p_tenant_id AND is_deleted = false\n RETURNING id INTO v_final_id;\n ELSE\n INSERT INTO plg_forms_templates (tenant_id, name, category, description, schema, created_by, updated_by)\n VALUES (p_tenant_id, v_name, v_category, v_description, v_schema, p_actor_user_id, p_actor_user_id)\n RETURNING id INTO v_final_id;\n END IF;\n\n INSERT INTO audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)\n VALUES (p_tenant_id, p_actor_user_id,\n CASE WHEN v_is_update THEN 'agent.updateFormTemplate' ELSE 'agent.createFormTemplate' END,\n 'form_template', v_final_id::text,\n jsonb_build_object('payload', p_payload));\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_final_id,\n 'record', jsonb_build_object(\n 'ref', jsonb_build_object('id', v_final_id, 'resource', 'plg_forms_templates',\n 'archetype', 'custom_forms:template'),\n 'name', COALESCE(NULLIF(v_name,''), (SELECT name FROM plg_forms_templates WHERE id = v_final_id)),\n 'category', v_category,\n 'fieldCount', jsonb_array_length(v_fields)\n )\n );\nEND;\n$$;\n\nREVOKE ALL ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb) FROM public;\nREVOKE EXECUTE ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb) FROM anon;\nGRANT EXECUTE ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb)\n TO authenticated, service_role;\n";
4
5
  export declare const MIGRATIONS: Array<{
5
6
  id: string;
6
7
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,iyCAqBpC,CAAA;AAED,eAAO,MAAM,sBAAsB,y1PA8KlC,CAAA;AAED,eAAO,MAAM,gCAAgC,0xUA+N5C,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAIzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,iyCAqBpC,CAAA;AAED,eAAO,MAAM,sBAAsB,y1PA8KlC,CAAA;AAED,eAAO,MAAM,gCAAgC,0xUA+N5C,CAAA;AAED,eAAO,MAAM,wBAAwB,20QA8KpC,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAKzD,CAAA"}
package/dist/types.d.ts CHANGED
@@ -18,6 +18,20 @@ export interface FormFieldDef {
18
18
  label: string;
19
19
  value: string;
20
20
  }>;
21
+ /**
22
+ * Optional binding to a field on the person/contact record. When set, a new
23
+ * document created from a person's profile is pre-filled with that person's
24
+ * value (e.g. `map: 'documentNumber'` fills the field with the person's CPF).
25
+ * Matched tolerantly against camelCase/snake_case record keys. Only prefills
26
+ * empty fields; the user can still edit the value. See `lib/person-fields`.
27
+ */
28
+ map?: string;
29
+ /**
30
+ * Static default value applied to a NEW document when the field has no mapped
31
+ * person value and no existing value — e.g. pre-written contract clause text
32
+ * on a richtext field. Editable by the user.
33
+ */
34
+ defaultValue?: unknown;
21
35
  /** Type-specific configuration */
22
36
  config?: Record<string, unknown>;
23
37
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CustomFormsSettingsTab.d.ts","sourceRoot":"","sources":["../../src/views/CustomFormsSettingsTab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAA;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAGhD,UAAU,2BAA2B;IACnC,MAAM,EAAE,iBAAiB,CAAA;IACzB,QAAQ,EAAE,uBAAuB,CAAA;IACjC,KAAK,EAAE,gBAAgB,CAAA;CACxB;AAED,wBAAgB,sBAAsB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,2BAA2B,qBA+C9F"}
1
+ {"version":3,"file":"CustomFormsSettingsTab.d.ts","sourceRoot":"","sources":["../../src/views/CustomFormsSettingsTab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAA;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAGhD,UAAU,2BAA2B;IACnC,MAAM,EAAE,iBAAiB,CAAA;IACzB,QAAQ,EAAE,uBAAuB,CAAA;IACjC,KAAK,EAAE,gBAAgB,CAAA;CACxB;AAaD,wBAAgB,sBAAsB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,2BAA2B,qBA2C9F"}
@@ -1 +1 @@
1
- {"version":3,"file":"TemplateListView.d.ts","sourceRoot":"","sources":["../../src/views/TemplateListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAQ/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,UAAU,CAAA;AAE9D,UAAU,qBAAqB;IAC7B,KAAK,EAAE,gBAAgB,CAAA;IACvB,MAAM,EAAE,iBAAiB,CAAA;IACzB,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IACxC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,qBAAqB,qBAuIvF"}
1
+ {"version":3,"file":"TemplateListView.d.ts","sourceRoot":"","sources":["../../src/views/TemplateListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAS/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,UAAU,CAAA;AAE9D,UAAU,qBAAqB;IAC7B,KAAK,EAAE,gBAAgB,CAAA;IACvB,MAAM,EAAE,iBAAiB,CAAA;IACzB,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IACxC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,qBAAqB,qBAiJvF"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.8.0",
6
+ "version": "0.8.1",
7
7
  "description": "Fayz SDK — plugin-forms plugin",
8
8
  "type": "module",
9
9
  "main": "./dist/index.cjs",
@@ -32,9 +32,9 @@
32
32
  "@dnd-kit/utilities": "^3.2.2",
33
33
  "lucide-react": "^0.400.0",
34
34
  "zustand": "^4.5.0",
35
- "@fayz-ai/core": "^0.8.0",
36
- "@fayz-ai/saas": "^0.8.0",
37
- "@fayz-ai/ui": "^0.8.0"
35
+ "@fayz-ai/core": "^0.8.4",
36
+ "@fayz-ai/saas": "^0.8.4",
37
+ "@fayz-ai/ui": "^0.8.4"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/react": "^18.3.0",
@@ -0,0 +1,73 @@
1
+ import type { PluginAITool } from '@fayz-ai/core'
2
+
3
+ /**
4
+ * The assistant's form-building tool. It is a SERVER-PLANE write (a pool RPC),
5
+ * not a client-side surface mutation — so it works from any page and any
6
+ * channel (in-app FAB, WhatsApp, MCP), and persists straight to the tenant's
7
+ * form templates. This is the same pattern agenda/financial/marketing use.
8
+ *
9
+ * The builder surface (see FormBuilder) still publishes CONTEXT — which form is
10
+ * open and its current fields with ids — so "add an allergies field to THIS
11
+ * form" resolves to the right template. But the action itself is this tool.
12
+ */
13
+
14
+ const CATEGORY_ENUM = ['anamnesis', 'evolution', 'report', 'contract', 'general'] as const
15
+
16
+ const FIELD_SHAPE =
17
+ 'A field object: { type, label, required?, placeholder?, options?[] }. ' +
18
+ 'type ∈ title|text|memo|richtext|date|select|radio|tags|checkbox|image|gallery|budget ' +
19
+ '(title = section heading, no input). options are plain strings, only for select/radio/tags.'
20
+
21
+ export const createOrUpdateFormTool: PluginAITool = {
22
+ id: 'custom_forms.createOrUpdateForm',
23
+ name: 'createOrUpdateForm',
24
+ description:
25
+ 'Creates or edits a form template (anamnese, contrato, ficha…). Persisted server-side, so it works from ANY page — the user does NOT need the form builder open. ' +
26
+ 'Omit templateId to CREATE (name required); pass templateId to EDIT an existing form. ' +
27
+ 'Send the FULL desired field list in `fields` (replaces all fields), or `append_fields` to add to what already exists. ' +
28
+ 'Layout is automatic — never send positions. Understand the intent and propose a sensible field set in one call rather than asking field by field. The user confirms before it is saved.',
29
+ icon: 'FileText',
30
+ mode: 'persist',
31
+ limitKey: 'form_templates',
32
+ permission: { feature: 'custom_forms', action: 'create' },
33
+ execution: { plane: 'server', kind: 'rpc', rpc: 'agent_forms_upsert_template' },
34
+ category: 'Forms',
35
+ suggestions: [
36
+ { label: 'Monte um formulário de anamnese' },
37
+ { label: 'Adicione um campo de alergias neste formulário' },
38
+ ],
39
+ parameters: {
40
+ type: 'object',
41
+ properties: {
42
+ templateId: {
43
+ type: 'string',
44
+ description: 'Existing form id to EDIT (from context or listFormTemplates). Omit to create a new form.',
45
+ },
46
+ name: { type: 'string', description: 'Form name — required when creating.' },
47
+ category: {
48
+ type: 'string',
49
+ enum: [...CATEGORY_ENUM],
50
+ description: 'anamnesis | evolution | report | contract | general',
51
+ },
52
+ description: { type: 'string', description: 'Optional short description of the form.' },
53
+ fields: {
54
+ type: 'array',
55
+ description: `The FULL desired field list — REPLACES every existing field. Each item: ${FIELD_SHAPE}`,
56
+ items: { type: 'object', description: FIELD_SHAPE },
57
+ },
58
+ append_fields: {
59
+ type: 'array',
60
+ description: `Fields to ADD to the existing form, keeping current ones. Use this for incremental edits instead of \`fields\`. Each item: ${FIELD_SHAPE}`,
61
+ items: { type: 'object', description: FIELD_SHAPE },
62
+ },
63
+ },
64
+ },
65
+ }
66
+
67
+ export const FORM_BUILDER_INSTRUCTIONS = [
68
+ 'Você está na tela do CONSTRUTOR DE FORMULÁRIOS. Para montar ou editar formulários use a tool createOrUpdateForm (ela persiste no backend e funciona mesmo fora desta tela).',
69
+ 'O formulário aberto agora — id, nome, categoria e campos com seus ids — está no JSON da tela em contexto.',
70
+ 'Para EDITAR este formulário, passe o templateId do contexto. Para adicionar campos mantendo os atuais, use append_fields; para redefinir tudo, use fields com a lista completa.',
71
+ 'Ao criar do zero: entenda a intenção e proponha um conjunto de campos enxuto numa única chamada (não pergunte campo a campo). Depois confirme em uma frase o que foi montado.',
72
+ 'Tipos de campo: title (cabeçalho de seção, sem input), text, memo (multilinha), richtext, date, select, radio, tags, checkbox, image, gallery, budget. Layout é automático.',
73
+ ].join(' ')
@@ -1,11 +1,13 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react'
2
- import { ArrowLeft, Save, Loader2 } from 'lucide-react'
3
- import { Button, useSaveBar } from '@fayz-ai/ui'
2
+ import { ArrowLeft } from 'lucide-react'
3
+ import { useSaveBar } from '@fayz-ai/ui'
4
4
  import { useTranslation } from '@fayz-ai/core'
5
+ import { useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
5
6
  import type { CustomFormsDataProvider } from '../data/types'
6
7
  import type { CustomFormsStore } from '../store'
7
8
  import type { FormTemplate, FormDocument } from '../types'
8
9
  import { FormRenderer } from './FormRenderer'
10
+ import { prefillFromPerson } from '../lib/person-fields'
9
11
 
10
12
  function generateTitle(templateName: string, personName?: string): string {
11
13
  const date = new Date().toLocaleDateString()
@@ -19,6 +21,8 @@ interface DocumentFormDialogProps {
19
21
  existingDocument?: FormDocument
20
22
  personId: string
21
23
  personName?: string
24
+ /** Full person record — mapped fields are pre-filled from it on new documents. */
25
+ person?: Record<string, unknown>
22
26
  provider: CustomFormsDataProvider
23
27
  store: CustomFormsStore
24
28
  onSaved: () => void
@@ -30,14 +34,20 @@ export function DocumentFormDialog({
30
34
  existingDocument,
31
35
  personId,
32
36
  personName,
37
+ person,
33
38
  provider,
34
39
  store,
35
40
  onSaved,
36
41
  onBack,
37
42
  }: DocumentFormDialogProps) {
38
43
  const t = useTranslation()
44
+ const guardDocuments = useLimitGuard('documents_month')
39
45
  const [template, setTemplate] = useState<FormTemplate | null>(initialTemplate)
40
- const [data, setData] = useState<Record<string, unknown>>(existingDocument?.data ?? {})
46
+ const [data, setData] = useState<Record<string, unknown>>(() =>
47
+ existingDocument
48
+ ? existingDocument.data
49
+ : prefillFromPerson(initialTemplate?.schema, person),
50
+ )
41
51
  const [saving, setSaving] = useState(false)
42
52
 
43
53
  useEffect(() => {
@@ -46,9 +56,21 @@ export function DocumentFormDialog({
46
56
  }
47
57
  }, [existingDocument, template, provider])
48
58
 
59
+ // When creating from a template that arrives asynchronously, apply prefill
60
+ // once the schema is known (guarded so it never clobbers user edits).
61
+ const prefilled = React.useRef(!!initialTemplate || !!existingDocument)
62
+ useEffect(() => {
63
+ if (prefilled.current || !template || existingDocument) return
64
+ prefilled.current = true
65
+ setData((prev) => prefillFromPerson(template.schema, person, prev))
66
+ }, [template, existingDocument, person])
67
+
49
68
  const handleSave = useCallback(
50
69
  async (status: 'draft' | 'completed') => {
51
70
  if (!template) return
71
+ // Plan monthly-quota guard (client-side, before the provider call) — only
72
+ // on create. Opens the global UpgradeModal and aborts when the cap is hit.
73
+ if (!existingDocument && (await guardDocuments()) === 'blocked') return
52
74
  setSaving(true)
53
75
  try {
54
76
  const autoTitle = generateTitle(template.name, personName)
@@ -62,20 +84,26 @@ export function DocumentFormDialog({
62
84
  data,
63
85
  status,
64
86
  })
87
+ invalidateLimit('documents_month')
65
88
  }
66
89
  onSaved()
67
90
  } finally {
68
91
  setSaving(false)
69
92
  }
70
93
  },
71
- [template, existingDocument, data, personId, personName, store, onSaved],
94
+ [template, existingDocument, data, personId, personName, store, onSaved, guardDocuments],
72
95
  )
73
96
 
74
- // Dirty detection snapshot editable field values once template is ready
97
+ // Dirty detection drives the floating SaveBar. The baseline is the document's
98
+ // ORIGINAL saved data (empty for a brand-new document), so a new document that
99
+ // arrives pre-filled from the person's record counts as dirty immediately and
100
+ // the floating "unsaved changes" bar shows right away.
75
101
  const snapshot = React.useRef<string | null>(null)
76
102
  useEffect(() => {
77
- if (template && snapshot.current === null) snapshot.current = JSON.stringify(data)
78
- }, [template])
103
+ if (template && snapshot.current === null) {
104
+ snapshot.current = JSON.stringify(existingDocument?.data ?? {})
105
+ }
106
+ }, [template, existingDocument])
79
107
  const dirty = snapshot.current !== null && JSON.stringify(data) !== snapshot.current
80
108
 
81
109
  useSaveBar({
@@ -113,7 +141,8 @@ export function DocumentFormDialog({
113
141
  </div>
114
142
  </div>
115
143
 
116
- {/* Form fields */}
144
+ {/* Form fields — saving is driven entirely by the floating SaveBar
145
+ (unsaved-changes bar), matching the rest of the CRUD detail UX. */}
117
146
  <div className="rounded-xl border p-4">
118
147
  <FormRenderer
119
148
  schema={template.schema}
@@ -121,14 +150,6 @@ export function DocumentFormDialog({
121
150
  onChange={setData}
122
151
  />
123
152
  </div>
124
-
125
- {/* Secondary action — primary "Save & Complete" lives in the floating SaveBar */}
126
- <div className="flex justify-end gap-2">
127
- <Button variant="outline" size="sm" onClick={() => handleSave('draft')} disabled={saving}>
128
- {saving ? <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" /> : <Save className="h-3.5 w-3.5 mr-1.5" />}
129
- {t('customForms.saveAsDraft')}
130
- </Button>
131
- </div>
132
153
  </div>
133
154
  )
134
155
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useState, useCallback, useRef } from 'react'
2
- import { FileText, Trash2, Eye, Upload, Image as ImageIcon, Paperclip } from 'lucide-react'
2
+ import { FileText, Archive, Eye, Upload, Image as ImageIcon, Paperclip } from 'lucide-react'
3
3
  import { Badge } from '@fayz-ai/ui'
4
4
  import { useTranslation } from '@fayz-ai/core'
5
5
  import type { CustomFormsDataProvider } from '../data/types'
@@ -69,7 +69,8 @@ export function DocumentList({ personId, provider, store, onView, onFileDrop }:
69
69
  setLoading(true)
70
70
  try {
71
71
  const result = await provider.getDocuments({ personId })
72
- setDocuments(result.data)
72
+ // Archived docs are kept (not deleted) but drop out of the active list.
73
+ setDocuments(result.data.filter((d) => d.status !== 'archived'))
73
74
  } finally {
74
75
  setLoading(false)
75
76
  }
@@ -85,9 +86,11 @@ export function DocumentList({ personId, provider, store, onView, onFileDrop }:
85
86
  return unsub
86
87
  }, [store, fetchDocs])
87
88
 
88
- const handleDelete = useCallback(async (doc: FormDocument) => {
89
- if (!window.confirm(t('customForms.deleteDocumentConfirm'))) return
90
- await provider.deleteDocument(doc.id)
89
+ // Archive, not delete: a filled document is a record of what was signed —
90
+ // it is moved out of the active list, never destroyed.
91
+ const handleArchive = useCallback(async (doc: FormDocument) => {
92
+ if (!window.confirm(t('customForms.archiveDocumentConfirm'))) return
93
+ await provider.updateDocument(doc.id, { status: 'archived' })
91
94
  setDocuments((prev) => prev.filter((d) => d.id !== doc.id))
92
95
  }, [provider, t])
93
96
 
@@ -231,10 +234,11 @@ export function DocumentList({ personId, provider, store, onView, onFileDrop }:
231
234
  <Eye className="h-3.5 w-3.5 text-muted-foreground" />
232
235
  </button>
233
236
  <button
234
- onClick={(e) => { e.stopPropagation(); handleDelete(doc) }}
235
- className="p-1.5 rounded-md hover:bg-destructive/10"
237
+ onClick={(e) => { e.stopPropagation(); handleArchive(doc) }}
238
+ className="p-1.5 rounded-md hover:bg-muted"
239
+ title={t('customForms.archive')}
236
240
  >
237
- <Trash2 className="h-3.5 w-3.5 text-destructive" />
241
+ <Archive className="h-3.5 w-3.5 text-muted-foreground" />
238
242
  </button>
239
243
  </div>
240
244
  </div>
@@ -10,9 +10,21 @@ import {
10
10
  } from '@fayz-ai/ui'
11
11
  import { Button } from '@fayz-ai/ui'
12
12
  import { Input } from '@fayz-ai/ui'
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from '@fayz-ai/ui'
13
20
  import { useTranslation } from '@fayz-ai/core'
14
21
  import type { FormFieldDef } from '../types'
15
22
  import { FIELD_TYPES } from './FieldPalette'
23
+ import { PERSON_FIELD_OPTIONS } from '../lib/person-fields'
24
+
25
+ /** Field types that can be auto-filled from a person record. */
26
+ const MAPPABLE_TYPES = ['text', 'memo', 'richtext', 'date']
27
+ const MAP_NONE = '__none__'
16
28
 
17
29
  const SIZE_OPTIONS = [
18
30
  { span: 3, label: '1/4' },
@@ -38,6 +50,7 @@ export function FieldConfigDrawer({ field, onSave, onUpdate, onClose }: FieldCon
38
50
  const [required, setRequired] = useState(false)
39
51
  const [colSpan, setColSpan] = useState(12)
40
52
  const [options, setOptions] = useState<Array<{ label: string; value: string }>>([])
53
+ const [map, setMap] = useState<string>('')
41
54
 
42
55
  // Sync state when field changes
43
56
  useEffect(() => {
@@ -47,11 +60,13 @@ export function FieldConfigDrawer({ field, onSave, onUpdate, onClose }: FieldCon
47
60
  setRequired(field.required ?? false)
48
61
  setColSpan(field.colSpan)
49
62
  setOptions(field.options ?? [])
63
+ setMap(field.map ?? '')
50
64
  }, [field?.id])
51
65
 
52
66
  if (!field) return null
53
67
 
54
68
  const hasOptions = ['select', 'radio', 'tags'].includes(field.type)
69
+ const canMap = MAPPABLE_TYPES.includes(field.type)
55
70
  const fieldTypeDef = FIELD_TYPES.find((ft) => ft.type === field.type)
56
71
  const Icon = fieldTypeDef?.icon
57
72
 
@@ -63,6 +78,7 @@ export function FieldConfigDrawer({ field, onSave, onUpdate, onClose }: FieldCon
63
78
  required,
64
79
  colSpan: Math.max(1, Math.min(12, colSpan)),
65
80
  options: hasOptions ? options.filter((o) => o.label.trim()) : field.options,
81
+ map: canMap && map ? map : undefined,
66
82
  })
67
83
  }
68
84
 
@@ -167,6 +183,30 @@ export function FieldConfigDrawer({ field, onSave, onUpdate, onClose }: FieldCon
167
183
  </label>
168
184
  )}
169
185
 
186
+ {/* Auto-fill from person record */}
187
+ {canMap && (
188
+ <div className="space-y-1.5">
189
+ <label className="text-xs font-medium">{t('customForms.builder.mapToPerson')}</label>
190
+ <Select
191
+ value={map || MAP_NONE}
192
+ onValueChange={(v) => setMap(v === MAP_NONE ? '' : v)}
193
+ >
194
+ <SelectTrigger className="h-9 text-sm">
195
+ <SelectValue />
196
+ </SelectTrigger>
197
+ <SelectContent>
198
+ <SelectItem value={MAP_NONE}>{t('customForms.builder.mapNone')}</SelectItem>
199
+ {PERSON_FIELD_OPTIONS.map((opt) => (
200
+ <SelectItem key={opt.value} value={opt.value}>
201
+ {t(opt.labelKey)}
202
+ </SelectItem>
203
+ ))}
204
+ </SelectContent>
205
+ </Select>
206
+ <p className="text-[10px] text-muted-foreground">{t('customForms.builder.mapHint')}</p>
207
+ </div>
208
+ )}
209
+
170
210
  {/* Options */}
171
211
  {hasOptions && (
172
212
  <div className="space-y-2">