@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
package/src/types.ts ADDED
@@ -0,0 +1,203 @@
1
+ // ============================================================
2
+ // FORM FIELD TYPES (builder palette)
3
+ // ============================================================
4
+
5
+ export type FormFieldType =
6
+ | 'title' // Section heading (no input)
7
+ | 'text' // Texto Simples
8
+ | 'memo' // Memorando (multiline plain text)
9
+ | 'richtext' // Texto Rico
10
+ | 'date' // Data
11
+ | 'select' // Seleção
12
+ | 'radio' // Radio
13
+ | 'tags' // Tags (multiselect chips)
14
+ | 'checkbox' // Checkbox
15
+ | 'image' // Imagem / Desenho
16
+ | 'gallery' // Galeria
17
+ | 'budget' // Orçamento (line items + prices)
18
+
19
+ // ============================================================
20
+ // FORM FIELD DEFINITION (stored in template.schema.fields[])
21
+ // ============================================================
22
+
23
+ export interface FormFieldDef {
24
+ id: string
25
+ type: FormFieldType
26
+ label: string
27
+ placeholder?: string
28
+ required?: boolean
29
+ /** Start column (0-based) */
30
+ col: number
31
+ /** Row index */
32
+ row: number
33
+ /** Column span (1–12) */
34
+ colSpan: number
35
+ /** Row span (default: 1) */
36
+ rowSpan?: number
37
+ /** Options for select / radio / tags */
38
+ options?: Array<{ label: string; value: string }>
39
+ /** Type-specific configuration */
40
+ config?: Record<string, unknown>
41
+ }
42
+
43
+ export interface FormSchema {
44
+ fields: FormFieldDef[]
45
+ layout: { columns: 12 }
46
+ }
47
+
48
+ // ============================================================
49
+ // TEMPLATE (maps to frm_templates)
50
+ // ============================================================
51
+
52
+ export type TemplateCategory =
53
+ | 'anamnesis'
54
+ | 'evolution'
55
+ | 'report'
56
+ | 'contract'
57
+ | 'general'
58
+
59
+ export interface FormTemplate {
60
+ id: string
61
+ tenantId: string
62
+ name: string
63
+ description?: string
64
+ category: TemplateCategory
65
+ version: number
66
+ isCurrent: boolean
67
+ parentId?: string
68
+ schema: FormSchema
69
+ specialty?: string
70
+ tags: string[]
71
+ metadata: Record<string, unknown>
72
+ isActive: boolean
73
+ isDeleted: boolean
74
+ createdBy?: string
75
+ updatedBy?: string
76
+ createdAt: string
77
+ updatedAt: string
78
+ }
79
+
80
+ // ============================================================
81
+ // DOCUMENT (maps to frm_documents)
82
+ // ============================================================
83
+
84
+ export type DocumentStatus = 'draft' | 'completed' | 'signed' | 'archived'
85
+
86
+ export interface FormDocument {
87
+ id: string
88
+ tenantId: string
89
+ templateId: string
90
+ templateName?: string
91
+ templateCategory?: TemplateCategory
92
+ personId?: string
93
+ personName?: string
94
+ title?: string
95
+ data: Record<string, unknown>
96
+ status: DocumentStatus
97
+ signedAt?: string
98
+ signedBy?: string
99
+ notes?: string
100
+ metadata: Record<string, unknown>
101
+ isDeleted: boolean
102
+ createdBy?: string
103
+ updatedBy?: string
104
+ createdAt: string
105
+ updatedAt: string
106
+ /** Document kind from saas_core.documents */
107
+ kind?: string
108
+ /** File URL for image/attachment documents */
109
+ fileUrl?: string
110
+ fileName?: string
111
+ fileSize?: number
112
+ mimeType?: string
113
+ }
114
+
115
+ // ============================================================
116
+ // DOCUMENT FILE (maps to frm_document_files)
117
+ // ============================================================
118
+
119
+ export interface DocumentFile {
120
+ id: string
121
+ tenantId: string
122
+ documentId: string
123
+ fieldKey: string
124
+ fileUrl: string
125
+ fileName?: string
126
+ fileSize?: number
127
+ mimeType?: string
128
+ sortOrder: number
129
+ metadata: Record<string, unknown>
130
+ createdAt: string
131
+ }
132
+
133
+ // ============================================================
134
+ // QUERIES
135
+ // ============================================================
136
+
137
+ export interface TemplateQuery {
138
+ category?: TemplateCategory
139
+ search?: string
140
+ page?: number
141
+ pageSize?: number
142
+ }
143
+
144
+ export interface DocumentQuery {
145
+ personId?: string
146
+ templateId?: string
147
+ status?: DocumentStatus | DocumentStatus[]
148
+ search?: string
149
+ page?: number
150
+ pageSize?: number
151
+ }
152
+
153
+ export interface PaginatedResult<T> {
154
+ data: T[]
155
+ total: number
156
+ }
157
+
158
+ // ============================================================
159
+ // INPUTS
160
+ // ============================================================
161
+
162
+ export interface CreateTemplateInput {
163
+ name: string
164
+ description?: string
165
+ category: TemplateCategory
166
+ schema: FormSchema
167
+ specialty?: string
168
+ tags?: string[]
169
+ metadata?: Record<string, unknown>
170
+ }
171
+
172
+ export interface UpdateTemplateInput {
173
+ name?: string
174
+ description?: string
175
+ category?: TemplateCategory
176
+ schema?: FormSchema
177
+ specialty?: string
178
+ tags?: string[]
179
+ metadata?: Record<string, unknown>
180
+ isActive?: boolean
181
+ }
182
+
183
+ export interface CreateDocumentInput {
184
+ templateId?: string
185
+ personId?: string
186
+ title?: string
187
+ data?: Record<string, unknown>
188
+ status?: DocumentStatus
189
+ /** Document kind: 'form', 'image', 'attachment', etc. */
190
+ kind?: string
191
+ /** File fields for non-form documents */
192
+ fileUrl?: string
193
+ fileName?: string
194
+ fileSize?: number
195
+ mimeType?: string
196
+ }
197
+
198
+ export interface UpdateDocumentInput {
199
+ data?: Record<string, unknown>
200
+ status?: DocumentStatus
201
+ title?: string
202
+ notes?: string
203
+ }
@@ -0,0 +1,115 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import { TemplateListView } from './TemplateListView'
3
+ import { useTranslation } from '@fayz-ai/core'
4
+ import type { PluginRegistryDef } from '@fayz-ai/core'
5
+ import type { CustomFormsConfig } from '../config'
6
+ import type { CustomFormsDataProvider } from '../data/types'
7
+ import type { CustomFormsStore } from '../store'
8
+ import type { FormTemplate } from '../types'
9
+
10
+ interface CustomFormsSettingsTabProps {
11
+ config: CustomFormsConfig
12
+ provider: CustomFormsDataProvider
13
+ store: CustomFormsStore
14
+ registries?: PluginRegistryDef[]
15
+ }
16
+
17
+ export function CustomFormsSettingsTab({ config, provider, store, registries }: CustomFormsSettingsTabProps) {
18
+ const t = useTranslation()
19
+ const [builderTemplateId, setBuilderTemplateId] = useState<string | null>(null)
20
+
21
+ const handleNew = useCallback(() => {
22
+ setBuilderTemplateId('__new__')
23
+ }, [])
24
+
25
+ const handleEdit = useCallback((template: FormTemplate) => {
26
+ setBuilderTemplateId(template.id)
27
+ }, [])
28
+
29
+ const handleBackFromBuilder = useCallback(() => {
30
+ setBuilderTemplateId(null)
31
+ // Re-fetch templates to reflect any saves
32
+ store.getState().fetchTemplates()
33
+ }, [store])
34
+
35
+ // Sync from hash on mount
36
+ React.useEffect(() => {
37
+ function checkHash() {
38
+ const hash = window.location.hash.slice(1) || '/'
39
+ const builderMatch = hash.match(/\/settings\/custom_forms\/templates\/(.+)/)
40
+ if (builderMatch && builderMatch[1] !== 'new') {
41
+ setBuilderTemplateId(builderMatch[1])
42
+ } else if (builderMatch && builderMatch[1] === 'new') {
43
+ setBuilderTemplateId('__new__')
44
+ }
45
+ }
46
+ checkHash()
47
+ }, [])
48
+
49
+ // Templates tab content — either list or builder
50
+ const templatesContent = builderTemplateId ? (
51
+ <BuilderWrapper
52
+ templateId={builderTemplateId === '__new__' ? undefined : builderTemplateId}
53
+ store={store}
54
+ provider={provider}
55
+ config={config}
56
+ onBack={handleBackFromBuilder}
57
+ />
58
+ ) : (
59
+ <TemplateListView
60
+ store={store}
61
+ config={config}
62
+ onEdit={handleEdit}
63
+ onNew={handleNew}
64
+ />
65
+ )
66
+
67
+ return (
68
+ <div style={{ padding: '24px' }}>
69
+ <div style={{ marginBottom: '16px' }}>
70
+ <h1 style={{ fontSize: '20px', fontWeight: 600, margin: 0 }}>{t('customForms.settingsTitle')}</h1>
71
+ <p style={{ color: 'var(--muted-foreground, #6b7280)', margin: '4px 0 0', fontSize: '14px' }}>
72
+ {t('customForms.settingsSubtitle')}
73
+ </p>
74
+ </div>
75
+ {templatesContent}
76
+ </div>
77
+ )
78
+ }
79
+
80
+ /** Lazy-loads the FormBuilder to avoid pulling @dnd-kit into the initial bundle */
81
+ function BuilderWrapper({
82
+ templateId,
83
+ store,
84
+ provider,
85
+ config,
86
+ onBack,
87
+ }: {
88
+ templateId?: string
89
+ store: CustomFormsStore
90
+ provider: CustomFormsDataProvider
91
+ config: CustomFormsConfig
92
+ onBack: () => void
93
+ }) {
94
+ const FormBuilderLazy = React.lazy(() =>
95
+ import('../components/FormBuilder').then((m) => ({ default: m.FormBuilder })),
96
+ )
97
+
98
+ return (
99
+ <React.Suspense
100
+ fallback={
101
+ <div className="flex items-center justify-center py-12">
102
+ <div className="h-6 w-6 border-2 border-primary border-t-transparent animate-spin rounded-full" />
103
+ </div>
104
+ }
105
+ >
106
+ <FormBuilderLazy
107
+ templateId={templateId}
108
+ store={store}
109
+ provider={provider}
110
+ config={config}
111
+ onBack={onBack}
112
+ />
113
+ </React.Suspense>
114
+ )
115
+ }
@@ -0,0 +1,163 @@
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
+ import { Plus, Pencil, Trash2, FileText, Search } from 'lucide-react'
3
+ import { Button } from '@fayz-ai/ui'
4
+ import { Card, CardContent } from '@fayz-ai/ui'
5
+ import { Badge } from '@fayz-ai/ui'
6
+ import { Input } from '@fayz-ai/ui'
7
+ import { useTranslation } from '@fayz-ai/core'
8
+ import { getFormsTenantId } from '../lib/tenant'
9
+ import type { CustomFormsStore } from '../store'
10
+ import type { CustomFormsConfig } from '../config'
11
+ import type { FormTemplate, TemplateCategory } from '../types'
12
+
13
+ interface TemplateListViewProps {
14
+ store: CustomFormsStore
15
+ config: CustomFormsConfig
16
+ onEdit: (template: FormTemplate) => void
17
+ onNew: () => void
18
+ }
19
+
20
+ const CATEGORY_COLORS: Record<TemplateCategory, string> = {
21
+ anamnesis: 'bg-info-soft text-info-soft-foreground',
22
+ evolution: 'bg-success-soft text-success-soft-foreground',
23
+ report: 'bg-magic-soft text-magic-soft-foreground',
24
+ contract: 'bg-warning-soft text-warning-soft-foreground',
25
+ general: 'bg-muted text-muted-foreground',
26
+ }
27
+
28
+ export function TemplateListView({ store, config, onEdit, onNew }: TemplateListViewProps) {
29
+ const t = useTranslation()
30
+ const [search, setSearch] = useState('')
31
+
32
+ // Subscribe to zustand store reactively
33
+ const templates = store((s) => s.templates)
34
+ const loading = store((s) => s.templatesLoading)
35
+
36
+ const orgId = getFormsTenantId()
37
+
38
+ // Fetch when org is ready or search changes — debounce to avoid Strict Mode double-fire
39
+ useEffect(() => {
40
+ if (!orgId) return
41
+ const timer = setTimeout(() => {
42
+ store.getState().fetchTemplates({ search: search || undefined })
43
+ }, 50)
44
+ return () => clearTimeout(timer)
45
+ }, [orgId, search])
46
+
47
+ const handleDelete = useCallback(async (template: FormTemplate) => {
48
+ if (!window.confirm(t('customForms.deleteTemplateConfirm'))) return
49
+ await store.getState().deleteTemplate(template.id)
50
+ }, [store, t])
51
+
52
+ const categoryLabel = (cat: TemplateCategory) =>
53
+ t(`customForms.category.${cat}`)
54
+
55
+ return (
56
+ <div className="space-y-4">
57
+ {/* Search + New */}
58
+ <div className="flex items-center gap-3">
59
+ <div className="relative flex-1 max-w-xs">
60
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
61
+ <Input
62
+ value={search}
63
+ onChange={(e) => setSearch(e.target.value)}
64
+ placeholder={t('common.search')}
65
+ className="h-9 pl-8 text-sm"
66
+ />
67
+ </div>
68
+ <Button onClick={onNew} size="sm" className="h-9">
69
+ <Plus className="h-3.5 w-3.5 mr-1.5" />
70
+ {t('customForms.newTemplate')}
71
+ </Button>
72
+ </div>
73
+
74
+ {/* Empty state */}
75
+ {!loading && templates.length === 0 && (
76
+ <Card>
77
+ <CardContent className="flex flex-col items-center justify-center py-16 text-center">
78
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4">
79
+ <FileText className="h-5 w-5 text-muted-foreground" />
80
+ </div>
81
+ <p className="font-medium">{t('customForms.noTemplates')}</p>
82
+ <p className="text-sm text-muted-foreground mt-1 max-w-sm">
83
+ {t('customForms.noTemplatesDescription')}
84
+ </p>
85
+ <Button onClick={onNew} size="sm" className="mt-4">
86
+ <Plus className="h-3.5 w-3.5 mr-1.5" />
87
+ {t('customForms.newTemplate')}
88
+ </Button>
89
+ </CardContent>
90
+ </Card>
91
+ )}
92
+
93
+ {/* Template list */}
94
+ {templates.length > 0 && (
95
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
96
+ {templates.map((template) => (
97
+ <Card
98
+ key={template.id}
99
+ className="group cursor-pointer hover:shadow-md transition-shadow"
100
+ onClick={() => onEdit(template)}
101
+ >
102
+ <CardContent className="p-4">
103
+ <div className="flex items-start justify-between gap-2">
104
+ <div className="min-w-0 flex-1">
105
+ <h3 className="font-medium text-sm truncate">{template.name}</h3>
106
+ {template.description && (
107
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
108
+ {template.description}
109
+ </p>
110
+ )}
111
+ </div>
112
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
113
+ <button
114
+ onClick={(e) => { e.stopPropagation(); onEdit(template) }}
115
+ className="p-1.5 rounded-md hover:bg-muted"
116
+ >
117
+ <Pencil className="h-3.5 w-3.5 text-muted-foreground" />
118
+ </button>
119
+ <button
120
+ onClick={(e) => { e.stopPropagation(); handleDelete(template) }}
121
+ className="p-1.5 rounded-md hover:bg-destructive/10"
122
+ >
123
+ <Trash2 className="h-3.5 w-3.5 text-destructive" />
124
+ </button>
125
+ </div>
126
+ </div>
127
+ <div className="flex items-center gap-2 mt-3">
128
+ <Badge variant="secondary" className={`text-[10px] ${CATEGORY_COLORS[template.category]}`}>
129
+ {categoryLabel(template.category)}
130
+ </Badge>
131
+ <span className="text-[10px] text-muted-foreground">
132
+ {template.schema.fields.length} {t('customForms.builder.fields').toLowerCase()}
133
+ </span>
134
+ {template.version > 1 && (
135
+ <span className="text-[10px] text-muted-foreground">v{template.version}</span>
136
+ )}
137
+ </div>
138
+ </CardContent>
139
+ </Card>
140
+ ))}
141
+ </div>
142
+ )}
143
+
144
+ {/* Loading skeleton */}
145
+ {loading && (
146
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
147
+ {Array.from({ length: 3 }).map((_, i) => (
148
+ <Card key={i}>
149
+ <CardContent className="p-4 space-y-3">
150
+ <div className="h-4 w-2/3 bg-muted animate-pulse rounded" />
151
+ <div className="h-3 w-full bg-muted animate-pulse rounded" />
152
+ <div className="flex gap-2">
153
+ <div className="h-5 w-16 bg-muted animate-pulse rounded-full" />
154
+ <div className="h-5 w-12 bg-muted animate-pulse rounded-full" />
155
+ </div>
156
+ </CardContent>
157
+ </Card>
158
+ ))}
159
+ </div>
160
+ )}
161
+ </div>
162
+ )
163
+ }