@fayz-ai/plugin-forms 0.9.0 → 0.9.2
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-YHJHF6HZ.js} +4 -4
- package/dist/{FormBuilder-7SWZQKFN.js.map → FormBuilder-YHJHF6HZ.js.map} +1 -1
- package/dist/{chunk-W2ZNJGQC.js → chunk-RBLXIKWB.js} +221 -8
- package/dist/chunk-RBLXIKWB.js.map +1 -0
- package/dist/components/AddDocumentDropdown.d.ts +1 -1
- package/dist/components/AddDocumentDropdown.d.ts.map +1 -1
- package/dist/components/DocumentFormDialog.d.ts.map +1 -1
- package/dist/components/DocumentSkeleton.d.ts +21 -0
- package/dist/components/DocumentSkeleton.d.ts.map +1 -0
- package/dist/components/FormRenderer.d.ts.map +1 -1
- package/dist/components/FormViewer.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/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +4 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +174 -17
- 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/dist/lib/html-text.d.ts +21 -0
- package/dist/lib/html-text.d.ts.map +1 -0
- package/dist/lib/print.d.ts.map +1 -1
- package/dist/lib/timeline-source.d.ts +10 -0
- package/dist/lib/timeline-source.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/store.d.ts +5 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/TemplateListView.d.ts.map +1 -1
- 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
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react'
|
|
2
|
-
import { ArrowLeft, Pencil, Printer } from 'lucide-react'
|
|
3
|
-
import { Button } from '@fayz-ai/ui'
|
|
4
|
-
import { Badge } from '@fayz-ai/ui'
|
|
5
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
6
|
-
import type { CustomFormsDataProvider } from '../data/types'
|
|
7
|
-
import type { FormDocument, FormTemplate } from '../types'
|
|
8
|
-
import { FormRenderer } from './FormRenderer'
|
|
9
|
-
import { printDocument } from '../lib/print'
|
|
10
|
-
|
|
11
|
-
interface FormViewerProps {
|
|
12
|
-
document: FormDocument
|
|
13
|
-
provider: CustomFormsDataProvider
|
|
14
|
-
onBack: () => void
|
|
15
|
-
onEdit?: (doc: FormDocument) => void
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const STATUS_COLORS: Record<string, string> = {
|
|
19
|
-
draft: 'bg-warning-soft text-warning-soft-foreground',
|
|
20
|
-
completed: 'bg-success-soft text-success-soft-foreground',
|
|
21
|
-
signed: 'bg-info-soft text-info-soft-foreground',
|
|
22
|
-
archived: 'bg-muted text-muted-foreground',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function FormViewer({ document: doc, provider, onBack, onEdit }: FormViewerProps) {
|
|
26
|
-
const t = useTranslation()
|
|
27
|
-
const [template, setTemplate] = useState<FormTemplate | null>(null)
|
|
28
|
-
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (!doc.templateId) return
|
|
31
|
-
const timer = setTimeout(() => {
|
|
32
|
-
provider.getTemplateById(doc.templateId).then(setTemplate)
|
|
33
|
-
}, 50)
|
|
34
|
-
return () => clearTimeout(timer)
|
|
35
|
-
}, [doc.templateId, provider])
|
|
36
|
-
|
|
37
|
-
if (!template) {
|
|
38
|
-
return (
|
|
39
|
-
<div className="flex items-center justify-center py-12">
|
|
40
|
-
<div className="h-5 w-5 border-2 border-primary border-t-transparent animate-spin rounded-full" />
|
|
41
|
-
</div>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div className="space-y-4">
|
|
47
|
-
{/* Header */}
|
|
48
|
-
<div className="flex items-center gap-3">
|
|
49
|
-
<button
|
|
50
|
-
onClick={onBack}
|
|
51
|
-
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
52
|
-
>
|
|
53
|
-
<ArrowLeft className="h-3.5 w-3.5" />
|
|
54
|
-
</button>
|
|
55
|
-
<div className="flex-1 min-w-0">
|
|
56
|
-
<h3 className="font-semibold text-sm truncate">
|
|
57
|
-
{doc.title ?? template.name}
|
|
58
|
-
</h3>
|
|
59
|
-
<p className="text-xs text-muted-foreground">
|
|
60
|
-
{new Date(doc.createdAt).toLocaleDateString()}
|
|
61
|
-
</p>
|
|
62
|
-
</div>
|
|
63
|
-
<Badge variant="secondary" className={`text-[10px] ${STATUS_COLORS[doc.status] ?? ''}`}>
|
|
64
|
-
{t(`customForms.status.${doc.status}`)}
|
|
65
|
-
</Badge>
|
|
66
|
-
<Button
|
|
67
|
-
variant="outline"
|
|
68
|
-
size="sm"
|
|
69
|
-
className="h-8"
|
|
70
|
-
onClick={() => printDocument(template, doc)}
|
|
71
|
-
>
|
|
72
|
-
<Printer className="h-3 w-3 mr-1" />
|
|
73
|
-
{t('customForms.printPdf')}
|
|
74
|
-
</Button>
|
|
75
|
-
{onEdit && doc.status === 'draft' && (
|
|
76
|
-
<Button variant="outline" size="sm" className="h-8" onClick={() => onEdit(doc)}>
|
|
77
|
-
<Pencil className="h-3 w-3 mr-1" />
|
|
78
|
-
{t('common.edit')}
|
|
79
|
-
</Button>
|
|
80
|
-
)}
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
{/* Form content (read-only) */}
|
|
84
|
-
<div className="rounded-xl border p-4">
|
|
85
|
-
<FormRenderer
|
|
86
|
-
schema={template.schema}
|
|
87
|
-
data={doc.data}
|
|
88
|
-
onChange={() => {}}
|
|
89
|
-
readOnly
|
|
90
|
-
/>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef } from 'react'
|
|
2
|
-
import { ArrowLeft, Image as ImageIcon, Paperclip } from 'lucide-react'
|
|
3
|
-
import { Badge } from '@fayz-ai/ui'
|
|
4
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
5
|
-
import { useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
|
|
6
|
-
import type { CustomFormsConfig } from '../config'
|
|
7
|
-
import type { CustomFormsDataProvider } from '../data/types'
|
|
8
|
-
import type { CustomFormsStore } from '../store'
|
|
9
|
-
import type { FormDocument, FormTemplate } from '../types'
|
|
10
|
-
import type { DocumentTypeOption } from '../document-types'
|
|
11
|
-
import { DocumentList } from './DocumentList'
|
|
12
|
-
import { DocumentFormDialog } from './DocumentFormDialog'
|
|
13
|
-
import { FormViewer } from './FormViewer'
|
|
14
|
-
import { AddDocumentDropdown } from './AddDocumentDropdown'
|
|
15
|
-
|
|
16
|
-
interface PersonDocumentsWidgetProps {
|
|
17
|
-
item: { id: string; name?: string; [key: string]: any }
|
|
18
|
-
config: CustomFormsConfig
|
|
19
|
-
provider: CustomFormsDataProvider
|
|
20
|
-
store: CustomFormsStore
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type WidgetView =
|
|
24
|
-
| { type: 'list' }
|
|
25
|
-
| { type: 'fill'; template: FormTemplate }
|
|
26
|
-
| { type: 'view'; document: FormDocument }
|
|
27
|
-
| { type: 'edit'; document: FormDocument }
|
|
28
|
-
|
|
29
|
-
export function PersonDocumentsWidget({ item, config, provider, store }: PersonDocumentsWidgetProps) {
|
|
30
|
-
const t = useTranslation()
|
|
31
|
-
const guardDocuments = useLimitGuard('documents_month')
|
|
32
|
-
const [view, setView] = useState<WidgetView>({ type: 'list' })
|
|
33
|
-
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
34
|
-
const [pendingFileAccept, setPendingFileAccept] = useState<string>('*/*')
|
|
35
|
-
|
|
36
|
-
const handleDocTypeSelect = useCallback(async (option: DocumentTypeOption) => {
|
|
37
|
-
// Form template
|
|
38
|
-
if (option.id.startsWith('template:')) {
|
|
39
|
-
const templateId = option.id.replace('template:', '')
|
|
40
|
-
const template = await provider.getTemplateById(templateId)
|
|
41
|
-
if (template) {
|
|
42
|
-
setView({ type: 'fill', template })
|
|
43
|
-
}
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Image — open file picker for images
|
|
48
|
-
if (option.id === 'core:image') {
|
|
49
|
-
setPendingFileAccept('image/*')
|
|
50
|
-
setTimeout(() => fileInputRef.current?.click(), 0)
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Attachment — open file picker for any file
|
|
55
|
-
if (option.id === 'core:attachment') {
|
|
56
|
-
setPendingFileAccept('*/*')
|
|
57
|
-
setTimeout(() => fileInputRef.current?.click(), 0)
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
}, [provider])
|
|
61
|
-
|
|
62
|
-
const handleFiles = useCallback(async (files: File[]) => {
|
|
63
|
-
// Plan monthly-quota guard (client-side, before the provider call) — count
|
|
64
|
-
// the whole batch. Opens the global UpgradeModal and aborts when the cap
|
|
65
|
-
// would be exceeded.
|
|
66
|
-
if ((await guardDocuments(files.length)) === 'blocked') return
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
const isImage = file.type.startsWith('image/')
|
|
69
|
-
await store.getState().createDocument({
|
|
70
|
-
kind: isImage ? 'image' : 'attachment',
|
|
71
|
-
personId: item.id,
|
|
72
|
-
title: file.name,
|
|
73
|
-
fileName: file.name,
|
|
74
|
-
fileSize: file.size,
|
|
75
|
-
mimeType: file.type,
|
|
76
|
-
status: 'completed',
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
invalidateLimit('documents_month')
|
|
80
|
-
}, [store, item.id, guardDocuments])
|
|
81
|
-
|
|
82
|
-
const handleFileInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
83
|
-
const files = Array.from(e.target.files ?? [])
|
|
84
|
-
if (files.length > 0) handleFiles(files)
|
|
85
|
-
e.target.value = ''
|
|
86
|
-
}, [handleFiles])
|
|
87
|
-
|
|
88
|
-
const handleViewDoc = useCallback((doc: FormDocument) => {
|
|
89
|
-
setView({ type: 'view', document: doc })
|
|
90
|
-
}, [])
|
|
91
|
-
|
|
92
|
-
const handleEditDoc = useCallback((doc: FormDocument) => {
|
|
93
|
-
setView({ type: 'edit', document: doc })
|
|
94
|
-
}, [])
|
|
95
|
-
|
|
96
|
-
const handleBack = useCallback(() => {
|
|
97
|
-
setView({ type: 'list' })
|
|
98
|
-
}, [])
|
|
99
|
-
|
|
100
|
-
const handleSaved = useCallback(() => {
|
|
101
|
-
setView({ type: 'list' })
|
|
102
|
-
}, [])
|
|
103
|
-
|
|
104
|
-
// Fill form
|
|
105
|
-
if (view.type === 'fill') {
|
|
106
|
-
return (
|
|
107
|
-
<DocumentFormDialog
|
|
108
|
-
template={view.template}
|
|
109
|
-
personId={item.id}
|
|
110
|
-
personName={item.name}
|
|
111
|
-
person={item}
|
|
112
|
-
provider={provider}
|
|
113
|
-
store={store}
|
|
114
|
-
onSaved={handleSaved}
|
|
115
|
-
onBack={handleBack}
|
|
116
|
-
/>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// View document — route by kind
|
|
121
|
-
if (view.type === 'view') {
|
|
122
|
-
const doc = view.document
|
|
123
|
-
const isForm = doc.kind === 'form' || (!doc.kind && doc.templateId)
|
|
124
|
-
|
|
125
|
-
if (!isForm) {
|
|
126
|
-
// File document (image/attachment) — simple preview
|
|
127
|
-
return (
|
|
128
|
-
<FileDocumentViewer document={doc} onBack={handleBack} />
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<FormViewer
|
|
134
|
-
document={doc}
|
|
135
|
-
provider={provider}
|
|
136
|
-
onBack={handleBack}
|
|
137
|
-
onEdit={handleEditDoc}
|
|
138
|
-
/>
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Edit document — only for form docs
|
|
143
|
-
if (view.type === 'edit') {
|
|
144
|
-
return (
|
|
145
|
-
<DocumentFormDialog
|
|
146
|
-
template={null}
|
|
147
|
-
existingDocument={view.document}
|
|
148
|
-
personId={item.id}
|
|
149
|
-
personName={item.name}
|
|
150
|
-
person={item}
|
|
151
|
-
provider={provider}
|
|
152
|
-
store={store}
|
|
153
|
-
onSaved={handleSaved}
|
|
154
|
-
onBack={handleBack}
|
|
155
|
-
/>
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Default: list with dropdown + drag zone
|
|
160
|
-
return (
|
|
161
|
-
<div className="space-y-3">
|
|
162
|
-
<div className="flex items-center justify-between">
|
|
163
|
-
<h3 className="text-sm font-semibold">{t('customForms.documents')}</h3>
|
|
164
|
-
<AddDocumentDropdown personId={item.id} onSelect={handleDocTypeSelect} />
|
|
165
|
-
</div>
|
|
166
|
-
<DocumentList
|
|
167
|
-
personId={item.id}
|
|
168
|
-
provider={provider}
|
|
169
|
-
store={store}
|
|
170
|
-
onView={handleViewDoc}
|
|
171
|
-
onFileDrop={handleFiles}
|
|
172
|
-
/>
|
|
173
|
-
{/* Hidden file input for image/attachment upload */}
|
|
174
|
-
<input
|
|
175
|
-
ref={fileInputRef}
|
|
176
|
-
type="file"
|
|
177
|
-
className="hidden"
|
|
178
|
-
accept={pendingFileAccept}
|
|
179
|
-
multiple
|
|
180
|
-
onChange={handleFileInputChange}
|
|
181
|
-
/>
|
|
182
|
-
</div>
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function FileDocumentViewer({ document: doc, onBack }: { document: FormDocument; onBack: () => void }) {
|
|
187
|
-
const isImage = doc.kind === 'image' || doc.mimeType?.startsWith('image/')
|
|
188
|
-
const Icon = isImage ? ImageIcon : Paperclip
|
|
189
|
-
|
|
190
|
-
return (
|
|
191
|
-
<div className="space-y-4">
|
|
192
|
-
<div className="flex items-center gap-3">
|
|
193
|
-
<button
|
|
194
|
-
onClick={onBack}
|
|
195
|
-
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
196
|
-
>
|
|
197
|
-
<ArrowLeft className="h-3.5 w-3.5" />
|
|
198
|
-
</button>
|
|
199
|
-
<div className="flex-1 min-w-0">
|
|
200
|
-
<h3 className="font-semibold text-sm truncate">{doc.title ?? doc.fileName ?? 'Document'}</h3>
|
|
201
|
-
<p className="text-xs text-muted-foreground">
|
|
202
|
-
{new Date(doc.createdAt).toLocaleDateString()}
|
|
203
|
-
</p>
|
|
204
|
-
</div>
|
|
205
|
-
<Badge variant="secondary" className="text-[10px] bg-success/15 text-success">
|
|
206
|
-
{doc.status === 'completed' ? 'Finalizado' : doc.status}
|
|
207
|
-
</Badge>
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
<div className="rounded-xl border p-6 flex flex-col items-center gap-4">
|
|
211
|
-
{isImage && doc.fileUrl ? (
|
|
212
|
-
<img src={doc.fileUrl} alt={doc.fileName ?? ''} className="max-w-full max-h-[500px] rounded-lg object-contain" />
|
|
213
|
-
) : (
|
|
214
|
-
<div className="flex flex-col items-center gap-3 py-8">
|
|
215
|
-
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
|
216
|
-
<Icon className="h-7 w-7 text-muted-foreground" />
|
|
217
|
-
</div>
|
|
218
|
-
<p className="text-sm font-medium">{doc.fileName ?? doc.title ?? 'Arquivo'}</p>
|
|
219
|
-
{doc.fileSize && (
|
|
220
|
-
<p className="text-xs text-muted-foreground">
|
|
221
|
-
{(doc.fileSize / 1024).toFixed(1)} KB
|
|
222
|
-
</p>
|
|
223
|
-
)}
|
|
224
|
-
</div>
|
|
225
|
-
)}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
)
|
|
229
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react'
|
|
2
|
-
import { FileText, Search } from 'lucide-react'
|
|
3
|
-
import { Input } from '@fayz-ai/ui'
|
|
4
|
-
import { Badge } from '@fayz-ai/ui'
|
|
5
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
6
|
-
import type { CustomFormsDataProvider } from '../data/types'
|
|
7
|
-
import type { FormTemplate } from '../types'
|
|
8
|
-
|
|
9
|
-
interface TemplateSelectorProps {
|
|
10
|
-
provider: CustomFormsDataProvider
|
|
11
|
-
onSelect: (template: FormTemplate) => void
|
|
12
|
-
onClose: () => void
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function TemplateSelector({ provider, onSelect, onClose }: TemplateSelectorProps) {
|
|
16
|
-
const t = useTranslation()
|
|
17
|
-
const [templates, setTemplates] = useState<FormTemplate[]>([])
|
|
18
|
-
const [search, setSearch] = useState('')
|
|
19
|
-
const [loading, setLoading] = useState(true)
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
setLoading(true)
|
|
23
|
-
provider
|
|
24
|
-
.getTemplates({ search: search || undefined })
|
|
25
|
-
.then((result) => setTemplates(result.data))
|
|
26
|
-
.finally(() => setLoading(false))
|
|
27
|
-
}, [provider, search])
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
31
|
-
<div
|
|
32
|
-
className="bg-card rounded-xl border shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col"
|
|
33
|
-
onClick={(e) => e.stopPropagation()}
|
|
34
|
-
>
|
|
35
|
-
{/* Header */}
|
|
36
|
-
<div className="p-4 border-b">
|
|
37
|
-
<h3 className="font-semibold text-sm mb-3">{t('customForms.selectTemplate')}</h3>
|
|
38
|
-
<div className="relative">
|
|
39
|
-
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
40
|
-
<Input
|
|
41
|
-
value={search}
|
|
42
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
43
|
-
placeholder={t('common.search')}
|
|
44
|
-
className="h-9 pl-8 text-sm"
|
|
45
|
-
autoFocus
|
|
46
|
-
/>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
{/* List */}
|
|
51
|
-
<div className="flex-1 overflow-y-auto p-2">
|
|
52
|
-
{loading && (
|
|
53
|
-
<div className="flex items-center justify-center py-8">
|
|
54
|
-
<div className="h-5 w-5 border-2 border-primary border-t-transparent animate-spin rounded-full" />
|
|
55
|
-
</div>
|
|
56
|
-
)}
|
|
57
|
-
|
|
58
|
-
{!loading && templates.length === 0 && (
|
|
59
|
-
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
60
|
-
<FileText className="h-8 w-8 text-muted-foreground/50 mb-2" />
|
|
61
|
-
<p className="text-sm text-muted-foreground">{t('customForms.noTemplates')}</p>
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
|
-
|
|
65
|
-
{templates.map((tpl) => (
|
|
66
|
-
<button
|
|
67
|
-
key={tpl.id}
|
|
68
|
-
onClick={() => onSelect(tpl)}
|
|
69
|
-
className="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg hover:bg-muted transition-colors text-left"
|
|
70
|
-
>
|
|
71
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
|
72
|
-
<FileText className="h-4 w-4 text-primary" />
|
|
73
|
-
</div>
|
|
74
|
-
<div className="flex-1 min-w-0">
|
|
75
|
-
<p className="text-sm font-medium truncate">{tpl.name}</p>
|
|
76
|
-
<div className="flex items-center gap-2 mt-0.5">
|
|
77
|
-
<Badge variant="secondary" className="text-[10px]">
|
|
78
|
-
{t(`customForms.category.${tpl.category}`)}
|
|
79
|
-
</Badge>
|
|
80
|
-
<span className="text-[10px] text-muted-foreground">
|
|
81
|
-
{tpl.schema.fields.length} {t('customForms.builder.fields').toLowerCase()}
|
|
82
|
-
</span>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</button>
|
|
86
|
-
))}
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
)
|
|
91
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { PluginRegistryDef, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
2
|
-
import type { CustomFormsDataProvider } from './data/types'
|
|
3
|
-
|
|
4
|
-
export interface CustomFormsPluginOptions {
|
|
5
|
-
scope?: PluginScope
|
|
6
|
-
verticalId?: VerticalId
|
|
7
|
-
dataProvider?: CustomFormsDataProvider
|
|
8
|
-
navSection?: 'main' | 'secondary' | 'settings'
|
|
9
|
-
navPosition?: number
|
|
10
|
-
labels?: Partial<CustomFormsLabels>
|
|
11
|
-
/** App-owned registries shown inside Forms & Documents settings. */
|
|
12
|
-
settingsRegistries?: PluginRegistryDef[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface CustomFormsLabels {
|
|
16
|
-
pageTitle: string
|
|
17
|
-
settingsLabel: string
|
|
18
|
-
settingsSubtitle: string
|
|
19
|
-
templates: string
|
|
20
|
-
documents: string
|
|
21
|
-
newTemplate: string
|
|
22
|
-
addDocument: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface CustomFormsConfig {
|
|
26
|
-
labels: CustomFormsLabels
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const DEFAULT_LABELS: CustomFormsLabels = {
|
|
30
|
-
pageTitle: 'Custom Forms',
|
|
31
|
-
settingsLabel: 'Forms & Documents',
|
|
32
|
-
settingsSubtitle: 'Create and manage custom forms for your business',
|
|
33
|
-
templates: 'Templates',
|
|
34
|
-
documents: 'Documents',
|
|
35
|
-
newTemplate: 'New Template',
|
|
36
|
-
addDocument: 'Add Document',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function resolveConfig(options?: CustomFormsPluginOptions): CustomFormsConfig {
|
|
40
|
-
return {
|
|
41
|
-
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
42
|
-
}
|
|
43
|
-
}
|
package/src/data/index.ts
DELETED
package/src/data/mock.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import type { CustomFormsDataProvider } from './types'
|
|
2
|
-
import type {
|
|
3
|
-
FormTemplate,
|
|
4
|
-
FormDocument,
|
|
5
|
-
DocumentFile,
|
|
6
|
-
TemplateQuery,
|
|
7
|
-
DocumentQuery,
|
|
8
|
-
PaginatedResult,
|
|
9
|
-
CreateTemplateInput,
|
|
10
|
-
UpdateTemplateInput,
|
|
11
|
-
CreateDocumentInput,
|
|
12
|
-
UpdateDocumentInput,
|
|
13
|
-
} from '../types'
|
|
14
|
-
|
|
15
|
-
function uuid() {
|
|
16
|
-
return crypto.randomUUID()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function now() {
|
|
20
|
-
return new Date().toISOString()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createMockFormsProvider(): CustomFormsDataProvider {
|
|
24
|
-
const templates: FormTemplate[] = []
|
|
25
|
-
const documents: FormDocument[] = []
|
|
26
|
-
const files: DocumentFile[] = []
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
// ── Templates ──────────────────────────────────────────
|
|
30
|
-
async getTemplates(query: TemplateQuery): Promise<PaginatedResult<FormTemplate>> {
|
|
31
|
-
let filtered = templates.filter((t) => !t.isDeleted && t.isCurrent)
|
|
32
|
-
if (query.category) {
|
|
33
|
-
filtered = filtered.filter((t) => t.category === query.category)
|
|
34
|
-
}
|
|
35
|
-
if (query.search) {
|
|
36
|
-
const s = query.search.toLowerCase()
|
|
37
|
-
filtered = filtered.filter((t) => t.name.toLowerCase().includes(s))
|
|
38
|
-
}
|
|
39
|
-
const page = query.page ?? 1
|
|
40
|
-
const pageSize = query.pageSize ?? 25
|
|
41
|
-
const start = (page - 1) * pageSize
|
|
42
|
-
return { data: filtered.slice(start, start + pageSize), total: filtered.length }
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
async getTemplateById(id: string): Promise<FormTemplate | null> {
|
|
46
|
-
return templates.find((t) => t.id === id && !t.isDeleted) ?? null
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
async createTemplate(input: CreateTemplateInput): Promise<FormTemplate> {
|
|
50
|
-
const template: FormTemplate = {
|
|
51
|
-
id: uuid(),
|
|
52
|
-
tenantId: 'mock-tenant',
|
|
53
|
-
name: input.name,
|
|
54
|
-
description: input.description,
|
|
55
|
-
category: input.category,
|
|
56
|
-
version: 1,
|
|
57
|
-
isCurrent: true,
|
|
58
|
-
schema: input.schema,
|
|
59
|
-
specialty: input.specialty,
|
|
60
|
-
tags: input.tags ?? [],
|
|
61
|
-
metadata: input.metadata ?? {},
|
|
62
|
-
isActive: true,
|
|
63
|
-
isDeleted: false,
|
|
64
|
-
createdAt: now(),
|
|
65
|
-
updatedAt: now(),
|
|
66
|
-
}
|
|
67
|
-
templates.push(template)
|
|
68
|
-
return template
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
async updateTemplate(id: string, input: UpdateTemplateInput): Promise<FormTemplate> {
|
|
72
|
-
const idx = templates.findIndex((t) => t.id === id)
|
|
73
|
-
if (idx === -1) throw new Error('Template not found')
|
|
74
|
-
const existing = templates[idx]
|
|
75
|
-
const updated: FormTemplate = {
|
|
76
|
-
...existing,
|
|
77
|
-
...input,
|
|
78
|
-
tags: input.tags ?? existing.tags,
|
|
79
|
-
metadata: input.metadata ?? existing.metadata,
|
|
80
|
-
schema: input.schema ?? existing.schema,
|
|
81
|
-
updatedAt: now(),
|
|
82
|
-
}
|
|
83
|
-
templates[idx] = updated
|
|
84
|
-
return updated
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
async deleteTemplate(id: string): Promise<void> {
|
|
88
|
-
const idx = templates.findIndex((t) => t.id === id)
|
|
89
|
-
if (idx !== -1) templates[idx] = { ...templates[idx], isDeleted: true }
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
// ── Documents ──────────────────────────────────────────
|
|
93
|
-
async getDocuments(query: DocumentQuery): Promise<PaginatedResult<FormDocument>> {
|
|
94
|
-
let filtered = documents.filter((d) => !d.isDeleted)
|
|
95
|
-
if (query.personId) {
|
|
96
|
-
filtered = filtered.filter((d) => d.personId === query.personId)
|
|
97
|
-
}
|
|
98
|
-
if (query.templateId) {
|
|
99
|
-
filtered = filtered.filter((d) => d.templateId === query.templateId)
|
|
100
|
-
}
|
|
101
|
-
if (query.status) {
|
|
102
|
-
const statuses = Array.isArray(query.status) ? query.status : [query.status]
|
|
103
|
-
filtered = filtered.filter((d) => statuses.includes(d.status))
|
|
104
|
-
}
|
|
105
|
-
if (query.search) {
|
|
106
|
-
const s = query.search.toLowerCase()
|
|
107
|
-
filtered = filtered.filter(
|
|
108
|
-
(d) =>
|
|
109
|
-
d.title?.toLowerCase().includes(s) ||
|
|
110
|
-
d.templateName?.toLowerCase().includes(s),
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
filtered.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
114
|
-
const page = query.page ?? 1
|
|
115
|
-
const pageSize = query.pageSize ?? 25
|
|
116
|
-
const start = (page - 1) * pageSize
|
|
117
|
-
return { data: filtered.slice(start, start + pageSize), total: filtered.length }
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
async getDocumentById(id: string): Promise<FormDocument | null> {
|
|
121
|
-
return documents.find((d) => d.id === id && !d.isDeleted) ?? null
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
async createDocument(input: CreateDocumentInput): Promise<FormDocument> {
|
|
125
|
-
const template = templates.find((t) => t.id === input.templateId)
|
|
126
|
-
const doc: FormDocument = {
|
|
127
|
-
id: uuid(),
|
|
128
|
-
tenantId: 'mock-tenant',
|
|
129
|
-
templateId: input.templateId ?? '',
|
|
130
|
-
templateName: template?.name,
|
|
131
|
-
templateCategory: template?.category,
|
|
132
|
-
personId: input.personId,
|
|
133
|
-
title: input.title,
|
|
134
|
-
data: input.data ?? {},
|
|
135
|
-
kind: input.kind ?? (input.templateId ? 'form' : 'attachment'),
|
|
136
|
-
status: input.status ?? 'draft',
|
|
137
|
-
metadata: {},
|
|
138
|
-
isDeleted: false,
|
|
139
|
-
createdAt: now(),
|
|
140
|
-
updatedAt: now(),
|
|
141
|
-
}
|
|
142
|
-
documents.push(doc)
|
|
143
|
-
return doc
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
async updateDocument(id: string, input: UpdateDocumentInput): Promise<FormDocument> {
|
|
147
|
-
const idx = documents.findIndex((d) => d.id === id)
|
|
148
|
-
if (idx === -1) throw new Error('Document not found')
|
|
149
|
-
const existing = documents[idx]
|
|
150
|
-
const updated: FormDocument = {
|
|
151
|
-
...existing,
|
|
152
|
-
...input,
|
|
153
|
-
data: input.data ?? existing.data,
|
|
154
|
-
updatedAt: now(),
|
|
155
|
-
}
|
|
156
|
-
documents[idx] = updated
|
|
157
|
-
return updated
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
async deleteDocument(id: string): Promise<void> {
|
|
161
|
-
const idx = documents.findIndex((d) => d.id === id)
|
|
162
|
-
if (idx !== -1) documents[idx] = { ...documents[idx], isDeleted: true }
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
// ── Files ──────────────────────────────────────────────
|
|
166
|
-
async getDocumentFiles(documentId: string): Promise<DocumentFile[]> {
|
|
167
|
-
return files.filter((f) => f.documentId === documentId)
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
async uploadFile(documentId: string, fieldKey: string, file: File): Promise<DocumentFile> {
|
|
171
|
-
const docFile: DocumentFile = {
|
|
172
|
-
id: uuid(),
|
|
173
|
-
tenantId: 'mock-tenant',
|
|
174
|
-
documentId,
|
|
175
|
-
fieldKey,
|
|
176
|
-
fileUrl: URL.createObjectURL(file),
|
|
177
|
-
fileName: file.name,
|
|
178
|
-
fileSize: file.size,
|
|
179
|
-
mimeType: file.type,
|
|
180
|
-
sortOrder: files.filter((f) => f.documentId === documentId && f.fieldKey === fieldKey).length,
|
|
181
|
-
metadata: {},
|
|
182
|
-
createdAt: now(),
|
|
183
|
-
}
|
|
184
|
-
files.push(docFile)
|
|
185
|
-
return docFile
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
async deleteFile(fileId: string): Promise<void> {
|
|
189
|
-
const idx = files.findIndex((f) => f.id === fileId)
|
|
190
|
-
if (idx !== -1) files.splice(idx, 1)
|
|
191
|
-
},
|
|
192
|
-
}
|
|
193
|
-
}
|