@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
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
-- ============================================================
|
|
2
|
-
-- Document Archetype —
|
|
2
|
+
-- Document Archetype — public.documents
|
|
3
3
|
-- A document is any record associated with a person (or standalone):
|
|
4
4
|
-- forms, images, attachments, prescriptions, contracts, etc.
|
|
5
5
|
-- The `kind` column discriminates the type.
|
|
6
|
+
--
|
|
7
|
+
-- DATA SAFETY (converted pools): plg_forms_documents may already hold REAL
|
|
8
|
+
-- rows in its PRE-archetype shape (its own `id` PK, no `document_id` column —
|
|
9
|
+
-- see 001_frm_base.sql). This migration MIGRATES those rows into the new
|
|
10
|
+
-- document archetype instead of dropping them:
|
|
11
|
+
-- * pre-archetype + rows -> copy each legacy row into public.documents
|
|
12
|
+
-- (reusing its id) + into the new extension
|
|
13
|
+
-- table, migrate its files, then drop legacy.
|
|
14
|
+
-- * pre-archetype + empty -> plain drop + create (fresh shape).
|
|
15
|
+
-- * already new shape -> no-op.
|
|
6
16
|
-- ============================================================
|
|
7
17
|
|
|
8
|
-
CREATE TABLE IF NOT EXISTS
|
|
18
|
+
CREATE TABLE IF NOT EXISTS public.documents (
|
|
9
19
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
10
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
20
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
11
21
|
kind text NOT NULL DEFAULT 'attachment',
|
|
12
22
|
-- kind: 'form', 'image', 'attachment', 'prescription', 'contract', etc.
|
|
13
|
-
person_id uuid REFERENCES
|
|
23
|
+
person_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
14
24
|
title text,
|
|
15
25
|
description text,
|
|
16
26
|
status text NOT NULL DEFAULT 'draft',
|
|
@@ -29,39 +39,63 @@ CREATE TABLE IF NOT EXISTS saas_core.documents (
|
|
|
29
39
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
30
40
|
);
|
|
31
41
|
|
|
32
|
-
ALTER TABLE
|
|
42
|
+
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
|
|
33
43
|
|
|
34
|
-
CREATE INDEX IF NOT EXISTS idx_documents_tenant ON
|
|
35
|
-
CREATE INDEX IF NOT EXISTS idx_documents_person ON
|
|
36
|
-
CREATE INDEX IF NOT EXISTS idx_documents_status ON
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_documents_tenant ON public.documents(tenant_id, kind);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_documents_person ON public.documents(person_id);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_documents_status ON public.documents(tenant_id, status);
|
|
37
47
|
|
|
38
|
-
|
|
48
|
+
DROP POLICY IF EXISTS "documents_select" ON public.documents;
|
|
49
|
+
CREATE POLICY "documents_select" ON public.documents
|
|
39
50
|
FOR SELECT TO authenticated
|
|
40
51
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
41
|
-
|
|
52
|
+
DROP POLICY IF EXISTS "documents_insert" ON public.documents;
|
|
53
|
+
CREATE POLICY "documents_insert" ON public.documents
|
|
42
54
|
FOR INSERT TO authenticated
|
|
43
55
|
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
44
|
-
|
|
56
|
+
DROP POLICY IF EXISTS "documents_update" ON public.documents;
|
|
57
|
+
CREATE POLICY "documents_update" ON public.documents
|
|
45
58
|
FOR UPDATE TO authenticated
|
|
46
59
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
47
|
-
|
|
60
|
+
DROP POLICY IF EXISTS "documents_delete" ON public.documents;
|
|
61
|
+
CREATE POLICY "documents_delete" ON public.documents
|
|
48
62
|
FOR DELETE TO authenticated
|
|
49
63
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
50
64
|
|
|
51
65
|
-- ============================================================
|
|
52
|
-
--
|
|
66
|
+
-- Reconcile plg_forms_documents / plg_forms_document_files with the archetype.
|
|
53
67
|
-- ============================================================
|
|
54
68
|
|
|
55
|
-
--
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
-- Step 1: if a PRE-archetype plg_forms_documents is present (no document_id
|
|
70
|
+
-- column), rename it (and its files table + dependent view) out of the way so
|
|
71
|
+
-- the new archetype-shaped tables can be created and — when it holds rows —
|
|
72
|
+
-- its data copied across. Fully guarded, so already-migrated pools skip this.
|
|
73
|
+
DO $$
|
|
74
|
+
BEGIN
|
|
75
|
+
IF to_regclass('public.plg_forms_documents') IS NOT NULL
|
|
76
|
+
AND NOT EXISTS (
|
|
77
|
+
SELECT 1 FROM information_schema.columns
|
|
78
|
+
WHERE table_schema = 'public'
|
|
79
|
+
AND table_name = 'plg_forms_documents'
|
|
80
|
+
AND column_name = 'document_id'
|
|
81
|
+
) THEN
|
|
82
|
+
DROP VIEW IF EXISTS public.v_frm_documents;
|
|
83
|
+
ALTER TABLE public.plg_forms_documents RENAME TO plg_forms_documents_legacy;
|
|
84
|
+
IF to_regclass('public.plg_forms_document_files') IS NOT NULL THEN
|
|
85
|
+
ALTER TABLE public.plg_forms_document_files RENAME TO plg_forms_document_files_legacy;
|
|
86
|
+
END IF;
|
|
87
|
+
END IF;
|
|
88
|
+
END $$;
|
|
59
89
|
|
|
60
|
-
--
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
-- Step 2: create the new archetype-shaped extension tables. IF NOT EXISTS makes
|
|
91
|
+
-- this a no-op on pools already migrated to the new shape; after Step 1 the
|
|
92
|
+
-- names are free on pre-archetype pools.
|
|
93
|
+
|
|
94
|
+
-- plg_forms_documents: extension table for form-type documents
|
|
95
|
+
CREATE TABLE IF NOT EXISTS public.plg_forms_documents (
|
|
96
|
+
document_id uuid PRIMARY KEY REFERENCES public.documents(id) ON DELETE CASCADE,
|
|
97
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
98
|
+
template_id uuid NOT NULL REFERENCES public.plg_forms_templates(id),
|
|
65
99
|
data jsonb NOT NULL DEFAULT '{}',
|
|
66
100
|
signed_at timestamptz,
|
|
67
101
|
signed_by uuid,
|
|
@@ -69,28 +103,32 @@ CREATE TABLE public.frm_documents (
|
|
|
69
103
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
70
104
|
);
|
|
71
105
|
|
|
72
|
-
ALTER TABLE public.
|
|
106
|
+
ALTER TABLE public.plg_forms_documents ENABLE ROW LEVEL SECURITY;
|
|
73
107
|
|
|
74
|
-
CREATE INDEX IF NOT EXISTS
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_documents_template ON public.plg_forms_documents(template_id);
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
DROP POLICY IF EXISTS "plg_forms_documents_select" ON public.plg_forms_documents;
|
|
111
|
+
CREATE POLICY "plg_forms_documents_select" ON public.plg_forms_documents
|
|
77
112
|
FOR SELECT TO authenticated
|
|
78
113
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
79
|
-
|
|
114
|
+
DROP POLICY IF EXISTS "plg_forms_documents_insert" ON public.plg_forms_documents;
|
|
115
|
+
CREATE POLICY "plg_forms_documents_insert" ON public.plg_forms_documents
|
|
80
116
|
FOR INSERT TO authenticated
|
|
81
117
|
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
82
|
-
|
|
118
|
+
DROP POLICY IF EXISTS "plg_forms_documents_update" ON public.plg_forms_documents;
|
|
119
|
+
CREATE POLICY "plg_forms_documents_update" ON public.plg_forms_documents
|
|
83
120
|
FOR UPDATE TO authenticated
|
|
84
121
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
85
|
-
|
|
122
|
+
DROP POLICY IF EXISTS "plg_forms_documents_delete" ON public.plg_forms_documents;
|
|
123
|
+
CREATE POLICY "plg_forms_documents_delete" ON public.plg_forms_documents
|
|
86
124
|
FOR DELETE TO authenticated
|
|
87
125
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
88
126
|
|
|
89
|
-
--
|
|
90
|
-
CREATE TABLE public.
|
|
127
|
+
-- plg_forms_document_files: file attachments for form fields
|
|
128
|
+
CREATE TABLE IF NOT EXISTS public.plg_forms_document_files (
|
|
91
129
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
92
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
93
|
-
document_id uuid NOT NULL REFERENCES
|
|
130
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
131
|
+
document_id uuid NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,
|
|
94
132
|
field_key text NOT NULL,
|
|
95
133
|
file_url text NOT NULL,
|
|
96
134
|
file_name text,
|
|
@@ -101,23 +139,73 @@ CREATE TABLE public.frm_document_files (
|
|
|
101
139
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
102
140
|
);
|
|
103
141
|
|
|
104
|
-
ALTER TABLE public.
|
|
142
|
+
ALTER TABLE public.plg_forms_document_files ENABLE ROW LEVEL SECURITY;
|
|
105
143
|
|
|
106
|
-
CREATE INDEX IF NOT EXISTS
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_document_files_document ON public.plg_forms_document_files(document_id);
|
|
107
145
|
|
|
108
|
-
|
|
146
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_select" ON public.plg_forms_document_files;
|
|
147
|
+
CREATE POLICY "plg_forms_document_files_select" ON public.plg_forms_document_files
|
|
109
148
|
FOR SELECT TO authenticated
|
|
110
149
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
111
|
-
|
|
150
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_insert" ON public.plg_forms_document_files;
|
|
151
|
+
CREATE POLICY "plg_forms_document_files_insert" ON public.plg_forms_document_files
|
|
112
152
|
FOR INSERT TO authenticated
|
|
113
153
|
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
114
|
-
|
|
154
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_update" ON public.plg_forms_document_files;
|
|
155
|
+
CREATE POLICY "plg_forms_document_files_update" ON public.plg_forms_document_files
|
|
115
156
|
FOR UPDATE TO authenticated
|
|
116
157
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
117
|
-
|
|
158
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_delete" ON public.plg_forms_document_files;
|
|
159
|
+
CREATE POLICY "plg_forms_document_files_delete" ON public.plg_forms_document_files
|
|
118
160
|
FOR DELETE TO authenticated
|
|
119
161
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
120
162
|
|
|
163
|
+
-- Step 3: if legacy rows exist, migrate them, then drop the quarantined legacy
|
|
164
|
+
-- tables. Reusing each legacy id as the documents.id keeps every legacy file's
|
|
165
|
+
-- document_id FK valid after the copy. ON CONFLICT DO NOTHING makes a partial
|
|
166
|
+
-- re-run safe.
|
|
167
|
+
DO $$
|
|
168
|
+
BEGIN
|
|
169
|
+
IF to_regclass('public.plg_forms_documents_legacy') IS NOT NULL THEN
|
|
170
|
+
-- 3a. base document (one per legacy form document; is_active mirrors NOT is_deleted)
|
|
171
|
+
INSERT INTO public.documents (
|
|
172
|
+
id, tenant_id, kind, person_id, title, status, notes, metadata,
|
|
173
|
+
is_active, created_by, updated_by, created_at, updated_at
|
|
174
|
+
)
|
|
175
|
+
SELECT
|
|
176
|
+
id, tenant_id, 'form', person_id, title, status, notes, metadata,
|
|
177
|
+
NOT COALESCE(is_deleted, false), created_by, updated_by, created_at, updated_at
|
|
178
|
+
FROM public.plg_forms_documents_legacy
|
|
179
|
+
ON CONFLICT (id) DO NOTHING;
|
|
180
|
+
|
|
181
|
+
-- 3b. form extension row (keyed by the reused document id)
|
|
182
|
+
INSERT INTO public.plg_forms_documents (
|
|
183
|
+
document_id, tenant_id, template_id, data, signed_at, signed_by, created_at, updated_at
|
|
184
|
+
)
|
|
185
|
+
SELECT
|
|
186
|
+
id, tenant_id, template_id, data, signed_at, signed_by, created_at, updated_at
|
|
187
|
+
FROM public.plg_forms_documents_legacy
|
|
188
|
+
ON CONFLICT (document_id) DO NOTHING;
|
|
189
|
+
|
|
190
|
+
-- 3c. file attachments (document_id already points at the reused id)
|
|
191
|
+
IF to_regclass('public.plg_forms_document_files_legacy') IS NOT NULL THEN
|
|
192
|
+
INSERT INTO public.plg_forms_document_files (
|
|
193
|
+
id, tenant_id, document_id, field_key, file_url, file_name,
|
|
194
|
+
file_size, mime_type, sort_order, metadata, created_at
|
|
195
|
+
)
|
|
196
|
+
SELECT
|
|
197
|
+
id, tenant_id, document_id, field_key, file_url, file_name,
|
|
198
|
+
file_size, mime_type, sort_order, metadata, created_at
|
|
199
|
+
FROM public.plg_forms_document_files_legacy
|
|
200
|
+
ON CONFLICT (id) DO NOTHING;
|
|
201
|
+
END IF;
|
|
202
|
+
END IF;
|
|
203
|
+
|
|
204
|
+
-- Drop legacy remnants now that any data has been copied across.
|
|
205
|
+
DROP TABLE IF EXISTS public.plg_forms_document_files_legacy;
|
|
206
|
+
DROP TABLE IF EXISTS public.plg_forms_documents_legacy;
|
|
207
|
+
END $$;
|
|
208
|
+
|
|
121
209
|
-- View: all documents for a person with form data joined when applicable
|
|
122
210
|
CREATE OR REPLACE VIEW public.v_documents AS
|
|
123
211
|
SELECT
|
|
@@ -129,7 +217,7 @@ SELECT
|
|
|
129
217
|
t.name AS template_name,
|
|
130
218
|
t.category AS template_category,
|
|
131
219
|
p.name AS person_name
|
|
132
|
-
FROM
|
|
133
|
-
LEFT JOIN public.
|
|
134
|
-
LEFT JOIN public.
|
|
135
|
-
LEFT JOIN
|
|
220
|
+
FROM public.documents d
|
|
221
|
+
LEFT JOIN public.plg_forms_documents f ON f.document_id = d.id
|
|
222
|
+
LEFT JOIN public.plg_forms_templates t ON t.id = f.template_id
|
|
223
|
+
LEFT JOIN public.people p ON p.id = d.person_id;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- plugin-forms 003: server-plane agent write RPC.
|
|
3
|
+
--
|
|
4
|
+
-- public.agent_forms_upsert_template — the assistant BUILDS or EDITS a form
|
|
5
|
+
-- template from any surface or channel (in-app FAB, WhatsApp, MCP). Same
|
|
6
|
+
-- contract as every agent_* RPC (agenda 005, financial 009):
|
|
7
|
+
-- (p_tenant_id, p_actor_user_id, p_payload jsonb) → jsonb
|
|
8
|
+
-- {ok:true, id, record:{...}} | {ok:false, denial:{...}} | {ok:false, error}
|
|
9
|
+
--
|
|
10
|
+
-- The grid layout is computed HERE (col 0, sequential rows, full-width span) so
|
|
11
|
+
-- the model only ever sends {type,label,required?,options?} — never positions.
|
|
12
|
+
--
|
|
13
|
+
-- Payload:
|
|
14
|
+
-- template_id? uuid — omit to CREATE, provide to EDIT
|
|
15
|
+
-- name? text — required on create
|
|
16
|
+
-- category? text — anamnesis|evolution|report|contract|general
|
|
17
|
+
-- description? text
|
|
18
|
+
-- fields? [{type,label,required?,placeholder?,options?[]}] — REPLACE all
|
|
19
|
+
-- append_fields? [{...}] — APPEND to existing
|
|
20
|
+
--
|
|
21
|
+
-- GRANT authenticated+service_role — NEVER anon (the broker calls with the pool
|
|
22
|
+
-- service key and injects tenant/actor; in-browser the signed-in user calls it
|
|
23
|
+
-- directly and spine RLS/actor guards bind them to their own tenant).
|
|
24
|
+
-- ============================================================================
|
|
25
|
+
|
|
26
|
+
CREATE OR REPLACE FUNCTION public.agent_forms_upsert_template(
|
|
27
|
+
p_tenant_id uuid,
|
|
28
|
+
p_actor_user_id uuid,
|
|
29
|
+
p_payload jsonb
|
|
30
|
+
) RETURNS jsonb
|
|
31
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
32
|
+
SET search_path = public
|
|
33
|
+
AS $$
|
|
34
|
+
DECLARE
|
|
35
|
+
v_denial jsonb;
|
|
36
|
+
v_used int;
|
|
37
|
+
v_template_id uuid;
|
|
38
|
+
v_is_update boolean;
|
|
39
|
+
v_name text;
|
|
40
|
+
v_category text;
|
|
41
|
+
v_description text;
|
|
42
|
+
v_existing jsonb;
|
|
43
|
+
v_input jsonb;
|
|
44
|
+
v_merged jsonb;
|
|
45
|
+
v_fields jsonb;
|
|
46
|
+
v_schema jsonb;
|
|
47
|
+
v_final_id uuid;
|
|
48
|
+
BEGIN
|
|
49
|
+
-- ── payload ──────────────────────────────────────────────────────────────
|
|
50
|
+
BEGIN
|
|
51
|
+
v_template_id := NULLIF(p_payload->>'template_id','')::uuid;
|
|
52
|
+
EXCEPTION WHEN OTHERS THEN
|
|
53
|
+
RETURN jsonb_build_object('ok', false, 'error', 'invalid template_id');
|
|
54
|
+
END;
|
|
55
|
+
v_is_update := v_template_id IS NOT NULL;
|
|
56
|
+
v_name := btrim(p_payload->>'name');
|
|
57
|
+
v_category := lower(COALESCE(NULLIF(p_payload->>'category',''), 'general'));
|
|
58
|
+
v_description := left(p_payload->>'description', 1000);
|
|
59
|
+
IF v_category NOT IN ('anamnesis','evolution','report','contract','general') THEN
|
|
60
|
+
v_category := 'general';
|
|
61
|
+
END IF;
|
|
62
|
+
|
|
63
|
+
IF NOT v_is_update AND (v_name IS NULL OR v_name = '') THEN
|
|
64
|
+
RETURN jsonb_build_object('ok', false, 'error', 'name is required to create a form');
|
|
65
|
+
END IF;
|
|
66
|
+
|
|
67
|
+
-- ── authorization: role → plan → form_templates cap ──────────────────────
|
|
68
|
+
SELECT count(*) INTO v_used FROM plg_forms_templates
|
|
69
|
+
WHERE tenant_id = p_tenant_id AND is_deleted = false;
|
|
70
|
+
v_denial := agent_guard(p_tenant_id, p_actor_user_id, 'custom_forms',
|
|
71
|
+
CASE WHEN v_is_update THEN 'update' ELSE 'create' END,
|
|
72
|
+
'form_templates', v_used, CASE WHEN v_is_update THEN 0 ELSE 1 END);
|
|
73
|
+
IF v_denial IS NOT NULL THEN
|
|
74
|
+
RETURN jsonb_build_object('ok', false, 'denial', v_denial);
|
|
75
|
+
END IF;
|
|
76
|
+
|
|
77
|
+
-- ── current fields (for APPEND / to reject an unknown id) ────────────────
|
|
78
|
+
IF v_is_update THEN
|
|
79
|
+
SELECT COALESCE(schema->'fields','[]'::jsonb) INTO v_existing
|
|
80
|
+
FROM plg_forms_templates
|
|
81
|
+
WHERE id = v_template_id AND tenant_id = p_tenant_id AND is_deleted = false;
|
|
82
|
+
IF v_existing IS NULL THEN
|
|
83
|
+
RETURN jsonb_build_object('ok', false, 'error', 'unknown template for this tenant');
|
|
84
|
+
END IF;
|
|
85
|
+
ELSE
|
|
86
|
+
v_existing := '[]'::jsonb;
|
|
87
|
+
END IF;
|
|
88
|
+
|
|
89
|
+
-- ── normalize the incoming fields to the builder's FormFieldDef shape ────
|
|
90
|
+
-- (id, type, label, required, placeholder, col, colSpan, options) — row is
|
|
91
|
+
-- assigned last, across the merged list.
|
|
92
|
+
WITH src AS (
|
|
93
|
+
SELECT CASE
|
|
94
|
+
WHEN p_payload ? 'fields' THEN COALESCE(p_payload->'fields','[]'::jsonb)
|
|
95
|
+
WHEN p_payload ? 'append_fields' THEN COALESCE(p_payload->'append_fields','[]'::jsonb)
|
|
96
|
+
ELSE '[]'::jsonb
|
|
97
|
+
END AS arr
|
|
98
|
+
),
|
|
99
|
+
norm AS (
|
|
100
|
+
SELECT f.ord, jsonb_strip_nulls(jsonb_build_object(
|
|
101
|
+
'id', COALESCE(NULLIF(f.value->>'id',''), gen_random_uuid()::text),
|
|
102
|
+
'type', COALESCE(NULLIF(f.value->>'type',''), 'text'),
|
|
103
|
+
'label', COALESCE(NULLIF(f.value->>'label',''), 'Campo'),
|
|
104
|
+
'required', COALESCE((f.value->>'required')::boolean, false),
|
|
105
|
+
'placeholder', NULLIF(f.value->>'placeholder',''),
|
|
106
|
+
'col', 0,
|
|
107
|
+
'colSpan', 12,
|
|
108
|
+
'options', CASE
|
|
109
|
+
WHEN jsonb_typeof(f.value->'options') = 'array' AND jsonb_array_length(f.value->'options') > 0
|
|
110
|
+
THEN (SELECT jsonb_agg(jsonb_build_object(
|
|
111
|
+
'label', opt,
|
|
112
|
+
'value', lower(regexp_replace(btrim(opt), '\s+', '_', 'g'))))
|
|
113
|
+
FROM jsonb_array_elements_text(f.value->'options') opt)
|
|
114
|
+
ELSE NULL END
|
|
115
|
+
)) AS field
|
|
116
|
+
FROM src, jsonb_array_elements(src.arr) WITH ORDINALITY AS f(value, ord)
|
|
117
|
+
)
|
|
118
|
+
SELECT COALESCE(jsonb_agg(field ORDER BY ord), '[]'::jsonb) INTO v_input FROM norm;
|
|
119
|
+
|
|
120
|
+
-- REPLACE with `fields`; otherwise APPEND onto what's already there.
|
|
121
|
+
IF p_payload ? 'fields' THEN
|
|
122
|
+
v_merged := v_input;
|
|
123
|
+
ELSE
|
|
124
|
+
v_merged := v_existing || v_input;
|
|
125
|
+
END IF;
|
|
126
|
+
|
|
127
|
+
-- sequential rows across the final list
|
|
128
|
+
SELECT COALESCE(jsonb_agg(jsonb_set(e.value, '{row}', to_jsonb((e.ord - 1))) ORDER BY e.ord), '[]'::jsonb)
|
|
129
|
+
INTO v_fields
|
|
130
|
+
FROM jsonb_array_elements(v_merged) WITH ORDINALITY AS e(value, ord);
|
|
131
|
+
|
|
132
|
+
v_schema := jsonb_build_object('layout', jsonb_build_object('columns', 12), 'fields', v_fields);
|
|
133
|
+
|
|
134
|
+
-- ── write + audit ────────────────────────────────────────────────────────
|
|
135
|
+
IF v_is_update THEN
|
|
136
|
+
UPDATE plg_forms_templates SET
|
|
137
|
+
name = COALESCE(NULLIF(v_name,''), name),
|
|
138
|
+
category = v_category,
|
|
139
|
+
description = COALESCE(v_description, description),
|
|
140
|
+
schema = v_schema,
|
|
141
|
+
updated_by = p_actor_user_id,
|
|
142
|
+
updated_at = now()
|
|
143
|
+
WHERE id = v_template_id AND tenant_id = p_tenant_id AND is_deleted = false
|
|
144
|
+
RETURNING id INTO v_final_id;
|
|
145
|
+
ELSE
|
|
146
|
+
INSERT INTO plg_forms_templates (tenant_id, name, category, description, schema, created_by, updated_by)
|
|
147
|
+
VALUES (p_tenant_id, v_name, v_category, v_description, v_schema, p_actor_user_id, p_actor_user_id)
|
|
148
|
+
RETURNING id INTO v_final_id;
|
|
149
|
+
END IF;
|
|
150
|
+
|
|
151
|
+
INSERT INTO audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)
|
|
152
|
+
VALUES (p_tenant_id, p_actor_user_id,
|
|
153
|
+
CASE WHEN v_is_update THEN 'agent.updateFormTemplate' ELSE 'agent.createFormTemplate' END,
|
|
154
|
+
'form_template', v_final_id::text,
|
|
155
|
+
jsonb_build_object('payload', p_payload));
|
|
156
|
+
|
|
157
|
+
RETURN jsonb_build_object(
|
|
158
|
+
'ok', true,
|
|
159
|
+
'id', v_final_id,
|
|
160
|
+
'record', jsonb_build_object(
|
|
161
|
+
'ref', jsonb_build_object('id', v_final_id, 'resource', 'plg_forms_templates',
|
|
162
|
+
'archetype', 'custom_forms:template'),
|
|
163
|
+
'name', COALESCE(NULLIF(v_name,''), (SELECT name FROM plg_forms_templates WHERE id = v_final_id)),
|
|
164
|
+
'category', v_category,
|
|
165
|
+
'fieldCount', jsonb_array_length(v_fields)
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
END;
|
|
169
|
+
$$;
|
|
170
|
+
|
|
171
|
+
REVOKE ALL ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb) FROM public;
|
|
172
|
+
REVOKE EXECUTE ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb) FROM anon;
|
|
173
|
+
GRANT EXECUTE ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb)
|
|
174
|
+
TO authenticated, service_role;
|