@fayz-ai/plugin-forms 0.9.0 → 0.9.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/README.md +3 -3
- package/dist/{FormBuilder-7SWZQKFN.js → FormBuilder-JCNHXQXT.js} +4 -4
- package/dist/{FormBuilder-7SWZQKFN.js.map → FormBuilder-JCNHXQXT.js.map} +1 -1
- package/dist/{chunk-W2ZNJGQC.js → chunk-JCAWSDX6.js} +3 -3
- package/dist/chunk-JCAWSDX6.js.map +1 -0
- package/dist/components/AddDocumentDropdown.d.ts +1 -1
- package/dist/components/AddDocumentDropdown.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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- 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/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
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
|
|
File without changes
|