@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
package/src/store.ts DELETED
@@ -1,167 +0,0 @@
1
- import { create } from 'zustand'
2
- import type { CustomFormsDataProvider } from './data/types'
3
- import type {
4
- FormTemplate,
5
- FormDocument,
6
- TemplateQuery,
7
- DocumentQuery,
8
- CreateTemplateInput,
9
- UpdateTemplateInput,
10
- CreateDocumentInput,
11
- UpdateDocumentInput,
12
- } from './types'
13
-
14
- export interface CustomFormsState {
15
- // Templates
16
- templates: FormTemplate[]
17
- templatesTotal: number
18
- templatesLoading: boolean
19
- templatesError: string | null
20
-
21
- // Documents
22
- documents: FormDocument[]
23
- documentsTotal: number
24
- documentsLoading: boolean
25
- documentsError: string | null
26
-
27
- // Active template (for builder)
28
- activeTemplate: FormTemplate | null
29
- activeTemplateLoading: boolean
30
-
31
- // Active document (for viewer/editor)
32
- activeDocument: FormDocument | null
33
- activeDocumentLoading: boolean
34
-
35
- // Actions
36
- fetchTemplates(query?: TemplateQuery): Promise<void>
37
- fetchTemplateById(id: string): Promise<FormTemplate | null>
38
- createTemplate(input: CreateTemplateInput): Promise<FormTemplate>
39
- updateTemplate(id: string, input: UpdateTemplateInput): Promise<FormTemplate>
40
- deleteTemplate(id: string): Promise<void>
41
-
42
- fetchDocuments(query?: DocumentQuery): Promise<void>
43
- fetchDocumentById(id: string): Promise<FormDocument | null>
44
- createDocument(input: CreateDocumentInput): Promise<FormDocument>
45
- updateDocument(id: string, input: UpdateDocumentInput): Promise<FormDocument>
46
- deleteDocument(id: string): Promise<void>
47
- }
48
-
49
- export function createCustomFormsStore(provider: CustomFormsDataProvider) {
50
- return create<CustomFormsState>((set) => ({
51
- templates: [],
52
- templatesTotal: 0,
53
- templatesLoading: false,
54
- templatesError: null,
55
-
56
- documents: [],
57
- documentsTotal: 0,
58
- documentsLoading: false,
59
- documentsError: null,
60
-
61
- activeTemplate: null,
62
- activeTemplateLoading: false,
63
-
64
- activeDocument: null,
65
- activeDocumentLoading: false,
66
-
67
- async fetchTemplates(query?: TemplateQuery) {
68
- set({ templatesLoading: true, templatesError: null })
69
- try {
70
- const result = await provider.getTemplates(query ?? {})
71
- set({ templates: result.data, templatesTotal: result.total })
72
- } catch (e: any) {
73
- set({ templatesError: e.message ?? 'Failed to fetch templates' })
74
- } finally {
75
- set({ templatesLoading: false })
76
- }
77
- },
78
-
79
- async fetchTemplateById(id: string) {
80
- set({ activeTemplateLoading: true })
81
- try {
82
- const template = await provider.getTemplateById(id)
83
- set({ activeTemplate: template })
84
- return template
85
- } catch {
86
- set({ activeTemplate: null })
87
- return null
88
- } finally {
89
- set({ activeTemplateLoading: false })
90
- }
91
- },
92
-
93
- async createTemplate(input: CreateTemplateInput) {
94
- const template = await provider.createTemplate(input)
95
- set((s) => ({ templates: [template, ...s.templates], templatesTotal: s.templatesTotal + 1 }))
96
- return template
97
- },
98
-
99
- async updateTemplate(id: string, input: UpdateTemplateInput) {
100
- const template = await provider.updateTemplate(id, input)
101
- set((s) => ({
102
- templates: s.templates.map((t) => (t.id === id ? template : t)),
103
- activeTemplate: s.activeTemplate?.id === id ? template : s.activeTemplate,
104
- }))
105
- return template
106
- },
107
-
108
- async deleteTemplate(id: string) {
109
- await provider.deleteTemplate(id)
110
- set((s) => ({
111
- templates: s.templates.filter((t) => t.id !== id),
112
- templatesTotal: s.templatesTotal - 1,
113
- }))
114
- },
115
-
116
- async fetchDocuments(query?: DocumentQuery) {
117
- set({ documentsLoading: true, documentsError: null })
118
- try {
119
- const result = await provider.getDocuments(query ?? {})
120
- set({ documents: result.data, documentsTotal: result.total })
121
- } catch (e: any) {
122
- set({ documentsError: e.message ?? 'Failed to fetch documents' })
123
- } finally {
124
- set({ documentsLoading: false })
125
- }
126
- },
127
-
128
- async fetchDocumentById(id: string) {
129
- set({ activeDocumentLoading: true })
130
- try {
131
- const doc = await provider.getDocumentById(id)
132
- set({ activeDocument: doc })
133
- return doc
134
- } catch {
135
- set({ activeDocument: null })
136
- return null
137
- } finally {
138
- set({ activeDocumentLoading: false })
139
- }
140
- },
141
-
142
- async createDocument(input: CreateDocumentInput) {
143
- const doc = await provider.createDocument(input)
144
- set((s) => ({ documents: [doc, ...s.documents], documentsTotal: s.documentsTotal + 1 }))
145
- return doc
146
- },
147
-
148
- async updateDocument(id: string, input: UpdateDocumentInput) {
149
- const doc = await provider.updateDocument(id, input)
150
- set((s) => ({
151
- documents: s.documents.map((d) => (d.id === id ? doc : d)),
152
- activeDocument: s.activeDocument?.id === id ? doc : s.activeDocument,
153
- }))
154
- return doc
155
- },
156
-
157
- async deleteDocument(id: string) {
158
- await provider.deleteDocument(id)
159
- set((s) => ({
160
- documents: s.documents.filter((d) => d.id !== id),
161
- documentsTotal: s.documentsTotal - 1,
162
- }))
163
- },
164
- }))
165
- }
166
-
167
- export type CustomFormsStore = ReturnType<typeof createCustomFormsStore>
package/src/types.ts DELETED
@@ -1,217 +0,0 @@
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
- /**
40
- * Optional binding to a field on the person/contact record. When set, a new
41
- * document created from a person's profile is pre-filled with that person's
42
- * value (e.g. `map: 'documentNumber'` fills the field with the person's CPF).
43
- * Matched tolerantly against camelCase/snake_case record keys. Only prefills
44
- * empty fields; the user can still edit the value. See `lib/person-fields`.
45
- */
46
- map?: string
47
- /**
48
- * Static default value applied to a NEW document when the field has no mapped
49
- * person value and no existing value — e.g. pre-written contract clause text
50
- * on a richtext field. Editable by the user.
51
- */
52
- defaultValue?: unknown
53
- /** Type-specific configuration */
54
- config?: Record<string, unknown>
55
- }
56
-
57
- export interface FormSchema {
58
- fields: FormFieldDef[]
59
- layout: { columns: 12 }
60
- }
61
-
62
- // ============================================================
63
- // TEMPLATE (maps to plg_forms_templates)
64
- // ============================================================
65
-
66
- export type TemplateCategory =
67
- | 'anamnesis'
68
- | 'evolution'
69
- | 'report'
70
- | 'contract'
71
- | 'general'
72
-
73
- export interface FormTemplate {
74
- id: string
75
- tenantId: string
76
- name: string
77
- description?: string
78
- category: TemplateCategory
79
- version: number
80
- isCurrent: boolean
81
- parentId?: string
82
- schema: FormSchema
83
- specialty?: string
84
- tags: string[]
85
- metadata: Record<string, unknown>
86
- isActive: boolean
87
- isDeleted: boolean
88
- createdBy?: string
89
- updatedBy?: string
90
- createdAt: string
91
- updatedAt: string
92
- }
93
-
94
- // ============================================================
95
- // DOCUMENT (maps to plg_forms_documents)
96
- // ============================================================
97
-
98
- export type DocumentStatus = 'draft' | 'completed' | 'signed' | 'archived'
99
-
100
- export interface FormDocument {
101
- id: string
102
- tenantId: string
103
- templateId: string
104
- templateName?: string
105
- templateCategory?: TemplateCategory
106
- personId?: string
107
- personName?: string
108
- title?: string
109
- data: Record<string, unknown>
110
- status: DocumentStatus
111
- signedAt?: string
112
- signedBy?: string
113
- notes?: string
114
- metadata: Record<string, unknown>
115
- isDeleted: boolean
116
- createdBy?: string
117
- updatedBy?: string
118
- createdAt: string
119
- updatedAt: string
120
- /** Document kind from public.documents */
121
- kind?: string
122
- /** File URL for image/attachment documents */
123
- fileUrl?: string
124
- fileName?: string
125
- fileSize?: number
126
- mimeType?: string
127
- }
128
-
129
- // ============================================================
130
- // DOCUMENT FILE (maps to plg_forms_document_files)
131
- // ============================================================
132
-
133
- export interface DocumentFile {
134
- id: string
135
- tenantId: string
136
- documentId: string
137
- fieldKey: string
138
- fileUrl: string
139
- fileName?: string
140
- fileSize?: number
141
- mimeType?: string
142
- sortOrder: number
143
- metadata: Record<string, unknown>
144
- createdAt: string
145
- }
146
-
147
- // ============================================================
148
- // QUERIES
149
- // ============================================================
150
-
151
- export interface TemplateQuery {
152
- category?: TemplateCategory
153
- search?: string
154
- page?: number
155
- pageSize?: number
156
- }
157
-
158
- export interface DocumentQuery {
159
- personId?: string
160
- templateId?: string
161
- status?: DocumentStatus | DocumentStatus[]
162
- search?: string
163
- page?: number
164
- pageSize?: number
165
- }
166
-
167
- export interface PaginatedResult<T> {
168
- data: T[]
169
- total: number
170
- }
171
-
172
- // ============================================================
173
- // INPUTS
174
- // ============================================================
175
-
176
- export interface CreateTemplateInput {
177
- name: string
178
- description?: string
179
- category: TemplateCategory
180
- schema: FormSchema
181
- specialty?: string
182
- tags?: string[]
183
- metadata?: Record<string, unknown>
184
- }
185
-
186
- export interface UpdateTemplateInput {
187
- name?: string
188
- description?: string
189
- category?: TemplateCategory
190
- schema?: FormSchema
191
- specialty?: string
192
- tags?: string[]
193
- metadata?: Record<string, unknown>
194
- isActive?: boolean
195
- }
196
-
197
- export interface CreateDocumentInput {
198
- templateId?: string
199
- personId?: string
200
- title?: string
201
- data?: Record<string, unknown>
202
- status?: DocumentStatus
203
- /** Document kind: 'form', 'image', 'attachment', etc. */
204
- kind?: string
205
- /** File fields for non-form documents */
206
- fileUrl?: string
207
- fileName?: string
208
- fileSize?: number
209
- mimeType?: string
210
- }
211
-
212
- export interface UpdateDocumentInput {
213
- data?: Record<string, unknown>
214
- status?: DocumentStatus
215
- title?: string
216
- notes?: string
217
- }
@@ -1,105 +0,0 @@
1
- import React, { useState, useCallback } from 'react'
2
- import { TemplateListView } from './TemplateListView'
3
- import type { CustomFormsConfig } from '../config'
4
- import type { CustomFormsDataProvider } from '../data/types'
5
- import type { CustomFormsStore } from '../store'
6
- import type { FormTemplate } from '../types'
7
-
8
- interface CustomFormsSettingsTabProps {
9
- config: CustomFormsConfig
10
- provider: CustomFormsDataProvider
11
- store: CustomFormsStore
12
- }
13
-
14
- const ROUTE_BASE = '/settings/custom_forms'
15
-
16
- /** Read the currently-open template id from the URL hash (the source of truth,
17
- * so deep links and browser back/forward work like any CRUD detail page). */
18
- function readBuilderIdFromHash(): string | null {
19
- const hash = window.location.hash.slice(1) || '/'
20
- const m = hash.match(/\/settings\/custom_forms\/templates\/([^/]+)/)
21
- if (!m) return null
22
- return m[1] === 'new' ? '__new__' : m[1]
23
- }
24
-
25
- export function CustomFormsSettingsTab({ config, provider, store }: CustomFormsSettingsTabProps) {
26
- // URL is the source of truth — opening/closing a template updates the hash,
27
- // so the record id shows in the URL just like opening a CRUD record.
28
- const [builderTemplateId, setBuilderTemplateId] = useState<string | null>(readBuilderIdFromHash)
29
-
30
- const handleNew = useCallback(() => {
31
- window.location.hash = `#${ROUTE_BASE}/templates/new`
32
- }, [])
33
-
34
- const handleEdit = useCallback((template: FormTemplate) => {
35
- window.location.hash = `#${ROUTE_BASE}/templates/${template.id}`
36
- }, [])
37
-
38
- const handleBackFromBuilder = useCallback(() => {
39
- // Return to the list route and re-fetch templates to reflect any saves.
40
- window.location.hash = `#${ROUTE_BASE}/forms`
41
- store.getState().fetchTemplates()
42
- }, [store])
43
-
44
- // Keep local state in sync with the URL (deep links, back/forward, and our
45
- // own hash writes above all flow through here).
46
- React.useEffect(() => {
47
- const sync = () => setBuilderTemplateId(readBuilderIdFromHash())
48
- window.addEventListener('hashchange', sync)
49
- return () => window.removeEventListener('hashchange', sync)
50
- }, [])
51
-
52
- return builderTemplateId ? (
53
- <BuilderWrapper
54
- templateId={builderTemplateId === '__new__' ? undefined : builderTemplateId}
55
- store={store}
56
- provider={provider}
57
- config={config}
58
- onBack={handleBackFromBuilder}
59
- />
60
- ) : (
61
- <TemplateListView
62
- store={store}
63
- config={config}
64
- onEdit={handleEdit}
65
- onNew={handleNew}
66
- />
67
- )
68
- }
69
-
70
- /** Lazy-loads the FormBuilder to avoid pulling @dnd-kit into the initial bundle */
71
- function BuilderWrapper({
72
- templateId,
73
- store,
74
- provider,
75
- config,
76
- onBack,
77
- }: {
78
- templateId?: string
79
- store: CustomFormsStore
80
- provider: CustomFormsDataProvider
81
- config: CustomFormsConfig
82
- onBack: () => void
83
- }) {
84
- const FormBuilderLazy = React.lazy(() =>
85
- import('../components/FormBuilder').then((m) => ({ default: m.FormBuilder })),
86
- )
87
-
88
- return (
89
- <React.Suspense
90
- fallback={
91
- <div className="flex items-center justify-center py-12">
92
- <div className="h-6 w-6 border-2 border-primary border-t-transparent animate-spin rounded-full" />
93
- </div>
94
- }
95
- >
96
- <FormBuilderLazy
97
- templateId={templateId}
98
- store={store}
99
- provider={provider}
100
- config={config}
101
- onBack={onBack}
102
- />
103
- </React.Suspense>
104
- )
105
- }
@@ -1,174 +0,0 @@
1
- import React, { useEffect, useState, useCallback } from 'react'
2
- import { Plus, Pencil, Archive, 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 { PermissionGate } from '@fayz-ai/saas'
8
- import { useTranslation } from '@fayz-ai/core'
9
- import { getFormsTenantId } from '../lib/tenant'
10
- import type { CustomFormsStore } from '../store'
11
- import type { CustomFormsConfig } from '../config'
12
- import type { FormTemplate, TemplateCategory } from '../types'
13
-
14
- interface TemplateListViewProps {
15
- store: CustomFormsStore
16
- config: CustomFormsConfig
17
- onEdit: (template: FormTemplate) => void
18
- onNew: () => void
19
- }
20
-
21
- const CATEGORY_COLORS: Record<TemplateCategory, string> = {
22
- anamnesis: 'bg-info-soft text-info-soft-foreground',
23
- evolution: 'bg-success-soft text-success-soft-foreground',
24
- report: 'bg-magic-soft text-magic-soft-foreground',
25
- contract: 'bg-warning-soft text-warning-soft-foreground',
26
- general: 'bg-muted text-muted-foreground',
27
- }
28
-
29
- export function TemplateListView({ store, config, onEdit, onNew }: TemplateListViewProps) {
30
- const t = useTranslation()
31
- const [search, setSearch] = useState('')
32
-
33
- // Subscribe to zustand store reactively
34
- const templates = store((s) => s.templates)
35
- const loading = store((s) => s.templatesLoading)
36
-
37
- const orgId = getFormsTenantId()
38
-
39
- // Fetch when org is ready or search changes — debounce to avoid Strict Mode double-fire
40
- useEffect(() => {
41
- if (!orgId) return
42
- const timer = setTimeout(() => {
43
- store.getState().fetchTemplates({ search: search || undefined })
44
- }, 50)
45
- return () => clearTimeout(timer)
46
- }, [orgId, search])
47
-
48
- // Archive, not delete: deactivate the template so it drops out of the list
49
- // and the "add document" picker, without destroying it or its documents.
50
- const handleArchive = useCallback(async (template: FormTemplate) => {
51
- if (!window.confirm(t('customForms.archiveTemplateConfirm'))) return
52
- await store.getState().updateTemplate(template.id, { isActive: false })
53
- }, [store, t])
54
-
55
- // Archived (isActive=false) templates stay in the store but leave the view.
56
- const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false)
57
-
58
- const categoryLabel = (cat: TemplateCategory) =>
59
- t(`customForms.category.${cat}`)
60
-
61
- return (
62
- <div className="space-y-4">
63
- {/* Search + New */}
64
- <div className="flex items-center gap-3">
65
- <div className="relative flex-1 max-w-xs">
66
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
67
- <Input
68
- value={search}
69
- onChange={(e) => setSearch(e.target.value)}
70
- placeholder={t('common.search')}
71
- className="h-9 pl-8 text-sm"
72
- />
73
- </div>
74
- <PermissionGate feature="custom_forms" action="create">
75
- <Button onClick={onNew} size="sm" className="h-9">
76
- <Plus className="h-3.5 w-3.5 mr-1.5" />
77
- {t('customForms.newTemplate')}
78
- </Button>
79
- </PermissionGate>
80
- </div>
81
-
82
- {/* Empty state */}
83
- {!loading && visibleTemplates.length === 0 && (
84
- <Card>
85
- <CardContent className="flex flex-col items-center justify-center py-16 text-center">
86
- <div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4">
87
- <FileText className="h-5 w-5 text-muted-foreground" />
88
- </div>
89
- <p className="font-medium">{t('customForms.noTemplates')}</p>
90
- <p className="text-sm text-muted-foreground mt-1 max-w-sm">
91
- {t('customForms.noTemplatesDescription')}
92
- </p>
93
- <PermissionGate feature="custom_forms" action="create">
94
- <Button onClick={onNew} size="sm" className="mt-4">
95
- <Plus className="h-3.5 w-3.5 mr-1.5" />
96
- {t('customForms.newTemplate')}
97
- </Button>
98
- </PermissionGate>
99
- </CardContent>
100
- </Card>
101
- )}
102
-
103
- {/* Template list */}
104
- {visibleTemplates.length > 0 && (
105
- <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
106
- {visibleTemplates.map((template) => (
107
- <Card
108
- key={template.id}
109
- className="group cursor-pointer hover:shadow-md transition-shadow"
110
- onClick={() => onEdit(template)}
111
- >
112
- <CardContent className="p-4">
113
- <div className="flex items-start justify-between gap-2">
114
- <div className="min-w-0 flex-1">
115
- <h3 className="font-medium text-sm truncate">{template.name}</h3>
116
- {template.description && (
117
- <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
118
- {template.description}
119
- </p>
120
- )}
121
- </div>
122
- <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
123
- <button
124
- onClick={(e) => { e.stopPropagation(); onEdit(template) }}
125
- className="p-1.5 rounded-md hover:bg-muted"
126
- >
127
- <Pencil className="h-3.5 w-3.5 text-muted-foreground" />
128
- </button>
129
- <button
130
- onClick={(e) => { e.stopPropagation(); handleArchive(template) }}
131
- className="p-1.5 rounded-md hover:bg-muted"
132
- title={t('customForms.archive')}
133
- >
134
- <Archive className="h-3.5 w-3.5 text-muted-foreground" />
135
- </button>
136
- </div>
137
- </div>
138
- <div className="flex items-center gap-2 mt-3">
139
- <Badge variant="secondary" className={`text-[10px] ${CATEGORY_COLORS[template.category]}`}>
140
- {categoryLabel(template.category)}
141
- </Badge>
142
- <span className="text-[10px] text-muted-foreground">
143
- {template.schema.fields.length} {t('customForms.builder.fields').toLowerCase()}
144
- </span>
145
- {template.version > 1 && (
146
- <span className="text-[10px] text-muted-foreground">v{template.version}</span>
147
- )}
148
- </div>
149
- </CardContent>
150
- </Card>
151
- ))}
152
- </div>
153
- )}
154
-
155
- {/* Loading skeleton */}
156
- {loading && (
157
- <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
158
- {Array.from({ length: 3 }).map((_, i) => (
159
- <Card key={i}>
160
- <CardContent className="p-4 space-y-3">
161
- <div className="h-4 w-2/3 bg-muted animate-pulse rounded" />
162
- <div className="h-3 w-full bg-muted animate-pulse rounded" />
163
- <div className="flex gap-2">
164
- <div className="h-5 w-16 bg-muted animate-pulse rounded-full" />
165
- <div className="h-5 w-12 bg-muted animate-pulse rounded-full" />
166
- </div>
167
- </CardContent>
168
- </Card>
169
- ))}
170
- </div>
171
- )}
172
- </div>
173
- )
174
- }
File without changes