@fayz-ai/plugin-forms 0.9.0-next.1 → 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
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { useDraggable } from '@dnd-kit/core'
|
|
3
|
-
import {
|
|
4
|
-
Heading, Type, AlignLeft, FileEdit, CalendarDays,
|
|
5
|
-
ChevronDown, CircleDot, Tags, CheckSquare,
|
|
6
|
-
Image, GalleryHorizontalEnd, Receipt,
|
|
7
|
-
} from 'lucide-react'
|
|
8
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
9
|
-
import type { FormFieldType } from '../types'
|
|
10
|
-
|
|
11
|
-
export interface FieldTypeItem {
|
|
12
|
-
type: FormFieldType
|
|
13
|
-
icon: React.ElementType
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const DEFAULT_COL_SPAN: Record<string, number> = {
|
|
17
|
-
title: 12,
|
|
18
|
-
memo: 12,
|
|
19
|
-
richtext: 12,
|
|
20
|
-
gallery: 12,
|
|
21
|
-
budget: 12,
|
|
22
|
-
image: 6,
|
|
23
|
-
text: 6,
|
|
24
|
-
date: 4,
|
|
25
|
-
select: 6,
|
|
26
|
-
radio: 6,
|
|
27
|
-
tags: 8,
|
|
28
|
-
checkbox: 4,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const FIELD_TYPES: FieldTypeItem[] = [
|
|
32
|
-
{ type: 'title', icon: Heading },
|
|
33
|
-
{ type: 'text', icon: Type },
|
|
34
|
-
{ type: 'memo', icon: AlignLeft },
|
|
35
|
-
{ type: 'richtext', icon: FileEdit },
|
|
36
|
-
{ type: 'date', icon: CalendarDays },
|
|
37
|
-
{ type: 'select', icon: ChevronDown },
|
|
38
|
-
{ type: 'radio', icon: CircleDot },
|
|
39
|
-
{ type: 'tags', icon: Tags },
|
|
40
|
-
{ type: 'checkbox', icon: CheckSquare },
|
|
41
|
-
{ type: 'image', icon: Image },
|
|
42
|
-
{ type: 'gallery', icon: GalleryHorizontalEnd },
|
|
43
|
-
{ type: 'budget', icon: Receipt },
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
function DraggableFieldType({ item }: { item: FieldTypeItem }) {
|
|
47
|
-
const t = useTranslation()
|
|
48
|
-
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
49
|
-
id: `palette-${item.type}`,
|
|
50
|
-
data: { source: 'palette', fieldType: item.type },
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const Icon = item.icon
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<button
|
|
57
|
-
ref={setNodeRef}
|
|
58
|
-
{...listeners}
|
|
59
|
-
{...attributes}
|
|
60
|
-
className={`flex items-center gap-2 w-full px-3 py-2 rounded-lg border bg-card text-sm font-medium transition-all hover:shadow-sm hover:border-primary/30 cursor-grab active:cursor-grabbing ${isDragging ? 'opacity-30' : ''}`}
|
|
61
|
-
>
|
|
62
|
-
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
63
|
-
<span className="truncate">{t(`customForms.fieldType.${item.type}`)}</span>
|
|
64
|
-
</button>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function FieldPalette() {
|
|
69
|
-
const t = useTranslation()
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className="w-44 shrink-0 space-y-1.5">
|
|
73
|
-
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground px-1 mb-2">
|
|
74
|
-
{t('customForms.builder.fields')}
|
|
75
|
-
</p>
|
|
76
|
-
{FIELD_TYPES.map((item) => (
|
|
77
|
-
<DraggableFieldType key={item.type} item={item} />
|
|
78
|
-
))}
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
DndContext,
|
|
4
|
-
DragEndEvent,
|
|
5
|
-
DragOverlay,
|
|
6
|
-
DragStartEvent,
|
|
7
|
-
PointerSensor,
|
|
8
|
-
pointerWithin,
|
|
9
|
-
useSensor,
|
|
10
|
-
useSensors,
|
|
11
|
-
} from '@dnd-kit/core'
|
|
12
|
-
import { arrayMove } from '@dnd-kit/sortable'
|
|
13
|
-
import { snapCenterToCursor } from '@dnd-kit/modifiers'
|
|
14
|
-
import { ArrowLeft, Eye, Pencil, Printer } from 'lucide-react'
|
|
15
|
-
import { Button, useSaveBar, toast } from '@fayz-ai/ui'
|
|
16
|
-
import { Input } from '@fayz-ai/ui'
|
|
17
|
-
import { Card, CardContent } from '@fayz-ai/ui'
|
|
18
|
-
import { Badge } from '@fayz-ai/ui'
|
|
19
|
-
import {
|
|
20
|
-
Select,
|
|
21
|
-
SelectContent,
|
|
22
|
-
SelectItem,
|
|
23
|
-
SelectTrigger,
|
|
24
|
-
SelectValue,
|
|
25
|
-
} from '@fayz-ai/ui'
|
|
26
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
27
|
-
import { useLimitGuard, invalidateLimit, useAgentSurface, type AgentSurfaceContribution } from '@fayz-ai/saas'
|
|
28
|
-
import type { CustomFormsStore } from '../store'
|
|
29
|
-
import type { CustomFormsDataProvider } from '../data/types'
|
|
30
|
-
import type { CustomFormsConfig } from '../config'
|
|
31
|
-
import type { FormFieldDef, FormSchema, TemplateCategory } from '../types'
|
|
32
|
-
import { FieldPalette, FIELD_TYPES, DEFAULT_COL_SPAN } from './FieldPalette'
|
|
33
|
-
import { BuilderCanvas } from './BuilderCanvas'
|
|
34
|
-
import { FieldConfigDrawer } from './FieldConfigDialog'
|
|
35
|
-
import { createPortal } from 'react-dom'
|
|
36
|
-
import { FormRenderer } from './FormRenderer'
|
|
37
|
-
import { buildPrintHtml } from '../lib/print'
|
|
38
|
-
import { FORM_BUILDER_INSTRUCTIONS } from '../agent-tools'
|
|
39
|
-
|
|
40
|
-
const TEMPLATE_CATEGORIES: TemplateCategory[] = ['anamnesis', 'evolution', 'report', 'contract', 'general']
|
|
41
|
-
|
|
42
|
-
interface FormBuilderProps {
|
|
43
|
-
templateId?: string
|
|
44
|
-
store: CustomFormsStore
|
|
45
|
-
provider: CustomFormsDataProvider
|
|
46
|
-
config: CustomFormsConfig
|
|
47
|
-
onBack: () => void
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function FormBuilder({ templateId, store, provider, config, onBack }: FormBuilderProps) {
|
|
51
|
-
const t = useTranslation()
|
|
52
|
-
const guardTemplates = useLimitGuard('form_templates')
|
|
53
|
-
|
|
54
|
-
const [name, setName] = useState('')
|
|
55
|
-
const [description, setDescription] = useState('')
|
|
56
|
-
const [category, setCategory] = useState<TemplateCategory>('general')
|
|
57
|
-
const [fields, setFields] = useState<FormFieldDef[]>([])
|
|
58
|
-
const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null)
|
|
59
|
-
const [saving, setSaving] = useState(false)
|
|
60
|
-
const [showPreview, setShowPreview] = useState(false)
|
|
61
|
-
const [previewMode, setPreviewMode] = useState<'editor' | 'print'>('editor')
|
|
62
|
-
const [previewData, setPreviewData] = useState<Record<string, unknown>>({})
|
|
63
|
-
const [draggingFieldType, setDraggingFieldType] = useState<import('../types').FormFieldType | null>(null)
|
|
64
|
-
const [templateName, setTemplateName] = useState<string | null>(null)
|
|
65
|
-
|
|
66
|
-
// Load existing template
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (!templateId) return
|
|
69
|
-
let cancelled = false
|
|
70
|
-
provider.getTemplateById(templateId).then((tpl) => {
|
|
71
|
-
if (cancelled || !tpl) return
|
|
72
|
-
setName(tpl.name)
|
|
73
|
-
setTemplateName(tpl.name)
|
|
74
|
-
setDescription(tpl.description ?? '')
|
|
75
|
-
setCategory(tpl.category)
|
|
76
|
-
setFields(tpl.schema.fields)
|
|
77
|
-
})
|
|
78
|
-
return () => { cancelled = true }
|
|
79
|
-
}, [templateId, provider])
|
|
80
|
-
|
|
81
|
-
const sensors = useSensors(
|
|
82
|
-
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
86
|
-
const data = event.active.data.current
|
|
87
|
-
if (data?.source === 'palette') {
|
|
88
|
-
setDraggingFieldType(data.fieldType)
|
|
89
|
-
}
|
|
90
|
-
}, [])
|
|
91
|
-
|
|
92
|
-
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
|
93
|
-
setDraggingFieldType(null)
|
|
94
|
-
const { active, over } = event
|
|
95
|
-
|
|
96
|
-
// Dropped from palette onto canvas
|
|
97
|
-
if (active.data.current?.source === 'palette' && over) {
|
|
98
|
-
const fieldType = active.data.current.fieldType
|
|
99
|
-
const typeDef = FIELD_TYPES.find((ft) => ft.type === fieldType)
|
|
100
|
-
if (!typeDef) return
|
|
101
|
-
|
|
102
|
-
const newField: FormFieldDef = {
|
|
103
|
-
id: crypto.randomUUID(),
|
|
104
|
-
type: fieldType,
|
|
105
|
-
label: t(`customForms.fieldType.${fieldType}`),
|
|
106
|
-
col: 0,
|
|
107
|
-
row: fields.length,
|
|
108
|
-
colSpan: DEFAULT_COL_SPAN[fieldType] ?? 12,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (['select', 'radio', 'tags'].includes(fieldType)) {
|
|
112
|
-
newField.options = [
|
|
113
|
-
{ label: 'Option 1', value: 'option_1' },
|
|
114
|
-
{ label: 'Option 2', value: 'option_2' },
|
|
115
|
-
]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
setFields((prev) => [...prev, newField])
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Reorder within canvas
|
|
123
|
-
if (active.id !== over?.id && over) {
|
|
124
|
-
setFields((prev) => {
|
|
125
|
-
const oldIndex = prev.findIndex((f) => f.id === active.id)
|
|
126
|
-
const newIndex = prev.findIndex((f) => f.id === over.id)
|
|
127
|
-
if (oldIndex === -1 || newIndex === -1) return prev
|
|
128
|
-
const reordered = arrayMove(prev, oldIndex, newIndex)
|
|
129
|
-
return reordered.map((f, i) => ({ ...f, row: i }))
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
}, [fields, t])
|
|
133
|
-
|
|
134
|
-
const handleSelectField = useCallback((field: FormFieldDef) => {
|
|
135
|
-
setSelectedFieldId(field.id)
|
|
136
|
-
}, [])
|
|
137
|
-
|
|
138
|
-
const handleRemoveField = useCallback((fieldId: string) => {
|
|
139
|
-
setFields((prev) => prev.filter((f) => f.id !== fieldId).map((f, i) => ({ ...f, row: i })))
|
|
140
|
-
if (selectedFieldId === fieldId) setSelectedFieldId(null)
|
|
141
|
-
}, [selectedFieldId])
|
|
142
|
-
|
|
143
|
-
const handleFieldUpdate = useCallback((updated: FormFieldDef) => {
|
|
144
|
-
setFields((prev) => prev.map((f) => (f.id === updated.id ? updated : f)))
|
|
145
|
-
}, [])
|
|
146
|
-
|
|
147
|
-
// --- Assistant surface: publish WHAT THE USER IS EDITING as context --------
|
|
148
|
-
// The build action is the server-plane createOrUpdateForm tool (a pool RPC),
|
|
149
|
-
// which works from any page. This surface only tells the model which template
|
|
150
|
-
// is open and its current fields+ids, so "add an allergies field to THIS
|
|
151
|
-
// form" resolves to the right templateId. No client-side mutation here.
|
|
152
|
-
const surfaceStateRef = React.useRef({ templateId, name, description, category, fields })
|
|
153
|
-
surfaceStateRef.current = { templateId, name, description, category, fields }
|
|
154
|
-
|
|
155
|
-
const agentSurface = React.useMemo<AgentSurfaceContribution>(
|
|
156
|
-
() => ({
|
|
157
|
-
id: 'custom_forms.builder',
|
|
158
|
-
pluginId: 'custom_forms',
|
|
159
|
-
instructions: FORM_BUILDER_INSTRUCTIONS,
|
|
160
|
-
describe: () => {
|
|
161
|
-
const o = surfaceStateRef.current
|
|
162
|
-
return {
|
|
163
|
-
title: `Construtor de formulário — ${o.name?.trim() || 'novo modelo'}`,
|
|
164
|
-
state: {
|
|
165
|
-
// Pass this as `templateId` to createOrUpdateForm when editing THIS
|
|
166
|
-
// form. Null = an unsaved new form (create, then it gets an id).
|
|
167
|
-
templateId: o.templateId ?? null,
|
|
168
|
-
unsaved: !o.templateId,
|
|
169
|
-
name: o.name ?? '',
|
|
170
|
-
category: o.category ?? 'general',
|
|
171
|
-
description: o.description ?? '',
|
|
172
|
-
fieldCount: o.fields.length,
|
|
173
|
-
fields: o.fields.map((f) => ({
|
|
174
|
-
id: f.id,
|
|
175
|
-
type: f.type,
|
|
176
|
-
label: f.label,
|
|
177
|
-
required: !!f.required,
|
|
178
|
-
...(f.options?.length ? { options: f.options.map((x) => x.label) } : {}),
|
|
179
|
-
})),
|
|
180
|
-
},
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
}),
|
|
184
|
-
[],
|
|
185
|
-
)
|
|
186
|
-
useAgentSurface(agentSurface)
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
const handleFieldConfigSave = useCallback((updated: FormFieldDef) => {
|
|
190
|
-
handleFieldUpdate(updated)
|
|
191
|
-
setSelectedFieldId(null)
|
|
192
|
-
}, [handleFieldUpdate])
|
|
193
|
-
|
|
194
|
-
const handleSave = useCallback(async () => {
|
|
195
|
-
if (!name.trim()) { toast.error(t('common.formIncomplete')); return }
|
|
196
|
-
// Plan quantity guard (client-side, before the provider call) — only on
|
|
197
|
-
// create. Opens the global UpgradeModal and aborts when the cap is reached.
|
|
198
|
-
if (!templateId && (await guardTemplates()) === 'blocked') return
|
|
199
|
-
setSaving(true)
|
|
200
|
-
try {
|
|
201
|
-
const schema: FormSchema = { fields, layout: { columns: 12 } }
|
|
202
|
-
|
|
203
|
-
if (templateId) {
|
|
204
|
-
await store.getState().updateTemplate(templateId, { name, description, category, schema })
|
|
205
|
-
} else {
|
|
206
|
-
await store.getState().createTemplate({ name, description, category, schema })
|
|
207
|
-
invalidateLimit('form_templates')
|
|
208
|
-
}
|
|
209
|
-
onBack()
|
|
210
|
-
} finally {
|
|
211
|
-
setSaving(false)
|
|
212
|
-
}
|
|
213
|
-
}, [templateId, name, description, category, fields, store, onBack, guardTemplates])
|
|
214
|
-
|
|
215
|
-
// Dirty detection — snapshot editable fields once async load settles
|
|
216
|
-
const currentFields = { name, description, category, fields }
|
|
217
|
-
const snapshot = React.useRef<string | null>(null)
|
|
218
|
-
const loaded = !templateId || templateName !== null
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
if (loaded && snapshot.current === null) snapshot.current = JSON.stringify(currentFields)
|
|
221
|
-
}, [loaded])
|
|
222
|
-
const dirty = snapshot.current !== null && JSON.stringify(currentFields) !== snapshot.current
|
|
223
|
-
|
|
224
|
-
useSaveBar({
|
|
225
|
-
dirty,
|
|
226
|
-
saving,
|
|
227
|
-
onSave: () => { void handleSave() },
|
|
228
|
-
onDiscard: () => onBack(),
|
|
229
|
-
saveLabel: t('customForms.builder.save'),
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
const selectedField = selectedFieldId
|
|
233
|
-
? fields.find((f) => f.id === selectedFieldId) ?? null
|
|
234
|
-
: null
|
|
235
|
-
|
|
236
|
-
const isNew = !templateId
|
|
237
|
-
const breadcrumbLabel = isNew
|
|
238
|
-
? t('customForms.newTemplate')
|
|
239
|
-
: templateName ?? t('customForms.editTemplate')
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<div className="space-y-4">
|
|
243
|
-
{/* Breadcrumb */}
|
|
244
|
-
<div className="flex items-center gap-1.5 text-sm">
|
|
245
|
-
<button
|
|
246
|
-
onClick={onBack}
|
|
247
|
-
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
|
|
248
|
-
>
|
|
249
|
-
<ArrowLeft className="h-3.5 w-3.5" />
|
|
250
|
-
{t('customForms.templates')}
|
|
251
|
-
</button>
|
|
252
|
-
<span className="text-muted-foreground">/</span>
|
|
253
|
-
<span className="font-medium">{breadcrumbLabel}</span>
|
|
254
|
-
</div>
|
|
255
|
-
|
|
256
|
-
{/* Form meta — card layout matching CRUD form */}
|
|
257
|
-
<Card>
|
|
258
|
-
<CardContent className="p-5 space-y-4">
|
|
259
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
260
|
-
<div className="space-y-1.5">
|
|
261
|
-
<label className="text-sm font-medium">
|
|
262
|
-
{t('customForms.templateName')} <span className="text-destructive">*</span>
|
|
263
|
-
</label>
|
|
264
|
-
<Input
|
|
265
|
-
value={name}
|
|
266
|
-
onChange={(e) => setName(e.target.value)}
|
|
267
|
-
placeholder={t('customForms.templateName')}
|
|
268
|
-
className="h-9 text-sm"
|
|
269
|
-
autoFocus={isNew}
|
|
270
|
-
/>
|
|
271
|
-
</div>
|
|
272
|
-
<div className="space-y-1.5">
|
|
273
|
-
<label className="text-sm font-medium">{t('customForms.templateCategory')}</label>
|
|
274
|
-
<Select value={category} onValueChange={(v) => setCategory(v as TemplateCategory)}>
|
|
275
|
-
<SelectTrigger className="h-9 text-sm">
|
|
276
|
-
<SelectValue />
|
|
277
|
-
</SelectTrigger>
|
|
278
|
-
<SelectContent>
|
|
279
|
-
{TEMPLATE_CATEGORIES.map((cat) => (
|
|
280
|
-
<SelectItem key={cat} value={cat}>
|
|
281
|
-
{t(`customForms.category.${cat}`)}
|
|
282
|
-
</SelectItem>
|
|
283
|
-
))}
|
|
284
|
-
</SelectContent>
|
|
285
|
-
</Select>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
<div className="space-y-1.5">
|
|
289
|
-
<label className="text-sm font-medium">{t('customForms.templateDescription')}</label>
|
|
290
|
-
<Input
|
|
291
|
-
value={description}
|
|
292
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
293
|
-
placeholder={t('customForms.templateDescription')}
|
|
294
|
-
className="h-9 text-sm"
|
|
295
|
-
/>
|
|
296
|
-
</div>
|
|
297
|
-
</CardContent>
|
|
298
|
-
</Card>
|
|
299
|
-
|
|
300
|
-
{/* Builder section header */}
|
|
301
|
-
<div className="flex items-center justify-between">
|
|
302
|
-
<div className="flex items-center gap-2">
|
|
303
|
-
<h3 className="text-sm font-semibold">{t('customForms.builder.fields')}</h3>
|
|
304
|
-
<Badge variant="secondary" className="text-[10px]">
|
|
305
|
-
{t('customForms.builder.fieldCount', { count: String(fields.length) })}
|
|
306
|
-
</Badge>
|
|
307
|
-
</div>
|
|
308
|
-
<div className="flex items-center gap-2">
|
|
309
|
-
<Button
|
|
310
|
-
variant="outline"
|
|
311
|
-
size="sm"
|
|
312
|
-
className="h-9"
|
|
313
|
-
disabled={fields.length === 0}
|
|
314
|
-
onClick={() => { setPreviewData({}); setPreviewMode('editor'); setShowPreview(true) }}
|
|
315
|
-
>
|
|
316
|
-
<Eye className="h-3.5 w-3.5 mr-1.5" />
|
|
317
|
-
Preview
|
|
318
|
-
</Button>
|
|
319
|
-
</div>
|
|
320
|
-
</div>
|
|
321
|
-
|
|
322
|
-
{/* Builder body — drag palette + canvas */}
|
|
323
|
-
<DndContext sensors={sensors} collisionDetection={pointerWithin} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
|
324
|
-
<div className="flex gap-4">
|
|
325
|
-
<FieldPalette />
|
|
326
|
-
<BuilderCanvas
|
|
327
|
-
fields={fields}
|
|
328
|
-
selectedFieldId={selectedFieldId}
|
|
329
|
-
onSelectField={handleSelectField}
|
|
330
|
-
onRemoveField={handleRemoveField}
|
|
331
|
-
draggingFieldType={draggingFieldType}
|
|
332
|
-
/>
|
|
333
|
-
</div>
|
|
334
|
-
|
|
335
|
-
<DragOverlay dropAnimation={null} modifiers={[snapCenterToCursor]}>
|
|
336
|
-
{draggingFieldType ? (
|
|
337
|
-
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border bg-card shadow-xl text-sm font-medium opacity-90 pointer-events-none">
|
|
338
|
-
{(() => {
|
|
339
|
-
const def = FIELD_TYPES.find((ft) => ft.type === draggingFieldType)
|
|
340
|
-
if (!def) return null
|
|
341
|
-
const Icon = def.icon
|
|
342
|
-
return (
|
|
343
|
-
<>
|
|
344
|
-
<Icon className="h-4 w-4 text-primary" />
|
|
345
|
-
{t(`customForms.fieldType.${draggingFieldType}`)}
|
|
346
|
-
</>
|
|
347
|
-
)
|
|
348
|
-
})()}
|
|
349
|
-
</div>
|
|
350
|
-
) : null}
|
|
351
|
-
</DragOverlay>
|
|
352
|
-
</DndContext>
|
|
353
|
-
|
|
354
|
-
{/* Field config drawer (right side) */}
|
|
355
|
-
<FieldConfigDrawer
|
|
356
|
-
field={selectedField}
|
|
357
|
-
onSave={handleFieldConfigSave}
|
|
358
|
-
onUpdate={handleFieldUpdate}
|
|
359
|
-
onClose={() => setSelectedFieldId(null)}
|
|
360
|
-
/>
|
|
361
|
-
|
|
362
|
-
{/* Preview — A4 paper overlay (portaled to body) */}
|
|
363
|
-
{showPreview && createPortal(
|
|
364
|
-
<div className="fixed inset-0 z-[100] flex items-start justify-center bg-black/60 overflow-y-auto py-10 px-4" onClick={() => setShowPreview(false)}>
|
|
365
|
-
<div
|
|
366
|
-
className="relative w-full max-w-3xl bg-white rounded-modal shadow-lg"
|
|
367
|
-
style={{ minHeight: '80vh' }}
|
|
368
|
-
onClick={(e) => e.stopPropagation()}
|
|
369
|
-
>
|
|
370
|
-
{/* Toggle Editor ⟷ Impressão (centered) + close, on a sticky top bar. */}
|
|
371
|
-
<div className="sticky top-0 z-10 flex items-center justify-center border-b bg-white/95 backdrop-blur px-4 py-2.5 rounded-t-modal">
|
|
372
|
-
<div className="inline-flex rounded-lg border p-0.5 text-xs">
|
|
373
|
-
<button
|
|
374
|
-
onClick={() => setPreviewMode('editor')}
|
|
375
|
-
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-md font-medium transition-colors ${previewMode === 'editor' ? 'bg-neutral-900 text-white' : 'text-neutral-500 hover:text-neutral-800'}`}
|
|
376
|
-
>
|
|
377
|
-
<Pencil className="h-3.5 w-3.5" />
|
|
378
|
-
{t('customForms.preview.editor')}
|
|
379
|
-
</button>
|
|
380
|
-
<button
|
|
381
|
-
onClick={() => setPreviewMode('print')}
|
|
382
|
-
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-md font-medium transition-colors ${previewMode === 'print' ? 'bg-neutral-900 text-white' : 'text-neutral-500 hover:text-neutral-800'}`}
|
|
383
|
-
>
|
|
384
|
-
<Printer className="h-3.5 w-3.5" />
|
|
385
|
-
{t('customForms.preview.print')}
|
|
386
|
-
</button>
|
|
387
|
-
</div>
|
|
388
|
-
<button
|
|
389
|
-
onClick={() => setShowPreview(false)}
|
|
390
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 flex h-8 w-8 items-center justify-center rounded-full text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700 transition-colors"
|
|
391
|
-
>
|
|
392
|
-
<span className="text-lg">✕</span>
|
|
393
|
-
</button>
|
|
394
|
-
</div>
|
|
395
|
-
|
|
396
|
-
{previewMode === 'editor' ? (
|
|
397
|
-
/* Editor — fillable form (same renderer used on the person's ficha) */
|
|
398
|
-
<div className="px-10 py-8 sm:px-14 sm:py-10 text-neutral-900">
|
|
399
|
-
<h1 className="text-2xl font-bold mb-1">{name || t('customForms.newTemplate')}</h1>
|
|
400
|
-
{description && <p className="text-sm text-neutral-500 mb-8">{description}</p>}
|
|
401
|
-
{!description && <div className="mb-8" />}
|
|
402
|
-
|
|
403
|
-
<FormRenderer
|
|
404
|
-
schema={{ fields, layout: { columns: 12 } }}
|
|
405
|
-
data={previewData}
|
|
406
|
-
onChange={setPreviewData}
|
|
407
|
-
/>
|
|
408
|
-
</div>
|
|
409
|
-
) : (
|
|
410
|
-
/* Impressão — exact print/PDF output rendered in an isolated iframe */
|
|
411
|
-
<iframe
|
|
412
|
-
title="print-preview"
|
|
413
|
-
className="w-full rounded-b-modal"
|
|
414
|
-
style={{ height: '75vh', border: 'none' }}
|
|
415
|
-
srcDoc={buildPrintHtml(
|
|
416
|
-
{ id: 'preview', name: name || t('customForms.newTemplate'), schema: { fields, layout: { columns: 12 } } } as any,
|
|
417
|
-
{ title: name || t('customForms.newTemplate'), createdAt: new Date().toISOString(), status: 'draft', data: previewData } as any,
|
|
418
|
-
)}
|
|
419
|
-
/>
|
|
420
|
-
)}
|
|
421
|
-
</div>
|
|
422
|
-
</div>,
|
|
423
|
-
document.body,
|
|
424
|
-
)}
|
|
425
|
-
</div>
|
|
426
|
-
)
|
|
427
|
-
}
|