@fayz-ai/plugin-forms 0.9.0 → 0.9.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 +3 -3
- package/dist/{FormBuilder-7SWZQKFN.js → FormBuilder-JCNHXQXT.js} +4 -4
- package/dist/{FormBuilder-7SWZQKFN.js.map → FormBuilder-JCNHXQXT.js.map} +1 -1
- package/dist/{chunk-W2ZNJGQC.js → chunk-JCAWSDX6.js} +3 -3
- package/dist/chunk-JCAWSDX6.js.map +1 -0
- package/dist/components/AddDocumentDropdown.d.ts +1 -1
- package/dist/components/AddDocumentDropdown.d.ts.map +1 -1
- package/dist/{CustomFormsContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-tools.d.ts.map +1 -0
- package/dist/lib/document-types.d.ts.map +1 -0
- package/package.json +5 -7
- package/dist/CustomFormsContext.d.ts.map +0 -1
- package/dist/agent-tools.d.ts.map +0 -1
- package/dist/chunk-W2ZNJGQC.js.map +0 -1
- package/dist/document-types.d.ts.map +0 -1
- package/src/CustomFormsContext.tsx +0 -28
- package/src/agent-tools.ts +0 -73
- package/src/components/AddDocumentDropdown.tsx +0 -92
- package/src/components/BuilderCanvas.tsx +0 -87
- package/src/components/BuilderField.tsx +0 -81
- package/src/components/DocumentFormDialog.tsx +0 -155
- package/src/components/DocumentList.tsx +0 -254
- package/src/components/FieldConfigDialog.tsx +0 -243
- package/src/components/FieldPalette.tsx +0 -81
- package/src/components/FormBuilder.tsx +0 -427
- package/src/components/FormRenderer.tsx +0 -256
- package/src/components/FormViewer.tsx +0 -94
- package/src/components/PersonDocumentsWidget.tsx +0 -229
- package/src/components/TemplateSelector.tsx +0 -91
- package/src/config.ts +0 -43
- package/src/data/index.ts +0 -3
- package/src/data/mock.ts +0 -193
- package/src/data/supabase.ts +0 -408
- package/src/data/tables.ts +0 -7
- package/src/data/types.ts +0 -33
- package/src/document-types.ts +0 -51
- package/src/index.ts +0 -234
- package/src/lib/person-fields.ts +0 -67
- package/src/lib/print.ts +0 -207
- package/src/lib/tenant.ts +0 -9
- package/src/locales/en.ts +0 -95
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -95
- package/src/migrations/000_plg_rename.sql +0 -21
- package/src/migrations/001_frm_base.sql +0 -174
- package/src/migrations/002_document_archetype.sql +0 -223
- package/src/migrations/003_agent_rpcs.sql +0 -174
- package/src/migrations/index.ts +0 -610
- package/src/store.ts +0 -167
- package/src/types.ts +0 -217
- package/src/views/CustomFormsSettingsTab.tsx +0 -105
- package/src/views/TemplateListView.tsx +0 -174
- /package/dist/{agent-tools.d.ts → lib/agent-tools.d.ts} +0 -0
- /package/dist/{document-types.d.ts → lib/document-types.d.ts} +0 -0
package/src/document-types.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type React from 'react'
|
|
2
|
-
|
|
3
|
-
// ============================================================
|
|
4
|
-
// Document Type Registry — extensible pattern for plugins
|
|
5
|
-
// to contribute document types to the "Add Document" dropdown
|
|
6
|
-
// ============================================================
|
|
7
|
-
|
|
8
|
-
export interface DocumentTypeOption {
|
|
9
|
-
/** Unique id, e.g. 'custom_forms:template:abc123' or 'core:image' */
|
|
10
|
-
id: string
|
|
11
|
-
label: string
|
|
12
|
-
icon: React.ElementType
|
|
13
|
-
/** Group label for dropdown sections (e.g. 'Formulários', 'Arquivos') */
|
|
14
|
-
group: string
|
|
15
|
-
/** Group sort order — lower groups appear first */
|
|
16
|
-
groupOrder?: number
|
|
17
|
-
/** Sort order within group */
|
|
18
|
-
order?: number
|
|
19
|
-
/** Description shown below label */
|
|
20
|
-
description?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface DocumentTypeProvider {
|
|
24
|
-
/** Unique provider id, e.g. 'custom_forms', 'core_files' */
|
|
25
|
-
id: string
|
|
26
|
-
/** Returns available document types for a given person */
|
|
27
|
-
getTypes(personId: string): DocumentTypeOption[] | Promise<DocumentTypeOption[]>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ── Global registry ────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
const providers: DocumentTypeProvider[] = []
|
|
33
|
-
|
|
34
|
-
export function registerDocumentTypeProvider(provider: DocumentTypeProvider) {
|
|
35
|
-
if (providers.find((p) => p.id === provider.id)) return
|
|
36
|
-
providers.push(provider)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getDocumentTypeProviders(): DocumentTypeProvider[] {
|
|
40
|
-
return providers
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function getAllDocumentTypes(personId: string): Promise<DocumentTypeOption[]> {
|
|
44
|
-
const results = await Promise.all(
|
|
45
|
-
providers.map((p) => Promise.resolve(p.getTypes(personId))),
|
|
46
|
-
)
|
|
47
|
-
const all = results.flat()
|
|
48
|
-
// Sort by groupOrder then order within group
|
|
49
|
-
all.sort((a, b) => (a.groupOrder ?? 50) - (b.groupOrder ?? 50) || (a.order ?? 0) - (b.order ?? 0))
|
|
50
|
-
return all
|
|
51
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import type { PluginManifest } from '@fayz-ai/core'
|
|
3
|
-
import type { CustomFormsDataProvider } from './data/types'
|
|
4
|
-
import { T } from './data/tables'
|
|
5
|
-
import { createMockFormsProvider } from './data/mock'
|
|
6
|
-
import { createSupabaseFormsProvider } from './data/supabase'
|
|
7
|
-
import { createSafeDataProvider, registerTranslations } from '@fayz-ai/core'
|
|
8
|
-
import { PluginSettingsPanel } from '@fayz-ai/saas'
|
|
9
|
-
import { createCustomFormsStore } from './store'
|
|
10
|
-
import { customFormsLocales } from './locales'
|
|
11
|
-
import { resolveConfig } from './config'
|
|
12
|
-
import type { CustomFormsPluginOptions } from './config'
|
|
13
|
-
import { CustomFormsSettingsTab } from './views/CustomFormsSettingsTab'
|
|
14
|
-
import { PersonDocumentsWidget as PersonDocumentsWidgetImpl } from './components/PersonDocumentsWidget'
|
|
15
|
-
import { registerDocumentTypeProvider } from './document-types'
|
|
16
|
-
import { createOrUpdateFormTool } from './agent-tools'
|
|
17
|
-
import { FileText as FileTextIcon, Image as ImageIcon, Paperclip } from 'lucide-react'
|
|
18
|
-
|
|
19
|
-
function registerBuiltInProviders(formsProvider: CustomFormsDataProvider) {
|
|
20
|
-
// Templates from custom_forms
|
|
21
|
-
registerDocumentTypeProvider({
|
|
22
|
-
id: 'custom_forms:templates',
|
|
23
|
-
async getTypes() {
|
|
24
|
-
try {
|
|
25
|
-
const result = await formsProvider.getTemplates({})
|
|
26
|
-
return result.data.map((tpl) => ({
|
|
27
|
-
id: `template:${tpl.id}`,
|
|
28
|
-
label: tpl.name,
|
|
29
|
-
icon: FileTextIcon,
|
|
30
|
-
group: 'Formulários',
|
|
31
|
-
groupOrder: 10,
|
|
32
|
-
order: 0,
|
|
33
|
-
description: tpl.description,
|
|
34
|
-
_templateId: tpl.id,
|
|
35
|
-
})) as any[]
|
|
36
|
-
} catch {
|
|
37
|
-
return []
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// Built-in file types
|
|
43
|
-
registerDocumentTypeProvider({
|
|
44
|
-
id: 'core:files',
|
|
45
|
-
getTypes() {
|
|
46
|
-
return [
|
|
47
|
-
{ id: 'core:image', label: 'Imagem', icon: ImageIcon, group: 'Arquivos', groupOrder: 90, order: 0 },
|
|
48
|
-
{ id: 'core:attachment', label: 'Anexo', icon: Paperclip, group: 'Arquivos', groupOrder: 90, order: 1 },
|
|
49
|
-
]
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export type { CustomFormsPluginOptions }
|
|
55
|
-
export { registerDocumentTypeProvider, type DocumentTypeOption, type DocumentTypeProvider } from './document-types'
|
|
56
|
-
|
|
57
|
-
export function createCustomFormsPlugin(options?: CustomFormsPluginOptions): PluginManifest {
|
|
58
|
-
registerTranslations(customFormsLocales)
|
|
59
|
-
const config = resolveConfig(options)
|
|
60
|
-
const provider = options?.dataProvider ?? createSafeDataProvider(
|
|
61
|
-
() => createSupabaseFormsProvider(),
|
|
62
|
-
() => createMockFormsProvider(),
|
|
63
|
-
)
|
|
64
|
-
const store = createCustomFormsStore(provider)
|
|
65
|
-
|
|
66
|
-
// Register built-in document type providers
|
|
67
|
-
registerBuiltInProviders(provider)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const formRegistries: import('@fayz-ai/core').PluginRegistryDef[] = [
|
|
71
|
-
{
|
|
72
|
-
id: 'form-categories',
|
|
73
|
-
entity: {
|
|
74
|
-
name: 'Category',
|
|
75
|
-
namePlural: 'Categories',
|
|
76
|
-
icon: 'FolderOpen',
|
|
77
|
-
displayField: 'name',
|
|
78
|
-
defaultSort: 'sort_order',
|
|
79
|
-
fields: [
|
|
80
|
-
{ key: 'name', label: 'Name', type: 'text' as const, required: true, showInTable: true },
|
|
81
|
-
{ key: 'icon', label: 'Icon', type: 'text' as const, showInTable: true },
|
|
82
|
-
{ key: 'color', label: 'Color', type: 'color' as const, showInTable: true },
|
|
83
|
-
{ key: 'sortOrder', label: 'Order', type: 'number' as const, showInTable: true, defaultValue: 0 },
|
|
84
|
-
{ key: 'isActive', label: 'Active', type: 'boolean' as const, showInTable: true, defaultValue: true, inlineToggle: true },
|
|
85
|
-
],
|
|
86
|
-
data: {
|
|
87
|
-
table: T.categories,
|
|
88
|
-
tenantScoped: true,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
icon: 'FolderOpen',
|
|
92
|
-
description: 'Categories for organizing form templates',
|
|
93
|
-
seedData: [
|
|
94
|
-
{ name: 'Anamnese', sortOrder: 1, isActive: true },
|
|
95
|
-
{ name: 'Evolução', sortOrder: 2, isActive: true },
|
|
96
|
-
{ name: 'Laudo', sortOrder: 3, isActive: true },
|
|
97
|
-
{ name: 'Contrato', sortOrder: 4, isActive: true },
|
|
98
|
-
{ name: 'Geral', sortOrder: 5, isActive: true },
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
const registries = [...formRegistries, ...(options?.settingsRegistries ?? [])]
|
|
104
|
-
|
|
105
|
-
const SettingsComponent: React.FC = () =>
|
|
106
|
-
React.createElement(PluginSettingsPanel, {
|
|
107
|
-
title: config.labels.settingsLabel,
|
|
108
|
-
subtitle: config.labels.settingsSubtitle,
|
|
109
|
-
// The settings shell already renders the section label as the page H1;
|
|
110
|
-
// hide the panel's own duplicate title (keeps the subtitle).
|
|
111
|
-
hideTitle: true,
|
|
112
|
-
customTabs: [
|
|
113
|
-
{
|
|
114
|
-
id: 'forms',
|
|
115
|
-
label: config.labels.pageTitle,
|
|
116
|
-
icon: 'FileText',
|
|
117
|
-
content: React.createElement(CustomFormsSettingsTab, { config, provider, store }),
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
registries,
|
|
121
|
-
routeBase: '/settings/custom_forms',
|
|
122
|
-
hostPluginId: 'custom_forms',
|
|
123
|
-
})
|
|
124
|
-
SettingsComponent.displayName = 'CustomFormsSettingsPanel'
|
|
125
|
-
|
|
126
|
-
const PersonDocumentsWidget: React.FC<any> = (props: any) =>
|
|
127
|
-
React.createElement(PersonDocumentsWidgetImpl, { ...props, config, provider, store })
|
|
128
|
-
PersonDocumentsWidget.displayName = 'PersonDocumentsWidget'
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
id: 'custom_forms',
|
|
132
|
-
name: config.labels.pageTitle,
|
|
133
|
-
icon: 'FileText',
|
|
134
|
-
version: '1.0.0',
|
|
135
|
-
scope: options?.scope ?? 'universal',
|
|
136
|
-
verticalId: options?.verticalId,
|
|
137
|
-
defaultEnabled: true,
|
|
138
|
-
dependencies: [],
|
|
139
|
-
declaredFeatures: [
|
|
140
|
-
{ id: 'custom_forms', label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
141
|
-
],
|
|
142
|
-
declaredLimits: [
|
|
143
|
-
{ key: 'form_templates', label: 'Form templates', table: 'plg_forms_templates' },
|
|
144
|
-
{ key: 'documents_month', label: 'Documents / month', table: 'documents', period: 'month' },
|
|
145
|
-
],
|
|
146
|
-
|
|
147
|
-
navigation: [],
|
|
148
|
-
|
|
149
|
-
routes: [],
|
|
150
|
-
|
|
151
|
-
settings: [
|
|
152
|
-
{
|
|
153
|
-
id: 'custom_forms',
|
|
154
|
-
label: config.labels.settingsLabel,
|
|
155
|
-
icon: 'FileText',
|
|
156
|
-
component: SettingsComponent as unknown as React.ComponentType<unknown>,
|
|
157
|
-
order: 15,
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
|
|
161
|
-
widgets: [
|
|
162
|
-
{
|
|
163
|
-
id: 'person-documents-tab',
|
|
164
|
-
zone: 'person.detail.documents',
|
|
165
|
-
component: PersonDocumentsWidget,
|
|
166
|
-
order: 0,
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
|
|
170
|
-
registries,
|
|
171
|
-
|
|
172
|
-
aiTools: [
|
|
173
|
-
{
|
|
174
|
-
id: 'custom_forms.list-templates',
|
|
175
|
-
name: 'listFormTemplates',
|
|
176
|
-
description: 'Lists available form templates for the current tenant.',
|
|
177
|
-
icon: 'FileText',
|
|
178
|
-
mode: 'read' as const,
|
|
179
|
-
category: 'Forms',
|
|
180
|
-
parameters: {
|
|
181
|
-
type: 'object' as const,
|
|
182
|
-
properties: {
|
|
183
|
-
category: {
|
|
184
|
-
type: 'string' as const,
|
|
185
|
-
description: 'Filter by category: anamnesis, evolution, report, contract, general',
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
suggestions: [
|
|
190
|
-
{ label: 'What forms do we have?' },
|
|
191
|
-
{ label: 'Show me anamnesis templates' },
|
|
192
|
-
],
|
|
193
|
-
permission: { feature: 'custom_forms', action: 'read' as const },
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
id: 'custom_forms.list-documents',
|
|
197
|
-
name: 'listDocuments',
|
|
198
|
-
description: 'Lists filled documents, optionally filtered by person or status.',
|
|
199
|
-
icon: 'FileText',
|
|
200
|
-
mode: 'read' as const,
|
|
201
|
-
category: 'Forms',
|
|
202
|
-
parameters: {
|
|
203
|
-
type: 'object' as const,
|
|
204
|
-
properties: {
|
|
205
|
-
personId: { type: 'string' as const, description: 'Person UUID to filter by' },
|
|
206
|
-
status: {
|
|
207
|
-
type: 'string' as const,
|
|
208
|
-
description: 'Status filter: draft, completed, signed, archived',
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
suggestions: [
|
|
213
|
-
{ label: "Show this client's documents" },
|
|
214
|
-
],
|
|
215
|
-
permission: { feature: 'custom_forms', action: 'read' as const },
|
|
216
|
-
},
|
|
217
|
-
// Server-plane write: builds/edits a form template via the pool RPC, so
|
|
218
|
-
// the assistant can montar formulários from any page or channel.
|
|
219
|
-
createOrUpdateFormTool,
|
|
220
|
-
],
|
|
221
|
-
|
|
222
|
-
declaredRpcs: [
|
|
223
|
-
{
|
|
224
|
-
name: 'agent_forms_upsert_template',
|
|
225
|
-
kind: 'write' as const,
|
|
226
|
-
description:
|
|
227
|
-
'Guarded form-template upsert: agent_guard (role→plan→form_templates cap), grid layout computed server-side, create or edit by id, audited.',
|
|
228
|
-
audits: true,
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
|
|
232
|
-
locales: customFormsLocales,
|
|
233
|
-
}
|
|
234
|
-
}
|
package/src/lib/person-fields.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// PERSON FIELD MAPPING — prefill a document from a person record
|
|
3
|
-
// ============================================================
|
|
4
|
-
//
|
|
5
|
-
// A template field can declare `map: '<personKey>'` (see FormFieldDef.map).
|
|
6
|
-
// When a document is created from a person's profile, fields with a mapping are
|
|
7
|
-
// pre-filled from that person's record. The person record reaching the widget
|
|
8
|
-
// is the host app's CRUD detail `item`, keyed by the entity's field keys
|
|
9
|
-
// (camelCase, e.g. `documentNumber`). We resolve tolerantly so the same mapping
|
|
10
|
-
// works whether the host exposes camelCase or snake_case keys.
|
|
11
|
-
|
|
12
|
-
import type { FormSchema } from '../types'
|
|
13
|
-
|
|
14
|
-
/** Curated, vertical-agnostic person keys offered in the field builder. */
|
|
15
|
-
export const PERSON_FIELD_OPTIONS: Array<{ value: string; labelKey: string }> = [
|
|
16
|
-
{ value: 'name', labelKey: 'customForms.personField.name' },
|
|
17
|
-
{ value: 'documentNumber', labelKey: 'customForms.personField.documentNumber' },
|
|
18
|
-
{ value: 'email', labelKey: 'customForms.personField.email' },
|
|
19
|
-
{ value: 'phone', labelKey: 'customForms.personField.phone' },
|
|
20
|
-
{ value: 'dateOfBirth', labelKey: 'customForms.personField.dateOfBirth' },
|
|
21
|
-
{ value: 'guardianName', labelKey: 'customForms.personField.guardianName' },
|
|
22
|
-
{ value: 'address', labelKey: 'customForms.personField.address' },
|
|
23
|
-
{ value: 'city', labelKey: 'customForms.personField.city' },
|
|
24
|
-
{ value: 'state', labelKey: 'customForms.personField.state' },
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
const toSnake = (k: string) => k.replace(/[A-Z]/g, (m) => '_' + m.toLowerCase())
|
|
28
|
-
const toCamel = (k: string) => k.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
|
|
29
|
-
|
|
30
|
-
/** Read a person field value, tolerating camelCase/snake_case key variants. */
|
|
31
|
-
export function resolvePersonValue(
|
|
32
|
-
person: Record<string, unknown> | undefined | null,
|
|
33
|
-
mapKey: string | undefined,
|
|
34
|
-
): unknown {
|
|
35
|
-
if (!person || !mapKey) return undefined
|
|
36
|
-
for (const key of [mapKey, toSnake(mapKey), toCamel(mapKey)]) {
|
|
37
|
-
const v = person[key]
|
|
38
|
-
if (v != null && v !== '') return v
|
|
39
|
-
}
|
|
40
|
-
return undefined
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Build the initial document data for a template. For each field, applies (in
|
|
45
|
-
* priority order) an existing value, then the mapped person value, then the
|
|
46
|
-
* field's static `defaultValue`. Never overwrites a value already present in
|
|
47
|
-
* `existing`.
|
|
48
|
-
*/
|
|
49
|
-
export function prefillFromPerson(
|
|
50
|
-
schema: FormSchema | undefined,
|
|
51
|
-
person: Record<string, unknown> | undefined | null,
|
|
52
|
-
existing: Record<string, unknown> = {},
|
|
53
|
-
): Record<string, unknown> {
|
|
54
|
-
const data: Record<string, unknown> = { ...existing }
|
|
55
|
-
if (!schema) return data
|
|
56
|
-
for (const field of schema.fields) {
|
|
57
|
-
const current = data[field.id]
|
|
58
|
-
if (current != null && current !== '') continue
|
|
59
|
-
const mapped = field.map ? resolvePersonValue(person, field.map) : undefined
|
|
60
|
-
if (mapped != null) {
|
|
61
|
-
data[field.id] = mapped
|
|
62
|
-
} else if (field.defaultValue != null) {
|
|
63
|
-
data[field.id] = field.defaultValue
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return data
|
|
67
|
-
}
|
package/src/lib/print.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Current tenant id holder (runtime DI), decoupling the plugin from saas-core's
|
|
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
|
-
|
|
7
|
-
let currentTenantId: string | undefined
|
|
8
|
-
export function setFormsTenantId(id: string | undefined): void { currentTenantId = id }
|
|
9
|
-
export function getFormsTenantId(): string | undefined { return currentTenantId ?? getActiveTenantId() }
|
package/src/locales/en.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
export const en: Record<string, string> = {
|
|
2
|
-
// Settings
|
|
3
|
-
'customForms.settingsTitle': 'Forms & Documents',
|
|
4
|
-
'customForms.settingsSubtitle': 'Create and manage custom forms for your business',
|
|
5
|
-
|
|
6
|
-
// Templates
|
|
7
|
-
'customForms.templates': 'Templates',
|
|
8
|
-
'customForms.newTemplate': 'New Template',
|
|
9
|
-
'customForms.editTemplate': 'Edit Template',
|
|
10
|
-
'customForms.templateName': 'Form name',
|
|
11
|
-
'customForms.templateDescription': 'Description',
|
|
12
|
-
'customForms.templateCategory': 'Category',
|
|
13
|
-
'customForms.templateSpecialty': 'Specialty',
|
|
14
|
-
'customForms.noTemplates': 'No form templates yet',
|
|
15
|
-
'customForms.noTemplatesDescription': 'Create your first custom form to start collecting structured data.',
|
|
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.',
|
|
18
|
-
|
|
19
|
-
// Categories
|
|
20
|
-
'customForms.category.anamnesis': 'Anamnesis',
|
|
21
|
-
'customForms.category.evolution': 'Evolution Note',
|
|
22
|
-
'customForms.category.report': 'Report',
|
|
23
|
-
'customForms.category.contract': 'Contract',
|
|
24
|
-
'customForms.category.general': 'General',
|
|
25
|
-
|
|
26
|
-
// Builder
|
|
27
|
-
'customForms.builder.fields': 'Fields',
|
|
28
|
-
'customForms.builder.fieldCount': '{{count}} fields',
|
|
29
|
-
'customForms.builder.settings': 'Settings',
|
|
30
|
-
'customForms.builder.save': 'Save',
|
|
31
|
-
'customForms.builder.saving': 'Saving...',
|
|
32
|
-
'customForms.builder.saved': 'Form saved',
|
|
33
|
-
'customForms.builder.emptyCanvas': 'Drag fields from the left panel to start building your form',
|
|
34
|
-
'customForms.builder.fieldLabel': 'Field label',
|
|
35
|
-
'customForms.builder.fieldPlaceholder': 'Placeholder',
|
|
36
|
-
'customForms.builder.fieldRequired': 'Required',
|
|
37
|
-
'customForms.builder.fieldOptions': 'Options',
|
|
38
|
-
'customForms.builder.addOption': 'Add option',
|
|
39
|
-
'customForms.builder.colSpan': 'Column span',
|
|
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',
|
|
57
|
-
|
|
58
|
-
// Field types
|
|
59
|
-
'customForms.fieldType.title': 'Title',
|
|
60
|
-
'customForms.fieldType.text': 'Simple Text',
|
|
61
|
-
'customForms.fieldType.memo': 'Memo',
|
|
62
|
-
'customForms.fieldType.richtext': 'Rich Text',
|
|
63
|
-
'customForms.fieldType.date': 'Date',
|
|
64
|
-
'customForms.fieldType.select': 'Select',
|
|
65
|
-
'customForms.fieldType.radio': 'Radio',
|
|
66
|
-
'customForms.fieldType.tags': 'Tags',
|
|
67
|
-
'customForms.fieldType.checkbox': 'Checkbox',
|
|
68
|
-
'customForms.fieldType.image': 'Image / Drawing',
|
|
69
|
-
'customForms.fieldType.gallery': 'Gallery',
|
|
70
|
-
'customForms.fieldType.budget': 'Budget',
|
|
71
|
-
|
|
72
|
-
// Documents
|
|
73
|
-
'customForms.documents': 'Documents',
|
|
74
|
-
'customForms.addDocument': 'Add Document',
|
|
75
|
-
'customForms.selectTemplate': 'Select a form',
|
|
76
|
-
'customForms.noDocuments': 'No documents yet',
|
|
77
|
-
'customForms.noDocumentsDescription': 'Add a document to start recording information.',
|
|
78
|
-
'customForms.documentTitle': 'Document title',
|
|
79
|
-
'customForms.saveAsDraft': 'Save as Draft',
|
|
80
|
-
'customForms.saveAndComplete': 'Save & Complete',
|
|
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',
|
|
85
|
-
|
|
86
|
-
// Registry
|
|
87
|
-
'registry.form-categories': 'Categories',
|
|
88
|
-
'registry.form-categories.description': 'Categories for organizing form templates',
|
|
89
|
-
|
|
90
|
-
// Status
|
|
91
|
-
'customForms.status.draft': 'Draft',
|
|
92
|
-
'customForms.status.completed': 'Completed',
|
|
93
|
-
'customForms.status.signed': 'Signed',
|
|
94
|
-
'customForms.status.archived': 'Archived',
|
|
95
|
-
}
|