@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.
- package/dist/CustomFormsContext.d.ts +15 -0
- package/dist/CustomFormsContext.d.ts.map +1 -0
- package/dist/FormBuilder-4VKYVZAC.cjs +619 -0
- package/dist/FormBuilder-4VKYVZAC.cjs.map +1 -0
- package/dist/FormBuilder-KDANEL6Z.js +613 -0
- package/dist/FormBuilder-KDANEL6Z.js.map +1 -0
- package/dist/chunk-32I43T37.js +198 -0
- package/dist/chunk-32I43T37.js.map +1 -0
- package/dist/chunk-XLUULIVL.cjs +200 -0
- package/dist/chunk-XLUULIVL.cjs.map +1 -0
- package/dist/components/AddDocumentDropdown.d.ts +9 -0
- package/dist/components/AddDocumentDropdown.d.ts.map +1 -0
- package/dist/components/BuilderCanvas.d.ts +12 -0
- package/dist/components/BuilderCanvas.d.ts.map +1 -0
- package/dist/components/BuilderField.d.ts +11 -0
- package/dist/components/BuilderField.d.ts.map +1 -0
- package/dist/components/DocumentFormDialog.d.ts +17 -0
- package/dist/components/DocumentFormDialog.d.ts.map +1 -0
- package/dist/components/DocumentList.d.ts +14 -0
- package/dist/components/DocumentList.d.ts.map +1 -0
- package/dist/components/FieldConfigDialog.d.ts +12 -0
- package/dist/components/FieldConfigDialog.d.ts.map +1 -0
- package/dist/components/FieldPalette.d.ts +10 -0
- package/dist/components/FieldPalette.d.ts.map +1 -0
- package/dist/components/FormBuilder.d.ts +14 -0
- package/dist/components/FormBuilder.d.ts.map +1 -0
- package/dist/components/FormRenderer.d.ts +11 -0
- package/dist/components/FormRenderer.d.ts.map +1 -0
- package/dist/components/FormViewer.d.ts +12 -0
- package/dist/components/FormViewer.d.ts.map +1 -0
- package/dist/components/PersonDocumentsWidget.d.ts +17 -0
- package/dist/components/PersonDocumentsWidget.d.ts.map +1 -0
- package/dist/components/TemplateSelector.d.ts +11 -0
- package/dist/components/TemplateSelector.d.ts.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/data/index.d.ts +4 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +17 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/document-types.d.ts +25 -0
- package/dist/document-types.d.ts.map +1 -0
- package/dist/index.cjs +1612 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1605 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/store.d.ts +29 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/CustomFormsSettingsTab.d.ts +14 -0
- package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -0
- package/dist/views/TemplateListView.d.ts +13 -0
- package/dist/views/TemplateListView.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/CustomFormsContext.tsx +28 -0
- package/src/components/AddDocumentDropdown.tsx +92 -0
- package/src/components/BuilderCanvas.tsx +87 -0
- package/src/components/BuilderField.tsx +81 -0
- package/src/components/DocumentFormDialog.tsx +134 -0
- package/src/components/DocumentList.tsx +250 -0
- package/src/components/FieldConfigDialog.tsx +203 -0
- package/src/components/FieldPalette.tsx +81 -0
- package/src/components/FormBuilder.tsx +345 -0
- package/src/components/FormRenderer.tsx +256 -0
- package/src/components/FormViewer.tsx +84 -0
- package/src/components/PersonDocumentsWidget.tsx +220 -0
- package/src/components/TemplateSelector.tsx +91 -0
- package/src/config.ts +39 -0
- package/src/data/index.ts +3 -0
- package/src/data/mock.ts +193 -0
- package/src/data/supabase.ts +405 -0
- package/src/data/types.ts +33 -0
- package/src/document-types.ts +51 -0
- package/src/index.ts +195 -0
- package/src/lib/tenant.ts +5 -0
- package/src/locales/en.ts +75 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +75 -0
- package/src/migrations/001_frm_base.sql +138 -0
- package/src/migrations/002_document_archetype.sql +135 -0
- package/src/store.ts +167 -0
- package/src/types.ts +203 -0
- package/src/views/CustomFormsSettingsTab.tsx +115 -0
- 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
|
+
}
|