@fayz-ai/plugin-forms 0.1.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 (99) hide show
  1. package/dist/CustomFormsContext.d.ts +15 -0
  2. package/dist/CustomFormsContext.d.ts.map +1 -0
  3. package/dist/FormBuilder-4VKYVZAC.cjs +619 -0
  4. package/dist/FormBuilder-4VKYVZAC.cjs.map +1 -0
  5. package/dist/FormBuilder-KDANEL6Z.js +613 -0
  6. package/dist/FormBuilder-KDANEL6Z.js.map +1 -0
  7. package/dist/chunk-32I43T37.js +198 -0
  8. package/dist/chunk-32I43T37.js.map +1 -0
  9. package/dist/chunk-XLUULIVL.cjs +200 -0
  10. package/dist/chunk-XLUULIVL.cjs.map +1 -0
  11. package/dist/components/AddDocumentDropdown.d.ts +9 -0
  12. package/dist/components/AddDocumentDropdown.d.ts.map +1 -0
  13. package/dist/components/BuilderCanvas.d.ts +12 -0
  14. package/dist/components/BuilderCanvas.d.ts.map +1 -0
  15. package/dist/components/BuilderField.d.ts +11 -0
  16. package/dist/components/BuilderField.d.ts.map +1 -0
  17. package/dist/components/DocumentFormDialog.d.ts +17 -0
  18. package/dist/components/DocumentFormDialog.d.ts.map +1 -0
  19. package/dist/components/DocumentList.d.ts +14 -0
  20. package/dist/components/DocumentList.d.ts.map +1 -0
  21. package/dist/components/FieldConfigDialog.d.ts +12 -0
  22. package/dist/components/FieldConfigDialog.d.ts.map +1 -0
  23. package/dist/components/FieldPalette.d.ts +10 -0
  24. package/dist/components/FieldPalette.d.ts.map +1 -0
  25. package/dist/components/FormBuilder.d.ts +14 -0
  26. package/dist/components/FormBuilder.d.ts.map +1 -0
  27. package/dist/components/FormRenderer.d.ts +11 -0
  28. package/dist/components/FormRenderer.d.ts.map +1 -0
  29. package/dist/components/FormViewer.d.ts +12 -0
  30. package/dist/components/FormViewer.d.ts.map +1 -0
  31. package/dist/components/PersonDocumentsWidget.d.ts +17 -0
  32. package/dist/components/PersonDocumentsWidget.d.ts.map +1 -0
  33. package/dist/components/TemplateSelector.d.ts +11 -0
  34. package/dist/components/TemplateSelector.d.ts.map +1 -0
  35. package/dist/config.d.ts +23 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/data/index.d.ts +4 -0
  38. package/dist/data/index.d.ts.map +1 -0
  39. package/dist/data/mock.d.ts +3 -0
  40. package/dist/data/mock.d.ts.map +1 -0
  41. package/dist/data/supabase.d.ts +3 -0
  42. package/dist/data/supabase.d.ts.map +1 -0
  43. package/dist/data/types.d.ts +17 -0
  44. package/dist/data/types.d.ts.map +1 -0
  45. package/dist/document-types.d.ts +25 -0
  46. package/dist/document-types.d.ts.map +1 -0
  47. package/dist/index.cjs +1612 -0
  48. package/dist/index.cjs.map +1 -0
  49. package/dist/index.d.ts +6 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +1605 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/lib/tenant.d.ts +3 -0
  54. package/dist/lib/tenant.d.ts.map +1 -0
  55. package/dist/locales/en.d.ts +2 -0
  56. package/dist/locales/en.d.ts.map +1 -0
  57. package/dist/locales/index.d.ts +2 -0
  58. package/dist/locales/index.d.ts.map +1 -0
  59. package/dist/locales/pt-BR.d.ts +2 -0
  60. package/dist/locales/pt-BR.d.ts.map +1 -0
  61. package/dist/store.d.ts +29 -0
  62. package/dist/store.d.ts.map +1 -0
  63. package/dist/types.d.ts +150 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/views/CustomFormsSettingsTab.d.ts +14 -0
  66. package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -0
  67. package/dist/views/TemplateListView.d.ts +13 -0
  68. package/dist/views/TemplateListView.d.ts.map +1 -0
  69. package/package.json +57 -0
  70. package/src/CustomFormsContext.tsx +28 -0
  71. package/src/components/AddDocumentDropdown.tsx +92 -0
  72. package/src/components/BuilderCanvas.tsx +87 -0
  73. package/src/components/BuilderField.tsx +81 -0
  74. package/src/components/DocumentFormDialog.tsx +134 -0
  75. package/src/components/DocumentList.tsx +250 -0
  76. package/src/components/FieldConfigDialog.tsx +203 -0
  77. package/src/components/FieldPalette.tsx +81 -0
  78. package/src/components/FormBuilder.tsx +345 -0
  79. package/src/components/FormRenderer.tsx +256 -0
  80. package/src/components/FormViewer.tsx +84 -0
  81. package/src/components/PersonDocumentsWidget.tsx +220 -0
  82. package/src/components/TemplateSelector.tsx +91 -0
  83. package/src/config.ts +39 -0
  84. package/src/data/index.ts +3 -0
  85. package/src/data/mock.ts +193 -0
  86. package/src/data/supabase.ts +405 -0
  87. package/src/data/types.ts +33 -0
  88. package/src/document-types.ts +51 -0
  89. package/src/index.ts +195 -0
  90. package/src/lib/tenant.ts +5 -0
  91. package/src/locales/en.ts +75 -0
  92. package/src/locales/index.ts +7 -0
  93. package/src/locales/pt-BR.ts +75 -0
  94. package/src/migrations/001_frm_base.sql +138 -0
  95. package/src/migrations/002_document_archetype.sql +135 -0
  96. package/src/store.ts +167 -0
  97. package/src/types.ts +203 -0
  98. package/src/views/CustomFormsSettingsTab.tsx +115 -0
  99. package/src/views/TemplateListView.tsx +163 -0
