@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.
Files changed (82) hide show
  1. package/README.md +3 -3
  2. package/dist/{FormBuilder-7SWZQKFN.js → FormBuilder-YHJHF6HZ.js} +4 -4
  3. package/dist/{FormBuilder-7SWZQKFN.js.map → FormBuilder-YHJHF6HZ.js.map} +1 -1
  4. package/dist/{chunk-W2ZNJGQC.js → chunk-RBLXIKWB.js} +221 -8
  5. package/dist/chunk-RBLXIKWB.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/components/DocumentFormDialog.d.ts.map +1 -1
  9. package/dist/components/DocumentSkeleton.d.ts +21 -0
  10. package/dist/components/DocumentSkeleton.d.ts.map +1 -0
  11. package/dist/components/FormRenderer.d.ts.map +1 -1
  12. package/dist/components/FormViewer.d.ts.map +1 -1
  13. package/dist/{CustomFormsContext.d.ts → context.d.ts} +1 -1
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/data/mock.d.ts.map +1 -1
  16. package/dist/data/supabase.d.ts.map +1 -1
  17. package/dist/data/types.d.ts +4 -1
  18. package/dist/data/types.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +174 -17
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/agent-tools.d.ts.map +1 -0
  24. package/dist/lib/document-types.d.ts.map +1 -0
  25. package/dist/lib/html-text.d.ts +21 -0
  26. package/dist/lib/html-text.d.ts.map +1 -0
  27. package/dist/lib/print.d.ts.map +1 -1
  28. package/dist/lib/timeline-source.d.ts +10 -0
  29. package/dist/lib/timeline-source.d.ts.map +1 -0
  30. package/dist/locales/en.d.ts.map +1 -1
  31. package/dist/locales/pt-BR.d.ts.map +1 -1
  32. package/dist/migrations/index.d.ts +1 -0
  33. package/dist/migrations/index.d.ts.map +1 -1
  34. package/dist/store.d.ts +5 -1
  35. package/dist/store.d.ts.map +1 -1
  36. package/dist/types.d.ts +13 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/views/TemplateListView.d.ts.map +1 -1
  39. package/package.json +5 -7
  40. package/dist/CustomFormsContext.d.ts.map +0 -1
  41. package/dist/agent-tools.d.ts.map +0 -1
  42. package/dist/chunk-W2ZNJGQC.js.map +0 -1
  43. package/dist/document-types.d.ts.map +0 -1
  44. package/src/CustomFormsContext.tsx +0 -28
  45. package/src/agent-tools.ts +0 -73
  46. package/src/components/AddDocumentDropdown.tsx +0 -92
  47. package/src/components/BuilderCanvas.tsx +0 -87
  48. package/src/components/BuilderField.tsx +0 -81
  49. package/src/components/DocumentFormDialog.tsx +0 -155
  50. package/src/components/DocumentList.tsx +0 -254
  51. package/src/components/FieldConfigDialog.tsx +0 -243
  52. package/src/components/FieldPalette.tsx +0 -81
  53. package/src/components/FormBuilder.tsx +0 -427
  54. package/src/components/FormRenderer.tsx +0 -256
  55. package/src/components/FormViewer.tsx +0 -94
  56. package/src/components/PersonDocumentsWidget.tsx +0 -229
  57. package/src/components/TemplateSelector.tsx +0 -91
  58. package/src/config.ts +0 -43
  59. package/src/data/index.ts +0 -3
  60. package/src/data/mock.ts +0 -193
  61. package/src/data/supabase.ts +0 -408
  62. package/src/data/tables.ts +0 -7
  63. package/src/data/types.ts +0 -33
  64. package/src/document-types.ts +0 -51
  65. package/src/index.ts +0 -234
  66. package/src/lib/person-fields.ts +0 -67
  67. package/src/lib/print.ts +0 -207
  68. package/src/lib/tenant.ts +0 -9
  69. package/src/locales/en.ts +0 -95
  70. package/src/locales/index.ts +0 -7
  71. package/src/locales/pt-BR.ts +0 -95
  72. package/src/migrations/000_plg_rename.sql +0 -21
  73. package/src/migrations/001_frm_base.sql +0 -174
  74. package/src/migrations/002_document_archetype.sql +0 -223
  75. package/src/migrations/003_agent_rpcs.sql +0 -174
  76. package/src/migrations/index.ts +0 -610
  77. package/src/store.ts +0 -167
  78. package/src/types.ts +0 -217
  79. package/src/views/CustomFormsSettingsTab.tsx +0 -105
  80. package/src/views/TemplateListView.tsx +0 -174
  81. /package/dist/{agent-tools.d.ts → lib/agent-tools.d.ts} +0 -0
  82. /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
@@ -1,3 +0,0 @@
1
- export type { CustomFormsDataProvider } from './types'
2
- export { createMockFormsProvider } from './mock'
3
- export { createSupabaseFormsProvider } from './supabase'
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
- }