@fayz-ai/plugin-forms 0.9.0-next.1 → 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.
Files changed (59) hide show
  1. package/README.md +3 -3
  2. package/dist/{FormBuilder-7SWZQKFN.js → FormBuilder-JCNHXQXT.js} +4 -4
  3. package/dist/{FormBuilder-7SWZQKFN.js.map → FormBuilder-JCNHXQXT.js.map} +1 -1
  4. package/dist/{chunk-W2ZNJGQC.js → chunk-JCAWSDX6.js} +3 -3
  5. package/dist/chunk-JCAWSDX6.js.map +1 -0
  6. package/dist/components/AddDocumentDropdown.d.ts +1 -1
  7. package/dist/components/AddDocumentDropdown.d.ts.map +1 -1
  8. package/dist/{CustomFormsContext.d.ts → context.d.ts} +1 -1
  9. package/dist/context.d.ts.map +1 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/agent-tools.d.ts.map +1 -0
  15. package/dist/lib/document-types.d.ts.map +1 -0
  16. package/package.json +5 -7
  17. package/dist/CustomFormsContext.d.ts.map +0 -1
  18. package/dist/agent-tools.d.ts.map +0 -1
  19. package/dist/chunk-W2ZNJGQC.js.map +0 -1
  20. package/dist/document-types.d.ts.map +0 -1
  21. package/src/CustomFormsContext.tsx +0 -28
  22. package/src/agent-tools.ts +0 -73
  23. package/src/components/AddDocumentDropdown.tsx +0 -92
  24. package/src/components/BuilderCanvas.tsx +0 -87
  25. package/src/components/BuilderField.tsx +0 -81
  26. package/src/components/DocumentFormDialog.tsx +0 -155
  27. package/src/components/DocumentList.tsx +0 -254
  28. package/src/components/FieldConfigDialog.tsx +0 -243
  29. package/src/components/FieldPalette.tsx +0 -81
  30. package/src/components/FormBuilder.tsx +0 -427
  31. package/src/components/FormRenderer.tsx +0 -256
  32. package/src/components/FormViewer.tsx +0 -94
  33. package/src/components/PersonDocumentsWidget.tsx +0 -229
  34. package/src/components/TemplateSelector.tsx +0 -91
  35. package/src/config.ts +0 -43
  36. package/src/data/index.ts +0 -3
  37. package/src/data/mock.ts +0 -193
  38. package/src/data/supabase.ts +0 -408
  39. package/src/data/tables.ts +0 -7
  40. package/src/data/types.ts +0 -33
  41. package/src/document-types.ts +0 -51
  42. package/src/index.ts +0 -234
  43. package/src/lib/person-fields.ts +0 -67
  44. package/src/lib/print.ts +0 -207
  45. package/src/lib/tenant.ts +0 -9
  46. package/src/locales/en.ts +0 -95
  47. package/src/locales/index.ts +0 -7
  48. package/src/locales/pt-BR.ts +0 -95
  49. package/src/migrations/000_plg_rename.sql +0 -21
  50. package/src/migrations/001_frm_base.sql +0 -174
  51. package/src/migrations/002_document_archetype.sql +0 -223
  52. package/src/migrations/003_agent_rpcs.sql +0 -174
  53. package/src/migrations/index.ts +0 -610
  54. package/src/store.ts +0 -167
  55. package/src/types.ts +0 -217
  56. package/src/views/CustomFormsSettingsTab.tsx +0 -105
  57. package/src/views/TemplateListView.tsx +0 -174
  58. /package/dist/{agent-tools.d.ts → lib/agent-tools.d.ts} +0 -0
  59. /package/dist/{document-types.d.ts → lib/document-types.d.ts} +0 -0
@@ -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
- }
@@ -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
- ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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(' &middot; ')
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
- }
@@ -1,7 +0,0 @@
1
- import { en } from './en'
2
- import { ptBR } from './pt-BR'
3
-
4
- export const customFormsLocales: Record<string, Record<string, string>> = {
5
- en,
6
- 'pt-BR': ptBR,
7
- }