@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/src/lib/print.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// PRINT / PDF — render a filled document to a printable A4 page
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// Browser print-to-PDF: we build a clean, self-contained A4 HTML document from
|
|
6
|
+
// the template schema + saved data, drop it into a hidden iframe, and call
|
|
7
|
+
// print() on it. The browser's print dialog covers both physical printing and
|
|
8
|
+
// "Save as PDF". No external deps.
|
|
9
|
+
|
|
10
|
+
import type { FormDocument, FormFieldDef, FormTemplate } from '../types'
|
|
11
|
+
|
|
12
|
+
function esc(value: unknown): string {
|
|
13
|
+
return String(value ?? '').replace(/[&<>"']/g, (c) =>
|
|
14
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] as string),
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nl2br(value: string): string {
|
|
19
|
+
return esc(value).replace(/\r?\n/g, '<br/>')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatDate(value: string): string {
|
|
23
|
+
// ISO date (YYYY-MM-DD) → locale date; anything else passes through.
|
|
24
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value)
|
|
25
|
+
if (!m) return esc(value)
|
|
26
|
+
return `${m[3]}/${m[2]}/${m[1]}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function optionLabel(field: FormFieldDef, value: unknown): string {
|
|
30
|
+
const opt = (field.options ?? []).find((o) => o.value === value)
|
|
31
|
+
return esc(opt ? opt.label : value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Render one field's value as an HTML fragment for the printed page. */
|
|
35
|
+
function renderValue(field: FormFieldDef, value: unknown): string {
|
|
36
|
+
const empty = '<span class="empty">—</span>'
|
|
37
|
+
switch (field.type) {
|
|
38
|
+
case 'memo':
|
|
39
|
+
case 'richtext':
|
|
40
|
+
return value ? `<div class="long">${nl2br(String(value))}</div>` : empty
|
|
41
|
+
case 'date':
|
|
42
|
+
return value ? formatDate(String(value)) : empty
|
|
43
|
+
case 'select':
|
|
44
|
+
case 'radio':
|
|
45
|
+
return value ? optionLabel(field, value) : empty
|
|
46
|
+
case 'tags': {
|
|
47
|
+
const arr = Array.isArray(value) ? value : []
|
|
48
|
+
return arr.length
|
|
49
|
+
? arr.map((v) => optionLabel(field, v)).join(', ')
|
|
50
|
+
: empty
|
|
51
|
+
}
|
|
52
|
+
case 'checkbox':
|
|
53
|
+
return value ? 'Sim' : 'Não'
|
|
54
|
+
case 'image':
|
|
55
|
+
case 'gallery':
|
|
56
|
+
case 'budget':
|
|
57
|
+
return '' // not meaningful on a printed contract
|
|
58
|
+
default:
|
|
59
|
+
return value ? esc(value) : empty
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BuildPrintOptions {
|
|
64
|
+
/** Heading shown at the top of the page (defaults to the document title). */
|
|
65
|
+
heading?: string
|
|
66
|
+
/** Small line under the heading (e.g. person name). */
|
|
67
|
+
subheading?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build a full, self-contained printable HTML document. */
|
|
71
|
+
export function buildPrintHtml(
|
|
72
|
+
template: FormTemplate,
|
|
73
|
+
doc: FormDocument,
|
|
74
|
+
opts: BuildPrintOptions = {},
|
|
75
|
+
): string {
|
|
76
|
+
const heading = opts.heading ?? doc.title ?? template.name
|
|
77
|
+
const subParts = [opts.subheading ?? doc.personName, new Date(doc.createdAt).toLocaleDateString()]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map((s) => esc(s))
|
|
80
|
+
.join(' · ')
|
|
81
|
+
|
|
82
|
+
const fields = [...template.schema.fields].sort((a, b) => a.row - b.row || a.col - b.col)
|
|
83
|
+
|
|
84
|
+
const clampSpan = (n: unknown) => Math.max(1, Math.min(12, Number(n) || 12))
|
|
85
|
+
|
|
86
|
+
// Mirror the builder's 12-column grid so the print keeps the authored layout
|
|
87
|
+
// (side-by-side fields stay side by side). Titles span the full row.
|
|
88
|
+
const body = fields
|
|
89
|
+
.map((field) => {
|
|
90
|
+
if (field.type === 'title') {
|
|
91
|
+
return `<h2 class="section" style="grid-column: 1 / -1">${esc(field.label)}</h2>`
|
|
92
|
+
}
|
|
93
|
+
const rendered = renderValue(field, doc.data[field.id])
|
|
94
|
+
if (rendered === '') return ''
|
|
95
|
+
const isLong = field.type === 'memo' || field.type === 'richtext'
|
|
96
|
+
return `<div class="field${isLong ? ' field--long' : ''}" style="grid-column: span ${clampSpan(field.colSpan)}">
|
|
97
|
+
<div class="label">${esc(field.label)}</div>
|
|
98
|
+
<div class="value">${rendered}</div>
|
|
99
|
+
</div>`
|
|
100
|
+
})
|
|
101
|
+
.join('\n')
|
|
102
|
+
|
|
103
|
+
return `<!doctype html>
|
|
104
|
+
<html lang="pt-BR">
|
|
105
|
+
<head>
|
|
106
|
+
<meta charset="utf-8"/>
|
|
107
|
+
<title>${esc(heading)}</title>
|
|
108
|
+
<style>
|
|
109
|
+
@page { size: A4; margin: 20mm; }
|
|
110
|
+
* { box-sizing: border-box; }
|
|
111
|
+
html, body { margin: 0; padding: 0; }
|
|
112
|
+
body {
|
|
113
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
114
|
+
color: #111; font-size: 12pt; line-height: 1.5;
|
|
115
|
+
-webkit-print-color-adjust: exact; print-color-adjust: exact;
|
|
116
|
+
}
|
|
117
|
+
.page { max-width: 170mm; margin: 0 auto; padding: 12mm 0; }
|
|
118
|
+
header { border-bottom: 2px solid #111; padding-bottom: 8px; margin-bottom: 20px; }
|
|
119
|
+
header h1 { font-size: 18pt; margin: 0 0 2px; }
|
|
120
|
+
header .sub { font-size: 10pt; color: #555; }
|
|
121
|
+
/* 12-column grid — mirrors the builder Preview (FormRenderer) so the print
|
|
122
|
+
keeps the authored column layout. */
|
|
123
|
+
.grid { display: grid; grid-template-columns: repeat(12, 1fr); column-gap: 16px; row-gap: 12px; align-items: start; }
|
|
124
|
+
h2.section { font-size: 13pt; margin: 22px 0 4px; padding-bottom: 3px; border-bottom: 1px solid #ccc; }
|
|
125
|
+
.grid > h2.section:first-child { margin-top: 4px; }
|
|
126
|
+
.field { margin: 0; min-width: 0; }
|
|
127
|
+
.field .label { font-size: 9pt; text-transform: uppercase; letter-spacing: .04em; color: #666; margin-bottom: 2px; }
|
|
128
|
+
.field .value { font-size: 12pt; }
|
|
129
|
+
.field--long .value .long { white-space: normal; text-align: justify; }
|
|
130
|
+
.empty { color: #bbb; }
|
|
131
|
+
footer { margin-top: 40px; font-size: 9pt; color: #888; text-align: center; }
|
|
132
|
+
@media screen { body { background: #f3f3f3; } .page { background: #fff; max-width: 210mm; min-height: 297mm; padding: 20mm; box-shadow: 0 1px 8px rgba(0,0,0,.15); } }
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<div class="page">
|
|
137
|
+
<header>
|
|
138
|
+
<h1>${esc(heading)}</h1>
|
|
139
|
+
${subParts ? `<div class="sub">${subParts}</div>` : ''}
|
|
140
|
+
</header>
|
|
141
|
+
<div class="grid">
|
|
142
|
+
${body}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</body>
|
|
146
|
+
</html>`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Print a prebuilt HTML document via a hidden iframe (popup-blocker safe).
|
|
151
|
+
* The browser print dialog handles both printing and "Save as PDF".
|
|
152
|
+
*/
|
|
153
|
+
export function printHtml(html: string): void {
|
|
154
|
+
if (typeof document === 'undefined') return
|
|
155
|
+
const iframe = document.createElement('iframe')
|
|
156
|
+
iframe.setAttribute('aria-hidden', 'true')
|
|
157
|
+
iframe.style.position = 'fixed'
|
|
158
|
+
iframe.style.right = '0'
|
|
159
|
+
iframe.style.bottom = '0'
|
|
160
|
+
iframe.style.width = '0'
|
|
161
|
+
iframe.style.height = '0'
|
|
162
|
+
iframe.style.border = '0'
|
|
163
|
+
document.body.appendChild(iframe)
|
|
164
|
+
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
// Give the print dialog a beat before tearing the frame down.
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe)
|
|
169
|
+
}, 1000)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const win = iframe.contentWindow
|
|
173
|
+
const idoc = iframe.contentDocument || win?.document
|
|
174
|
+
if (!win || !idoc) {
|
|
175
|
+
cleanup()
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
idoc.open()
|
|
180
|
+
idoc.write(html)
|
|
181
|
+
idoc.close()
|
|
182
|
+
|
|
183
|
+
const doPrint = () => {
|
|
184
|
+
try {
|
|
185
|
+
win.focus()
|
|
186
|
+
win.print()
|
|
187
|
+
} finally {
|
|
188
|
+
cleanup()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Wait for the frame document to be ready before printing.
|
|
193
|
+
if (idoc.readyState === 'complete') {
|
|
194
|
+
setTimeout(doPrint, 50)
|
|
195
|
+
} else {
|
|
196
|
+
iframe.addEventListener('load', () => setTimeout(doPrint, 50), { once: true })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Convenience: build + print a document in one call. */
|
|
201
|
+
export function printDocument(
|
|
202
|
+
template: FormTemplate,
|
|
203
|
+
doc: FormDocument,
|
|
204
|
+
opts?: BuildPrintOptions,
|
|
205
|
+
): void {
|
|
206
|
+
printHtml(buildPrintHtml(template, doc, opts))
|
|
207
|
+
}
|
package/src/lib/tenant.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// Current tenant id holder (runtime DI), decoupling the plugin from saas-core's
|
|
2
|
-
// organization.store.
|
|
2
|
+
// organization.store. Falls back to the core active-tenant when the host has
|
|
3
|
+
// not injected one explicitly (mirrors plugin-crm's resolver), so the settings
|
|
4
|
+
// template list — gated on this id — loads without extra host wiring.
|
|
5
|
+
import { getActiveTenantId } from '@fayz-ai/core'
|
|
6
|
+
|
|
3
7
|
let currentTenantId: string | undefined
|
|
4
8
|
export function setFormsTenantId(id: string | undefined): void { currentTenantId = id }
|
|
5
|
-
export function getFormsTenantId(): string | undefined { return currentTenantId }
|
|
9
|
+
export function getFormsTenantId(): string | undefined { return currentTenantId ?? getActiveTenantId() }
|
package/src/locales/en.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const en: Record<string, string> = {
|
|
|
14
14
|
'customForms.noTemplates': 'No form templates yet',
|
|
15
15
|
'customForms.noTemplatesDescription': 'Create your first custom form to start collecting structured data.',
|
|
16
16
|
'customForms.deleteTemplateConfirm': 'Delete this form template? Existing documents will be preserved.',
|
|
17
|
+
'customForms.archiveTemplateConfirm': 'Archive this form template? It leaves the list and the document picker, but is kept and existing documents are preserved.',
|
|
17
18
|
|
|
18
19
|
// Categories
|
|
19
20
|
'customForms.category.anamnesis': 'Anamnesis',
|
|
@@ -37,6 +38,22 @@ export const en: Record<string, string> = {
|
|
|
37
38
|
'customForms.builder.addOption': 'Add option',
|
|
38
39
|
'customForms.builder.colSpan': 'Column span',
|
|
39
40
|
'customForms.builder.deleteField': 'Remove field',
|
|
41
|
+
'customForms.builder.mapToPerson': 'Auto-fill from record',
|
|
42
|
+
'customForms.builder.mapNone': "Don't auto-fill",
|
|
43
|
+
'customForms.builder.mapHint': "When creating the document from a person's profile, this field is pre-filled with the chosen value (still editable).",
|
|
44
|
+
'customForms.preview.editor': 'Editor',
|
|
45
|
+
'customForms.preview.print': 'Print',
|
|
46
|
+
|
|
47
|
+
// Person field mappings (auto-fill)
|
|
48
|
+
'customForms.personField.name': 'Name',
|
|
49
|
+
'customForms.personField.documentNumber': 'ID / Document',
|
|
50
|
+
'customForms.personField.email': 'Email',
|
|
51
|
+
'customForms.personField.phone': 'Phone',
|
|
52
|
+
'customForms.personField.dateOfBirth': 'Date of Birth',
|
|
53
|
+
'customForms.personField.guardianName': 'Guardian',
|
|
54
|
+
'customForms.personField.address': 'Address',
|
|
55
|
+
'customForms.personField.city': 'City',
|
|
56
|
+
'customForms.personField.state': 'State',
|
|
40
57
|
|
|
41
58
|
// Field types
|
|
42
59
|
'customForms.fieldType.title': 'Title',
|
|
@@ -62,6 +79,9 @@ export const en: Record<string, string> = {
|
|
|
62
79
|
'customForms.saveAsDraft': 'Save as Draft',
|
|
63
80
|
'customForms.saveAndComplete': 'Save & Complete',
|
|
64
81
|
'customForms.deleteDocumentConfirm': 'Delete this document?',
|
|
82
|
+
'customForms.archive': 'Archive',
|
|
83
|
+
'customForms.archiveDocumentConfirm': 'Archive this document? It leaves the active list but is kept on file.',
|
|
84
|
+
'customForms.printPdf': 'Print / PDF',
|
|
65
85
|
|
|
66
86
|
// Registry
|
|
67
87
|
'registry.form-categories': 'Categories',
|
package/src/locales/pt-BR.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const ptBR: Record<string, string> = {
|
|
|
14
14
|
'customForms.noTemplates': 'Nenhum modelo criado',
|
|
15
15
|
'customForms.noTemplatesDescription': 'Crie seu primeiro formulário personalizado para começar a coletar dados estruturados.',
|
|
16
16
|
'customForms.deleteTemplateConfirm': 'Excluir este modelo de formulário? Documentos existentes serão preservados.',
|
|
17
|
+
'customForms.archiveTemplateConfirm': 'Arquivar este modelo? Ele sai da lista e do seletor de documentos, mas fica guardado e os documentos existentes são preservados.',
|
|
17
18
|
|
|
18
19
|
// Categories
|
|
19
20
|
'customForms.category.anamnesis': 'Anamnese',
|
|
@@ -37,6 +38,22 @@ export const ptBR: Record<string, string> = {
|
|
|
37
38
|
'customForms.builder.addOption': 'Adicionar opção',
|
|
38
39
|
'customForms.builder.colSpan': 'Largura em colunas',
|
|
39
40
|
'customForms.builder.deleteField': 'Remover campo',
|
|
41
|
+
'customForms.builder.mapToPerson': 'Preencher com dado da ficha',
|
|
42
|
+
'customForms.builder.mapNone': 'Não preencher',
|
|
43
|
+
'customForms.builder.mapHint': 'Ao criar o documento na ficha da pessoa, este campo já vem preenchido com o dado escolhido (editável).',
|
|
44
|
+
'customForms.preview.editor': 'Editor',
|
|
45
|
+
'customForms.preview.print': 'Impressão',
|
|
46
|
+
|
|
47
|
+
// Person field mappings (auto-fill)
|
|
48
|
+
'customForms.personField.name': 'Nome',
|
|
49
|
+
'customForms.personField.documentNumber': 'CPF / Documento',
|
|
50
|
+
'customForms.personField.email': 'E-mail',
|
|
51
|
+
'customForms.personField.phone': 'Telefone',
|
|
52
|
+
'customForms.personField.dateOfBirth': 'Data de Nascimento',
|
|
53
|
+
'customForms.personField.guardianName': 'Responsável',
|
|
54
|
+
'customForms.personField.address': 'Endereço',
|
|
55
|
+
'customForms.personField.city': 'Cidade',
|
|
56
|
+
'customForms.personField.state': 'Estado',
|
|
40
57
|
|
|
41
58
|
// Field types
|
|
42
59
|
'customForms.fieldType.title': 'Título',
|
|
@@ -62,6 +79,9 @@ export const ptBR: Record<string, string> = {
|
|
|
62
79
|
'customForms.saveAsDraft': 'Salvar Rascunho',
|
|
63
80
|
'customForms.saveAndComplete': 'Salvar e Finalizar',
|
|
64
81
|
'customForms.deleteDocumentConfirm': 'Excluir este documento?',
|
|
82
|
+
'customForms.archive': 'Arquivar',
|
|
83
|
+
'customForms.archiveDocumentConfirm': 'Arquivar este documento? Ele sai da lista ativa mas fica guardado.',
|
|
84
|
+
'customForms.printPdf': 'Imprimir / PDF',
|
|
65
85
|
|
|
66
86
|
// Registry
|
|
67
87
|
'registry.form-categories': 'Categorias',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- 000_plg_rename.sql — rename legacy forms tables to plg_forms_* for pools
|
|
2
|
+
-- provisioned before the industry-pool rename. Guarded: fires only when the legacy
|
|
3
|
+
-- name exists and the target does not, so fresh pools skip every branch.
|
|
4
|
+
-- The core `documents` archetype table is NOT renamed — it stays public.documents.
|
|
5
|
+
DO $$
|
|
6
|
+
BEGIN
|
|
7
|
+
IF to_regclass('public.frm_documents') IS NOT NULL AND to_regclass('public.plg_forms_documents') IS NULL THEN
|
|
8
|
+
ALTER TABLE public.frm_documents RENAME TO plg_forms_documents;
|
|
9
|
+
END IF;
|
|
10
|
+
IF to_regclass('public.frm_templates') IS NOT NULL AND to_regclass('public.plg_forms_templates') IS NULL THEN
|
|
11
|
+
ALTER TABLE public.frm_templates RENAME TO plg_forms_templates;
|
|
12
|
+
END IF;
|
|
13
|
+
IF to_regclass('public.frm_document_files') IS NOT NULL AND to_regclass('public.plg_forms_document_files') IS NULL THEN
|
|
14
|
+
ALTER TABLE public.frm_document_files RENAME TO plg_forms_document_files;
|
|
15
|
+
END IF;
|
|
16
|
+
-- frm_categories: registry-declared (form-template categories); no base-table
|
|
17
|
+
-- DDL ships in this plugin, but rename in place if a pool created one.
|
|
18
|
+
IF to_regclass('public.frm_categories') IS NOT NULL AND to_regclass('public.plg_forms_categories') IS NULL THEN
|
|
19
|
+
ALTER TABLE public.frm_categories RENAME TO plg_forms_categories;
|
|
20
|
+
END IF;
|
|
21
|
+
END $$;
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
-- Custom Forms Plugin — Base Tables
|
|
3
3
|
-- ============================================================
|
|
4
4
|
|
|
5
|
-
--
|
|
6
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
5
|
+
-- plg_forms_templates: form template definitions (versioned)
|
|
6
|
+
CREATE TABLE IF NOT EXISTS public.plg_forms_templates (
|
|
7
7
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
8
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
8
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
9
9
|
name text NOT NULL,
|
|
10
10
|
description text,
|
|
11
11
|
category text NOT NULL DEFAULT 'general',
|
|
12
12
|
version integer NOT NULL DEFAULT 1,
|
|
13
13
|
is_current boolean NOT NULL DEFAULT true,
|
|
14
|
-
parent_id uuid REFERENCES public.
|
|
14
|
+
parent_id uuid REFERENCES public.plg_forms_templates(id),
|
|
15
15
|
schema jsonb NOT NULL DEFAULT '{"fields":[],"layout":{"columns":12}}',
|
|
16
16
|
specialty text,
|
|
17
17
|
tags text[] DEFAULT '{}',
|
|
@@ -24,37 +24,41 @@ CREATE TABLE IF NOT EXISTS public.frm_templates (
|
|
|
24
24
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
-
ALTER TABLE public.
|
|
27
|
+
ALTER TABLE public.plg_forms_templates ENABLE ROW LEVEL SECURITY;
|
|
28
28
|
|
|
29
|
-
CREATE INDEX IF NOT EXISTS
|
|
30
|
-
ON public.
|
|
31
|
-
CREATE INDEX IF NOT EXISTS
|
|
32
|
-
ON public.
|
|
33
|
-
CREATE INDEX IF NOT EXISTS
|
|
34
|
-
ON public.
|
|
35
|
-
CREATE INDEX IF NOT EXISTS
|
|
36
|
-
ON public.
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_templates_tenant
|
|
30
|
+
ON public.plg_forms_templates(tenant_id);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_templates_parent
|
|
32
|
+
ON public.plg_forms_templates(parent_id);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_templates_category
|
|
34
|
+
ON public.plg_forms_templates(tenant_id, category);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_templates_current
|
|
36
|
+
ON public.plg_forms_templates(tenant_id, is_current, is_active)
|
|
37
37
|
WHERE is_current = true AND is_active = true AND is_deleted = false;
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
DROP POLICY IF EXISTS "plg_forms_templates_select" ON public.plg_forms_templates;
|
|
40
|
+
CREATE POLICY "plg_forms_templates_select" ON public.plg_forms_templates
|
|
40
41
|
FOR SELECT TO authenticated
|
|
41
42
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
42
|
-
|
|
43
|
+
DROP POLICY IF EXISTS "plg_forms_templates_insert" ON public.plg_forms_templates;
|
|
44
|
+
CREATE POLICY "plg_forms_templates_insert" ON public.plg_forms_templates
|
|
43
45
|
FOR INSERT TO authenticated
|
|
44
46
|
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
45
|
-
|
|
47
|
+
DROP POLICY IF EXISTS "plg_forms_templates_update" ON public.plg_forms_templates;
|
|
48
|
+
CREATE POLICY "plg_forms_templates_update" ON public.plg_forms_templates
|
|
46
49
|
FOR UPDATE TO authenticated
|
|
47
50
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
48
|
-
|
|
51
|
+
DROP POLICY IF EXISTS "plg_forms_templates_delete" ON public.plg_forms_templates;
|
|
52
|
+
CREATE POLICY "plg_forms_templates_delete" ON public.plg_forms_templates
|
|
49
53
|
FOR DELETE TO authenticated
|
|
50
54
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
51
55
|
|
|
52
|
-
--
|
|
53
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
56
|
+
-- plg_forms_documents: filled form instances
|
|
57
|
+
CREATE TABLE IF NOT EXISTS public.plg_forms_documents (
|
|
54
58
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
55
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
56
|
-
template_id uuid NOT NULL REFERENCES public.
|
|
57
|
-
person_id uuid REFERENCES
|
|
59
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
60
|
+
template_id uuid NOT NULL REFERENCES public.plg_forms_templates(id),
|
|
61
|
+
person_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
58
62
|
title text,
|
|
59
63
|
data jsonb NOT NULL DEFAULT '{}',
|
|
60
64
|
status text NOT NULL DEFAULT 'draft',
|
|
@@ -69,35 +73,51 @@ CREATE TABLE IF NOT EXISTS public.frm_documents (
|
|
|
69
73
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
70
74
|
);
|
|
71
75
|
|
|
72
|
-
ALTER TABLE public.
|
|
76
|
+
ALTER TABLE public.plg_forms_documents ENABLE ROW LEVEL SECURITY;
|
|
73
77
|
|
|
74
|
-
CREATE INDEX IF NOT EXISTS
|
|
75
|
-
ON public.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_documents_tenant
|
|
79
|
+
ON public.plg_forms_documents(tenant_id);
|
|
80
|
+
-- person_id/status only exist in the pre-archetype shape this file creates;
|
|
81
|
+
-- converted pools (salon) already carry the archetype extension shape
|
|
82
|
+
-- (document_id PK, no person_id) — 002 owns that shape, so guard these.
|
|
83
|
+
DO $$
|
|
84
|
+
BEGIN
|
|
85
|
+
IF EXISTS (
|
|
86
|
+
SELECT 1 FROM information_schema.columns
|
|
87
|
+
WHERE table_schema = 'public' AND table_name = 'plg_forms_documents'
|
|
88
|
+
AND column_name = 'person_id'
|
|
89
|
+
) THEN
|
|
90
|
+
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_plg_forms_documents_person
|
|
91
|
+
ON public.plg_forms_documents(person_id)';
|
|
92
|
+
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_plg_forms_documents_status
|
|
93
|
+
ON public.plg_forms_documents(tenant_id, status)';
|
|
94
|
+
END IF;
|
|
95
|
+
END $$;
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_documents_template
|
|
97
|
+
ON public.plg_forms_documents(template_id);
|
|
82
98
|
|
|
83
|
-
|
|
99
|
+
DROP POLICY IF EXISTS "plg_forms_documents_select" ON public.plg_forms_documents;
|
|
100
|
+
CREATE POLICY "plg_forms_documents_select" ON public.plg_forms_documents
|
|
84
101
|
FOR SELECT TO authenticated
|
|
85
102
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
86
|
-
|
|
103
|
+
DROP POLICY IF EXISTS "plg_forms_documents_insert" ON public.plg_forms_documents;
|
|
104
|
+
CREATE POLICY "plg_forms_documents_insert" ON public.plg_forms_documents
|
|
87
105
|
FOR INSERT TO authenticated
|
|
88
106
|
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
89
|
-
|
|
107
|
+
DROP POLICY IF EXISTS "plg_forms_documents_update" ON public.plg_forms_documents;
|
|
108
|
+
CREATE POLICY "plg_forms_documents_update" ON public.plg_forms_documents
|
|
90
109
|
FOR UPDATE TO authenticated
|
|
91
110
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
92
|
-
|
|
111
|
+
DROP POLICY IF EXISTS "plg_forms_documents_delete" ON public.plg_forms_documents;
|
|
112
|
+
CREATE POLICY "plg_forms_documents_delete" ON public.plg_forms_documents
|
|
93
113
|
FOR DELETE TO authenticated
|
|
94
114
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
95
115
|
|
|
96
|
-
--
|
|
97
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
116
|
+
-- plg_forms_document_files: file attachments for image/gallery/drawing fields
|
|
117
|
+
CREATE TABLE IF NOT EXISTS public.plg_forms_document_files (
|
|
98
118
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
99
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
100
|
-
document_id uuid NOT NULL REFERENCES public.
|
|
119
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
120
|
+
document_id uuid NOT NULL REFERENCES public.plg_forms_documents(id) ON DELETE CASCADE,
|
|
101
121
|
field_key text NOT NULL,
|
|
102
122
|
file_url text NOT NULL,
|
|
103
123
|
file_name text,
|
|
@@ -108,31 +128,47 @@ CREATE TABLE IF NOT EXISTS public.frm_document_files (
|
|
|
108
128
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
109
129
|
);
|
|
110
130
|
|
|
111
|
-
ALTER TABLE public.
|
|
131
|
+
ALTER TABLE public.plg_forms_document_files ENABLE ROW LEVEL SECURITY;
|
|
112
132
|
|
|
113
|
-
CREATE INDEX IF NOT EXISTS
|
|
114
|
-
ON public.
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_plg_forms_document_files_document
|
|
134
|
+
ON public.plg_forms_document_files(document_id);
|
|
115
135
|
|
|
116
|
-
|
|
136
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_select" ON public.plg_forms_document_files;
|
|
137
|
+
CREATE POLICY "plg_forms_document_files_select" ON public.plg_forms_document_files
|
|
117
138
|
FOR SELECT TO authenticated
|
|
118
139
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
119
|
-
|
|
140
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_insert" ON public.plg_forms_document_files;
|
|
141
|
+
CREATE POLICY "plg_forms_document_files_insert" ON public.plg_forms_document_files
|
|
120
142
|
FOR INSERT TO authenticated
|
|
121
143
|
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
122
|
-
|
|
144
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_update" ON public.plg_forms_document_files;
|
|
145
|
+
CREATE POLICY "plg_forms_document_files_update" ON public.plg_forms_document_files
|
|
123
146
|
FOR UPDATE TO authenticated
|
|
124
147
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
125
|
-
|
|
148
|
+
DROP POLICY IF EXISTS "plg_forms_document_files_delete" ON public.plg_forms_document_files;
|
|
149
|
+
CREATE POLICY "plg_forms_document_files_delete" ON public.plg_forms_document_files
|
|
126
150
|
FOR DELETE TO authenticated
|
|
127
151
|
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
128
152
|
|
|
129
|
-
-- View:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
-- View: pre-archetype read model (needs person_id; archetype pools get
|
|
154
|
+
-- v_documents from 002 instead, which also drops this view when migrating).
|
|
155
|
+
DO $$
|
|
156
|
+
BEGIN
|
|
157
|
+
IF EXISTS (
|
|
158
|
+
SELECT 1 FROM information_schema.columns
|
|
159
|
+
WHERE table_schema = 'public' AND table_name = 'plg_forms_documents'
|
|
160
|
+
AND column_name = 'person_id'
|
|
161
|
+
) THEN
|
|
162
|
+
EXECUTE $v$
|
|
163
|
+
CREATE OR REPLACE VIEW public.v_frm_documents AS
|
|
164
|
+
SELECT
|
|
165
|
+
d.*,
|
|
166
|
+
t.name AS template_name,
|
|
167
|
+
t.category AS template_category,
|
|
168
|
+
p.name AS person_name
|
|
169
|
+
FROM public.plg_forms_documents d
|
|
170
|
+
LEFT JOIN public.plg_forms_templates t ON t.id = d.template_id
|
|
171
|
+
LEFT JOIN public.people p ON p.id = d.person_id
|
|
172
|
+
$v$;
|
|
173
|
+
END IF;
|
|
174
|
+
END $$;
|