@@ -0,0 +1,405 @@
1
+ import { getFormsTenantId } from '../lib/tenant'
2
+ import type { CustomFormsDataProvider } from './types'
3
+ import type {
4
+ FormTemplate,
5
+ FormDocument,
6
+ DocumentFile,
7
+ TemplateQuery,
8
+ DocumentQuery,
9
+ PaginatedResult,
10
+ CreateTemplateInput,
11
+ UpdateTemplateInput,
12
+ CreateDocumentInput,
13
+ UpdateDocumentInput,
14
+ } from '../types'
15
+ import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
16
+
17
+ function getTenantId(): string {
18
+ // Local override wins; else use the app's active tenant so writes pass RLS.
19
+ return getFormsTenantId() ?? getActiveTenantId() ?? ''
20
+ }
21
+
22
+ function getClient() {
23
+ const supabase = getSupabaseClientOptional() as any
24
+ if (!supabase) throw new Error('Supabase not initialized')
25
+ return supabase
26
+ }
27
+
28
+
29
+ function mapTemplate(r: any): FormTemplate {
30
+ return {
31
+ id: r.id,
32
+ tenantId: r.tenant_id,
33
+ name: r.name,
34
+ description: r.description,
35
+ category: r.category,
36
+ version: r.version,
37
+ isCurrent: r.is_current,
38
+ parentId: r.parent_id,
39
+ schema: r.schema,
40
+ specialty: r.specialty,
41
+ tags: r.tags ?? [],
42
+ metadata: r.metadata ?? {},
43
+ isActive: r.is_active,
44
+ isDeleted: r.is_deleted,
45
+ createdBy: r.created_by,
46
+ updatedBy: r.updated_by,
47
+ createdAt: r.created_at,
48
+ updatedAt: r.updated_at,
49
+ }
50
+ }
51
+
52
+ function mapDocument(r: any): FormDocument {
53
+ return {
54
+ id: r.id,
55
+ tenantId: r.tenant_id,
56
+ templateId: r.template_id ?? '',
57
+ templateName: r.template_name,
58
+ templateCategory: r.template_category,
59
+ personId: r.person_id,
60
+ personName: r.person_name,
61
+ title: r.title,
62
+ data: r.form_data ?? {},
63
+ status: r.status,
64
+ signedAt: r.signed_at,
65
+ signedBy: r.signed_by,
66
+ notes: r.notes,
67
+ metadata: r.metadata ?? {},
68
+ isDeleted: false,
69
+ createdBy: r.created_by,
70
+ updatedBy: r.updated_by,
71
+ createdAt: r.created_at,
72
+ updatedAt: r.updated_at,
73
+ // Extra fields from core document
74
+ kind: r.kind,
75
+ fileUrl: r.file_url,
76
+ fileName: r.file_name,
77
+ fileSize: r.file_size,
78
+ mimeType: r.mime_type,
79
+ }
80
+ }
81
+
82
+ function mapFile(r: any): DocumentFile {
83
+ return {
84
+ id: r.id,
85
+ tenantId: r.tenant_id,
86
+ documentId: r.document_id,
87
+ fieldKey: r.field_key,
88
+ fileUrl: r.file_url,
89
+ fileName: r.file_name,
90
+ fileSize: r.file_size,
91
+ mimeType: r.mime_type,
92
+ sortOrder: r.sort_order,
93
+ metadata: r.metadata ?? {},
94
+ createdAt: r.created_at,
95
+ }
96
+ }
97
+
98
+ export function createSupabaseFormsProvider(): CustomFormsDataProvider {
99
+ return {
100
+ // ── Templates ──────────────────────────────────────────
101
+
102
+ async getTemplates(query: TemplateQuery): Promise<PaginatedResult<FormTemplate>> {
103
+ const db = getClient()
104
+ let qb = db
105
+ .from('frm_templates')
106
+ .select('*', { count: 'exact' })
107
+ .eq('tenant_id', getTenantId())
108
+ .eq('is_current', true)
109
+ .eq('is_deleted', false)
110
+
111
+ if (query.category) qb = qb.eq('category', query.category)
112
+ if (query.search) qb = qb.ilike('name', `%${query.search}%`)
113
+
114
+ const page = query.page ?? 1
115
+ const pageSize = query.pageSize ?? 25
116
+ qb = qb
117
+ .order('updated_at', { ascending: false })
118
+ .range((page - 1) * pageSize, page * pageSize - 1)
119
+
120
+ const { data, count, error } = await qb
121
+ if (error) throw new Error(error.message)
122
+ return { data: (data ?? []).map(mapTemplate), total: count ?? 0 }
123
+ },
124
+
125
+ async getTemplateById(id: string): Promise<FormTemplate | null> {
126
+ const db = getClient()
127
+ const { data, error } = await db
128
+ .from('frm_templates')
129
+ .select('*')
130
+ .eq('id', id)
131
+ .eq('is_deleted', false)
132
+ .single()
133
+ if (error || !data) return null
134
+ return mapTemplate(data)
135
+ },
136
+
137
+ async createTemplate(input: CreateTemplateInput): Promise<FormTemplate> {
138
+ const db = getClient()
139
+ const tenantId = getTenantId()
140
+ const { data, error } = await db
141
+ .from('frm_templates')
142
+ .insert({
143
+ tenant_id: tenantId,
144
+ name: input.name,
145
+ description: input.description,
146
+ category: input.category,
147
+ schema: input.schema,
148
+ specialty: input.specialty,
149
+ tags: input.tags ?? [],
150
+ metadata: input.metadata ?? {},
151
+ })
152
+ .select()
153
+ .single()
154
+ if (error) throw new Error(error.message)
155
+ return mapTemplate(data)
156
+ },
157
+
158
+ async updateTemplate(id: string, input: UpdateTemplateInput): Promise<FormTemplate> {
159
+ const db = getClient()
160
+ const tenantId = getTenantId()
161
+
162
+ const { count } = await db
163
+ .from('frm_documents')
164
+ .select('document_id', { count: 'exact', head: true })
165
+ .eq('template_id', id)
166
+
167
+ const hasDocuments = (count ?? 0) > 0
168
+
169
+ if (hasDocuments && input.schema) {
170
+ const existing = await this.getTemplateById(id)
171
+ if (!existing) throw new Error('Template not found')
172
+
173
+ const { data: newRow, error: insertError } = await db
174
+ .from('frm_templates')
175
+ .insert({
176
+ tenant_id: tenantId,
177
+ name: input.name ?? existing.name,
178
+ description: input.description ?? existing.description,
179
+ category: input.category ?? existing.category,
180
+ schema: input.schema,
181
+ specialty: input.specialty ?? existing.specialty,
182
+ tags: input.tags ?? existing.tags,
183
+ metadata: input.metadata ?? existing.metadata,
184
+ is_active: input.isActive ?? existing.isActive,
185
+ version: existing.version + 1,
186
+ is_current: true,
187
+ parent_id: existing.parentId ?? existing.id,
188
+ })
189
+ .select()
190
+ .single()
191
+ if (insertError) throw new Error(insertError.message)
192
+
193
+ await db
194
+ .from('frm_templates')
195
+ .update({ is_current: false, updated_at: new Date().toISOString() })
196
+ .eq('id', id)
197
+
198
+ return mapTemplate(newRow)
199
+ }
200
+
201
+ const updates: Record<string, unknown> = { updated_at: new Date().toISOString() }
202
+ if (input.name !== undefined) updates.name = input.name
203
+ if (input.description !== undefined) updates.description = input.description
204
+ if (input.category !== undefined) updates.category = input.category
205
+ if (input.schema !== undefined) updates.schema = input.schema
206
+ if (input.specialty !== undefined) updates.specialty = input.specialty
207
+ if (input.tags !== undefined) updates.tags = input.tags
208
+ if (input.metadata !== undefined) updates.metadata = input.metadata
209
+ if (input.isActive !== undefined) updates.is_active = input.isActive
210
+
211
+ const { data, error } = await db
212
+ .from('frm_templates')
213
+ .update(updates)
214
+ .eq('id', id)
215
+ .select()
216
+ .single()
217
+ if (error) throw new Error(error.message)
218
+ return mapTemplate(data)
219
+ },
220
+
221
+ async deleteTemplate(id: string): Promise<void> {
222
+ const db = getClient()
223
+ const { error } = await db
224
+ .from('frm_templates')
225
+ .update({ is_deleted: true, updated_at: new Date().toISOString() })
226
+ .eq('id', id)
227
+ if (error) throw new Error(error.message)
228
+ },
229
+
230
+ // ── Documents (saas_core.documents + frm_documents extension) ──
231
+
232
+ async getDocuments(query: DocumentQuery): Promise<PaginatedResult<FormDocument>> {
233
+ const db = getClient()
234
+ // Query through the v_documents view (joins core + form extension)
235
+ let qb = db
236
+ .from('v_documents')
237
+ .select('*', { count: 'exact' })
238
+ .eq('tenant_id', getTenantId())
239
+ .eq('is_active', true)
240
+
241
+ if (query.personId) qb = qb.eq('person_id', query.personId)
242
+ if (query.templateId) qb = qb.eq('template_id', query.templateId)
243
+ if (query.status) {
244
+ const statuses = Array.isArray(query.status) ? query.status : [query.status]
245
+ qb = qb.in('status', statuses)
246
+ }
247
+ if (query.search) qb = qb.or(`title.ilike.%${query.search}%,template_name.ilike.%${query.search}%`)
248
+
249
+ const page = query.page ?? 1
250
+ const pageSize = query.pageSize ?? 50
251
+ qb = qb
252
+ .order('created_at', { ascending: false })
253
+ .range((page - 1) * pageSize, page * pageSize - 1)
254
+
255
+ const { data, count, error } = await qb
256
+ if (error) throw new Error(error.message)
257
+ return { data: (data ?? []).map(mapDocument), total: count ?? 0 }
258
+ },
259
+
260
+ async getDocumentById(id: string): Promise<FormDocument | null> {
261
+ const db = getClient()
262
+ const { data, error } = await db
263
+ .from('v_documents')
264
+ .select('*')
265
+ .eq('id', id)
266
+ .single()
267
+ if (error || !data) return null
268
+ return mapDocument(data)
269
+ },
270
+
271
+ async createDocument(input: CreateDocumentInput): Promise<FormDocument> {
272
+ const tenantId = getTenantId()
273
+ const isForm = !!input.templateId
274
+
275
+ // Step 1: Insert into saas_core.documents
276
+ const { data: coreDoc, error: coreError } = await getClient()
277
+ .from('frm_core_documents')
278
+ .insert({
279
+ tenant_id: tenantId,
280
+ kind: input.kind ?? (isForm ? 'form' : 'attachment'),
281
+ person_id: input.personId,
282
+ title: input.title,
283
+ status: input.status ?? 'draft',
284
+ file_url: input.fileUrl,
285
+ file_name: input.fileName,
286
+ file_size: input.fileSize,
287
+ mime_type: input.mimeType,
288
+ metadata: {},
289
+ })
290
+ .select()
291
+ .single()
292
+ if (coreError) throw new Error(coreError.message)
293
+
294
+ // Step 2: For form documents, insert extension row
295
+ if (isForm && input.templateId) {
296
+ const db = getClient()
297
+ const { error: extError } = await db
298
+ .from('frm_documents')
299
+ .insert({
300
+ document_id: coreDoc.id,
301
+ tenant_id: tenantId,
302
+ template_id: input.templateId,
303
+ data: input.data ?? {},
304
+ })
305
+ if (extError) {
306
+ // Rollback core insert
307
+ await getClient().from('frm_core_documents').delete().eq('id', coreDoc.id)
308
+ throw new Error(extError.message)
309
+ }
310
+ }
311
+
312
+ // Fetch via view to get joined data
313
+ const doc = await this.getDocumentById(coreDoc.id)
314
+ return doc!
315
+ },
316
+
317
+ async updateDocument(id: string, input: UpdateDocumentInput): Promise<FormDocument> {
318
+ const now = new Date().toISOString()
319
+
320
+ // Update core document fields
321
+ const coreUpdates: Record<string, unknown> = { updated_at: now }
322
+ if (input.status !== undefined) coreUpdates.status = input.status
323
+ if (input.title !== undefined) coreUpdates.title = input.title
324
+ if (input.notes !== undefined) coreUpdates.notes = input.notes
325
+
326
+ const { error: coreError } = await getClient()
327
+ .from('frm_core_documents')
328
+ .update(coreUpdates)
329
+ .eq('id', id)
330
+ if (coreError) throw new Error(coreError.message)
331
+
332
+ // Update form extension if data is provided
333
+ if (input.data !== undefined) {
334
+ const db = getClient()
335
+ await db
336
+ .from('frm_documents')
337
+ .update({ data: input.data, updated_at: now })
338
+ .eq('document_id', id)
339
+ }
340
+
341
+ const doc = await this.getDocumentById(id)
342
+ return doc!
343
+ },
344
+
345
+ async deleteDocument(id: string): Promise<void> {
346
+ const { error } = await getClient()
347
+ .from('frm_core_documents')
348
+ .update({ is_active: false, updated_at: new Date().toISOString() })
349
+ .eq('id', id)
350
+ if (error) throw new Error(error.message)
351
+ },
352
+
353
+ // ── Files ──────────────────────────────────────────────
354
+
355
+ async getDocumentFiles(documentId: string): Promise<DocumentFile[]> {
356
+ const db = getClient()
357
+ const { data, error } = await db
358
+ .from('frm_document_files')
359
+ .select('*')
360
+ .eq('document_id', documentId)
361
+ .order('sort_order')
362
+ if (error) throw new Error(error.message)
363
+ return (data ?? []).map(mapFile)
364
+ },
365
+
366
+ async uploadFile(documentId: string, fieldKey: string, file: File): Promise<DocumentFile> {
367
+ const db = getClient()
368
+ const tenantId = getTenantId()
369
+ const ext = file.name.split('.').pop() ?? 'bin'
370
+ const storagePath = `${tenantId}/forms/${documentId}/${fieldKey}/${crypto.randomUUID()}.${ext}`
371
+
372
+ const { error: uploadError } = await db.storage
373
+ .from('documents')
374
+ .upload(storagePath, file, { contentType: file.type })
375
+ if (uploadError) throw new Error(uploadError.message)
376
+
377
+ const { data: urlData } = db.storage.from('documents').getPublicUrl(storagePath)
378
+
379
+ const { data, error } = await db
380
+ .from('frm_document_files')
381
+ .insert({
382
+ tenant_id: tenantId,
383
+ document_id: documentId,
384
+ field_key: fieldKey,
385
+ file_url: urlData.publicUrl,
386
+ file_name: file.name,
387
+ file_size: file.size,
388
+ mime_type: file.type,
389
+ })
390
+ .select()
391
+ .single()
392
+ if (error) throw new Error(error.message)
393
+ return mapFile(data)
394
+ },
395
+
396
+ async deleteFile(fileId: string): Promise<void> {
397
+ const db = getClient()
398
+ const { error } = await db
399
+ .from('frm_document_files')
400
+ .delete()
401
+ .eq('id', fileId)
402
+ if (error) throw new Error(error.message)
403
+ },
404
+ }
405
+ }
@@ -0,0 +1,33 @@
1
+ import type {
2
+ FormTemplate,
3
+ FormDocument,
4
+ DocumentFile,
5
+ TemplateQuery,
6
+ DocumentQuery,
7
+ PaginatedResult,
8
+ CreateTemplateInput,
9
+ UpdateTemplateInput,
10
+ CreateDocumentInput,
11
+ UpdateDocumentInput,
12
+ } from '../types'
13
+
14
+ export interface CustomFormsDataProvider {
15
+ // Templates
16
+ getTemplates(query: TemplateQuery): Promise<PaginatedResult<FormTemplate>>
17
+ getTemplateById(id: string): Promise<FormTemplate | null>
18
+ createTemplate(input: CreateTemplateInput): Promise<FormTemplate>
19
+ updateTemplate(id: string, input: UpdateTemplateInput): Promise<FormTemplate>
20
+ deleteTemplate(id: string): Promise<void>
21
+
22
+ // Documents
23
+ getDocuments(query: DocumentQuery): Promise<PaginatedResult<FormDocument>>
24
+ getDocumentById(id: string): Promise<FormDocument | null>
25
+ createDocument(input: CreateDocumentInput): Promise<FormDocument>
26
+ updateDocument(id: string, input: UpdateDocumentInput): Promise<FormDocument>
27
+ deleteDocument(id: string): Promise<void>
28
+
29
+ // Files
30
+ getDocumentFiles(documentId: string): Promise<DocumentFile[]>
31
+ uploadFile(documentId: string, fieldKey: string, file: File): Promise<DocumentFile>
32
+ deleteFile(fileId: string): Promise<void>
33
+ }
@@ -0,0 +1,51 @@
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 ADDED
@@ -0,0 +1,195 @@
1
+ import React from 'react'
2
+ import type { PluginManifest } from '@fayz-ai/core'
3
+ import type { CustomFormsDataProvider } from './data/types'
4
+ import { createMockFormsProvider } from './data/mock'
5
+ import { createSupabaseFormsProvider } from './data/supabase'
6
+ import { createSafeDataProvider, registerTranslations } from '@fayz-ai/core'
7
+ import { createCustomFormsStore } from './store'
8
+ import { customFormsLocales } from './locales'
9
+ import { resolveConfig } from './config'
10
+ import type { CustomFormsPluginOptions } from './config'
11
+ import { CustomFormsSettingsTab } from './views/CustomFormsSettingsTab'
12
+ import { PersonDocumentsWidget as PersonDocumentsWidgetImpl } from './components/PersonDocumentsWidget'
13
+ import { registerDocumentTypeProvider } from './document-types'
14
+ import { FileText as FileTextIcon, Image as ImageIcon, Paperclip } from 'lucide-react'
15
+
16
+ function registerBuiltInProviders(formsProvider: CustomFormsDataProvider) {
17
+ // Templates from custom_forms
18
+ registerDocumentTypeProvider({
19
+ id: 'custom_forms:templates',
20
+ async getTypes() {
21
+ try {
22
+ const result = await formsProvider.getTemplates({})
23
+ return result.data.map((tpl) => ({
24
+ id: `template:${tpl.id}`,
25
+ label: tpl.name,
26
+ icon: FileTextIcon,
27
+ group: 'Formulários',
28
+ groupOrder: 10,
29
+ order: 0,
30
+ description: tpl.description,
31
+ _templateId: tpl.id,
32
+ })) as any[]
33
+ } catch {
34
+ return []
35
+ }
36
+ },
37
+ })
38
+
39
+ // Built-in file types
40
+ registerDocumentTypeProvider({
41
+ id: 'core:files',
42
+ getTypes() {
43
+ return [
44
+ { id: 'core:image', label: 'Imagem', icon: ImageIcon, group: 'Arquivos', groupOrder: 90, order: 0 },
45
+ { id: 'core:attachment', label: 'Anexo', icon: Paperclip, group: 'Arquivos', groupOrder: 90, order: 1 },
46
+ ]
47
+ },
48
+ })
49
+ }
50
+
51
+ export type { CustomFormsPluginOptions }
52
+ export { registerDocumentTypeProvider, type DocumentTypeOption, type DocumentTypeProvider } from './document-types'
53
+
54
+ export function createCustomFormsPlugin(options?: CustomFormsPluginOptions): PluginManifest {
55
+ registerTranslations(customFormsLocales)
56
+ const config = resolveConfig(options)
57
+ const provider = options?.dataProvider ?? createSafeDataProvider(
58
+ () => createSupabaseFormsProvider(),
59
+ () => createMockFormsProvider(),
60
+ )
61
+ const store = createCustomFormsStore(provider)
62
+
63
+ // Register built-in document type providers
64
+ registerBuiltInProviders(provider)
65
+
66
+
67
+ const formRegistries: import('@fayz-ai/core').PluginRegistryDef[] = [
68
+ {
69
+ id: 'form-categories',
70
+ entity: {
71
+ name: 'Category',
72
+ namePlural: 'Categories',
73
+ icon: 'FolderOpen',
74
+ displayField: 'name',
75
+ defaultSort: 'sort_order',
76
+ fields: [
77
+ { key: 'name', label: 'Name', type: 'text' as const, required: true, showInTable: true },
78
+ { key: 'icon', label: 'Icon', type: 'text' as const, showInTable: true },
79
+ { key: 'color', label: 'Color', type: 'color' as const, showInTable: true },
80
+ { key: 'sortOrder', label: 'Order', type: 'number' as const, showInTable: true, defaultValue: 0 },
81
+ { key: 'isActive', label: 'Active', type: 'boolean' as const, showInTable: true, defaultValue: true, inlineToggle: true },
82
+ ],
83
+ data: {
84
+ table: 'frm_categories',
85
+ tenantScoped: true,
86
+ },
87
+ },
88
+ icon: 'FolderOpen',
89
+ description: 'Categories for organizing form templates',
90
+ seedData: [
91
+ { name: 'Anamnese', sortOrder: 1, isActive: true },
92
+ { name: 'Evolução', sortOrder: 2, isActive: true },
93
+ { name: 'Laudo', sortOrder: 3, isActive: true },
94
+ { name: 'Contrato', sortOrder: 4, isActive: true },
95
+ { name: 'Geral', sortOrder: 5, isActive: true },
96
+ ],
97
+ },
98
+ ]
99
+
100
+ const SettingsComponent: React.FC = () =>
101
+ React.createElement(CustomFormsSettingsTab, { config, provider, store, registries: formRegistries })
102
+ SettingsComponent.displayName = 'CustomFormsSettingsTab'
103
+
104
+ const PersonDocumentsWidget: React.FC<any> = (props: any) =>
105
+ React.createElement(PersonDocumentsWidgetImpl, { ...props, config, provider, store })
106
+ PersonDocumentsWidget.displayName = 'PersonDocumentsWidget'
107
+
108
+ return {
109
+ id: 'custom_forms',
110
+ name: config.labels.pageTitle,
111
+ icon: 'FileText',
112
+ version: '1.0.0',
113
+ scope: options?.scope ?? 'universal',
114
+ verticalId: options?.verticalId,
115
+ defaultEnabled: true,
116
+ dependencies: [],
117
+ declaredFeatures: [
118
+ { id: 'custom_forms', label: config.labels.pageTitle, group: config.labels.pageTitle },
119
+ ],
120
+
121
+ navigation: [],
122
+
123
+ routes: [],
124
+
125
+ settings: [
126
+ {
127
+ id: 'custom_forms',
128
+ label: config.labels.settingsLabel,
129
+ icon: 'FileText',
130
+ component: SettingsComponent as unknown as React.ComponentType<unknown>,
131
+ order: 15,
132
+ },
133
+ ],
134
+
135
+ widgets: [
136
+ {
137
+ id: 'person-documents-tab',
138
+ zone: 'person.detail.documents',
139
+ component: PersonDocumentsWidget,
140
+ order: 0,
141
+ },
142
+ ],
143
+
144
+ registries: formRegistries,
145
+
146
+ aiTools: [
147
+ {
148
+ id: 'custom_forms.list-templates',
149
+ name: 'listFormTemplates',
150
+ description: 'Lists available form templates for the current tenant.',
151
+ icon: 'FileText',
152
+ mode: 'read' as const,
153
+ category: 'Forms',
154
+ parameters: {
155
+ type: 'object' as const,
156
+ properties: {
157
+ category: {
158
+ type: 'string' as const,
159
+ description: 'Filter by category: anamnesis, evolution, report, contract, general',
160
+ },
161
+ },
162
+ },
163
+ suggestions: [
164
+ { label: 'What forms do we have?' },
165
+ { label: 'Show me anamnesis templates' },
166
+ ],
167
+ permission: { feature: 'custom_forms', action: 'read' as const },
168
+ },
169
+ {
170
+ id: 'custom_forms.list-documents',
171
+ name: 'listDocuments',
172
+ description: 'Lists filled documents, optionally filtered by person or status.',
173
+ icon: 'FileText',
174
+ mode: 'read' as const,
175
+ category: 'Forms',
176
+ parameters: {
177
+ type: 'object' as const,
178
+ properties: {
179
+ personId: { type: 'string' as const, description: 'Person UUID to filter by' },
180
+ status: {
181
+ type: 'string' as const,
182
+ description: 'Status filter: draft, completed, signed, archived',
183
+ },
184
+ },
185
+ },
186
+ suggestions: [
187
+ { label: "Show this client's documents" },
188
+ ],
189
+ permission: { feature: 'custom_forms', action: 'read' as const },
190
+ },
191
+ ],
192
+
193
+ locales: customFormsLocales,
194
+ }
195
+ }
@@ -0,0 +1,5 @@
1
+ // Current tenant id holder (runtime DI), decoupling the plugin from saas-core's
2
+ // organization.store. Defaults to undefined → Supabase RLS scopes by session.
3
+ let currentTenantId: string | undefined
4
+ export function setFormsTenantId(id: string | undefined): void { currentTenantId = id }
5
+ export function getFormsTenantId(): string | undefined { return currentTenantId }