init-source

This commit is contained in:
2026-06-15 16:21:53 -05:00
parent a12a3fc72e
commit 631890c5bd
1010 changed files with 107132 additions and 0 deletions
@@ -0,0 +1,309 @@
import { useState, useEffect } from 'react'
import Layout from '@/components/layout/Layout'
import { Card, EmptyState, Btn, Modal, Field, Input, Select, Textarea, showToast, Tag, StatusDot, Table } from '@/components/ui'
import { FormFieldList, FormFieldDef } from '@/components/forms/FieldRenderer'
import { FieldType } from '@/types'
const FIELD_TYPES: { value: FieldType; label: string }[] = [
{ value: 'SHORT_TEXT', label: 'Short text' },
{ value: 'LONG_TEXT', label: 'Long text' },
{ value: 'NUMBER', label: 'Number' },
{ value: 'DATE', label: 'Date' },
{ value: 'SINGLE_CHOICE', label: 'Single choice' },
{ value: 'MULTI_CHOICE', label: 'Multi-choice' },
{ value: 'RATING', label: 'Rating (15)' },
{ value: 'PHOTO', label: 'Photo / file' },
]
const STATUS_COLOR: Record<string, string> = {
DRAFT: 'purple', ACTIVE: 'green', SUSPENDED: 'amber',
REVIEW_READY: 'blue', STANDARD_SET: 'teal', ARCHIVED: 'gray',
}
interface BuilderField extends FormFieldDef {
order: number
}
export default function FormBuilderPage() {
const [forms, setForms] = useState<any[]>([])
const [loading, setLoading] = useState(true)
// Builder modal
const [builderOpen, setBuilderOpen] = useState(false)
const [builderMode, setBuilderMode] = useState<'create' | 'edit'>('create')
const [editingId, setEditingId] = useState<string | null>(null)
const [saving, setSaving] = useState(false)
const [formMeta, setFormMeta] = useState({ name: '', product: '', description: '', minSubmissions: 10 })
const [fields, setFields] = useState<BuilderField[]>([])
const [newField, setNewField] = useState({ label: '', type: 'SHORT_TEXT' as FieldType, hint: '', required: false, options: '' })
// Preview modal
const [previewOpen, setPreviewOpen] = useState(false)
const [previewFields, setPreviewFields] = useState<FormFieldDef[]>([])
const [previewTitle, setPreviewTitle] = useState('')
const [previewSubtitle, setPreviewSubtitle] = useState('')
const [previewAnswers, setPreviewAnswers] = useState<Record<string, any>>({})
useEffect(() => { loadForms() }, [])
async function loadForms() {
setLoading(true)
const res = await fetch('/api/forms')
if (res.ok) {
const { data } = await res.json()
setForms(data || [])
}
setLoading(false)
}
// ── Builder open/close ──────────────────────────────────────────────────
function openCreate() {
setFormMeta({ name: '', product: '', description: '', minSubmissions: 10 })
setFields([])
setNewField({ label: '', type: 'SHORT_TEXT', hint: '', required: false, options: '' })
setEditingId(null)
setBuilderMode('create')
setBuilderOpen(true)
}
function openEdit(form: any) {
setFormMeta({
name: form.name,
product: form.product || '',
description: form.description || '',
minSubmissions: form.minSubmissions,
})
setFields((form.fields || []).map((f: any) => ({
id: f.id, label: f.label, type: f.type, hint: f.hint || '',
required: !!f.required, options: f.options || [], order: f.order ?? 0,
})))
setNewField({ label: '', type: 'SHORT_TEXT', hint: '', required: false, options: '' })
setEditingId(form.id)
setBuilderMode('edit')
setBuilderOpen(true)
}
// ── Field add/remove ─────────────────────────────────────────────────────
function addField() {
if (!newField.label) return
const opts = newField.options.split('\n').map(o => o.trim()).filter(Boolean)
setFields(f => [...f, { ...newField, options: opts, order: f.length, id: `new_${Date.now()}` }])
setNewField({ label: '', type: 'SHORT_TEXT', hint: '', required: false, options: '' })
}
function removeField(index: number) {
setFields(fs => fs.filter((_, i) => i !== index))
}
// ── Save (create or edit) ───────────────────────────────────────────────
async function saveForm() {
if (!formMeta.name) return
setSaving(true)
const url = builderMode === 'edit' ? `/api/forms/${editingId}` : '/api/forms'
const method = builderMode === 'edit' ? 'PATCH' : 'POST'
const res = await fetch(url, {
method, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formMeta, fields }),
})
setSaving(false)
if (res.ok) {
setBuilderOpen(false)
showToast(builderMode === 'edit' ? 'Form updated' : 'Form created — saved as draft')
loadForms()
} else {
showToast('Failed to save form', 'error')
}
}
// ── Status transitions ──────────────────────────────────────────────────
async function setStatus(id: string, status: string, successMsg: string) {
const res = await fetch(`/api/forms/${id}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
})
if (res.ok) { showToast(successMsg); loadForms() }
else showToast('Update failed', 'error')
}
// ── Clone ────────────────────────────────────────────────────────────────
async function cloneForm(id: string) {
const res = await fetch(`/api/forms/${id}/clone`, { method: 'POST' })
if (res.ok) {
const { data } = await res.json()
showToast(`Cloned as "${data.name}" — edit to customize`)
loadForms()
} else {
showToast('Clone failed', 'error')
}
}
// ── Preview ──────────────────────────────────────────────────────────────
function openPreview(fieldsToShow: FormFieldDef[], title: string, subtitle?: string) {
setPreviewFields(fieldsToShow)
setPreviewTitle(title || 'Untitled form')
setPreviewSubtitle(subtitle || '')
setPreviewAnswers({})
setPreviewOpen(true)
}
const needsOptions = ['SINGLE_CHOICE', 'MULTI_CHOICE'].includes(newField.type)
return (
<Layout title="Form builder">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div>
<h2 style={{ fontSize: '16px', fontWeight: '500', margin: 0 }}>First build form builder</h2>
<p style={{ fontSize: '11px', color: '#aaa', margin: '2px 0 0' }}>Admin only design, edit, suspend, clone, and archive data collection forms</p>
</div>
<Btn onClick={openCreate}>+ New form</Btn>
</div>
<Card>
{loading ? (
<div style={{ textAlign: 'center', padding: '32px', color: '#aaa', fontSize: '12px' }}>Loading</div>
) : forms.length === 0 ? (
<EmptyState
title="No forms yet"
message="Create your first data collection form. Production teams will fill it out on the shop floor to help establish quality standards."
action={{ label: '+ Build first form', onClick: openCreate }}
/>
) : (
<div style={{ overflowX: 'auto' }}>
<Table headers={['Form name', 'Product', 'Fields', 'Submissions', 'Target', 'Status', 'Actions']}>
{forms.map((f: any) => (
<tr key={f.id}>
<td style={{ padding: '8px 6px', borderBottom: '0.5px solid #f5f5f5' }}>
<div style={{ fontWeight: '500', fontSize: '12px' }}>{f.name}</div>
{f.clonedFromName && <div style={{ fontSize: '10px', color: '#aaa', marginTop: '1px' }}>Cloned from {f.clonedFromName}</div>}
</td>
<td style={{ padding: '8px 6px', borderBottom: '0.5px solid #f5f5f5', fontSize: '12px', color: '#888' }}>{f.product || '—'}</td>
<td style={{ padding: '8px 6px', borderBottom: '0.5px solid #f5f5f5', fontSize: '12px' }}>{f._count?.fields || 0}</td>
<td style={{ padding: '8px 6px', borderBottom: '0.5px solid #f5f5f5', fontSize: '12px', fontWeight: '500', color: f._count?.submissions >= f.minSubmissions ? '#1D9E75' : '#333' }}>{f._count?.submissions || 0}</td>
<td style={{ padding: '8px 6px', borderBottom: '0.5px solid #f5f5f5', fontSize: '12px', color: '#888' }}>{f.minSubmissions}</td>
<td style={{ padding: '8px 6px', borderBottom: '0.5px solid #f5f5f5', whiteSpace: 'nowrap' }}>
<StatusDot status={f.status}/>
<Tag color={STATUS_COLOR[f.status]}>{f.status.replace('_', ' ')}</Tag>
</td>
<td style={{ padding: '8px 6px', borderBottom: '0.5px solid #f5f5f5', minWidth: '220px' }}>
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
<Btn size="sm" variant="ghost" onClick={() => openPreview(f.fields || [], f.name, f.product)}>Preview</Btn>
<Btn size="sm" variant="ghost" onClick={() => openEdit(f)}>Edit</Btn>
<Btn size="sm" variant="ghost" onClick={() => cloneForm(f.id)}>Clone</Btn>
{f.status === 'DRAFT' && (
<Btn size="sm" onClick={() => setStatus(f.id, 'ACTIVE', 'Form published — production team can now fill it')}>Publish</Btn>
)}
{f.status === 'ACTIVE' && (
<Btn size="sm" variant="ghost" onClick={() => setStatus(f.id, 'SUSPENDED', 'Form suspended')}>Suspend</Btn>
)}
{f.status === 'SUSPENDED' && (
<Btn size="sm" onClick={() => setStatus(f.id, 'ACTIVE', 'Form reactivated')}>Reactivate</Btn>
)}
{f.status === 'ARCHIVED' ? (
<Btn size="sm" variant="ghost" onClick={() => setStatus(f.id, 'DRAFT', 'Form restored to draft')}>Restore</Btn>
) : (
<Btn size="sm" variant="ghost" onClick={() => setStatus(f.id, 'ARCHIVED', 'Form archived')}>Archive</Btn>
)}
</div>
</td>
</tr>
))}
</Table>
</div>
)}
</Card>
{/* Builder Modal (create / edit) */}
<Modal open={builderOpen} onClose={() => setBuilderOpen(false)} title={builderMode === 'edit' ? `Edit form — ${formMeta.name || ''}` : 'Build new form'} width={640}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<div style={{ fontSize: '11px', fontWeight: '500', color: '#aaa', textTransform: 'uppercase', letterSpacing: '0.6px', marginBottom: '10px' }}>Form details</div>
<Field label="Form name" required><Input value={formMeta.name} onChange={e => setFormMeta(m => ({ ...m, name: e.target.value }))} placeholder="First Build Data Sheet — Line 3"/></Field>
<Field label="Product / assembly"><Input value={formMeta.product} onChange={e => setFormMeta(m => ({ ...m, product: e.target.value }))} placeholder="Widget A — Rev 2"/></Field>
<Field label="Description"><Textarea value={formMeta.description} onChange={e => setFormMeta(m => ({ ...m, description: e.target.value }))} style={{ minHeight: '52px' }} placeholder="What data does this form collect?"/></Field>
<Field label="Min. submissions before QC review"><Input type="number" value={formMeta.minSubmissions} min={1} onChange={e => setFormMeta(m => ({ ...m, minSubmissions: parseInt(e.target.value) || 1 }))}/></Field>
<div style={{ fontSize: '11px', fontWeight: '500', color: '#aaa', textTransform: 'uppercase', letterSpacing: '0.6px', marginBottom: '10px', marginTop: '6px' }}>Add field</div>
<Field label="Field label" required><Input value={newField.label} onChange={e => setNewField(f => ({ ...f, label: e.target.value }))} placeholder="e.g. Torque reading (Nm)"/></Field>
<Field label="Field type">
<Select value={newField.type} onChange={e => setNewField(f => ({ ...f, type: e.target.value as FieldType }))}>
{FIELD_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
</Field>
{needsOptions && (
<Field label="Options (one per line)">
<Textarea value={newField.options} onChange={e => setNewField(f => ({ ...f, options: e.target.value }))} style={{ minHeight: '60px' }} placeholder={'Pass\nFail\nRework'}/>
</Field>
)}
<Field label="Helper hint"><Input value={newField.hint} onChange={e => setNewField(f => ({ ...f, hint: e.target.value }))} placeholder="Instruction shown to filler"/></Field>
<label style={{ display: 'flex', alignItems: 'center', gap: '7px', fontSize: '12px', cursor: 'pointer', marginBottom: '10px' }}>
<input type="checkbox" checked={newField.required} onChange={e => setNewField(f => ({ ...f, required: e.target.checked }))} style={{ accentColor: '#534AB7' }}/>
Required field
</label>
<Btn variant="ghost" onClick={addField} style={{ width: '100%', justifyContent: 'center' }}>+ Add field</Btn>
</div>
<div>
<div style={{ fontSize: '11px', fontWeight: '500', color: '#aaa', textTransform: 'uppercase', letterSpacing: '0.6px', marginBottom: '10px' }}>Fields ({fields.length})</div>
{fields.length === 0 ? (
<div style={{ border: '0.5px dashed #ddd', borderRadius: '8px', padding: '24px', textAlign: 'center', color: '#bbb', fontSize: '11px' }}>
Add fields from the left
</div>
) : fields.map((f, i) => (
<div key={f.id} style={{
display: 'flex', alignItems: 'center', gap: '8px',
padding: '8px 10px', border: `0.5px solid ${f.required ? '#AFA9EC' : '#eee'}`,
borderRadius: '8px', marginBottom: '5px', background: f.required ? '#FAFAFE' : 'white'
}}>
<span style={{ fontSize: '10px', color: '#bbb', width: '16px', textAlign: 'center' }}>{i + 1}</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', fontWeight: '500' }}>{f.label}{f.required && <span style={{ color: '#E24B4A', marginLeft: '2px' }}>*</span>}</div>
<div style={{ fontSize: '10px', color: '#aaa' }}>{FIELD_TYPES.find(t => t.value === f.type)?.label}</div>
</div>
<button onClick={() => removeField(i)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#ddd', fontSize: '16px' }} aria-label="Remove field">×</button>
</div>
))}
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '16px', paddingTop: '14px', borderTop: '0.5px solid #eee' }}>
<Btn
variant="ghost"
onClick={() => openPreview(fields, formMeta.name || 'Untitled form', formMeta.product)}
disabled={fields.length === 0}
>
Preview form
</Btn>
<div style={{ display: 'flex', gap: '8px' }}>
<Btn variant="ghost" onClick={() => setBuilderOpen(false)}>Cancel</Btn>
<Btn onClick={saveForm} disabled={saving || !formMeta.name}>
{saving ? 'Saving…' : builderMode === 'edit' ? 'Save changes' : 'Create form (saved as draft)'}
</Btn>
</div>
</div>
</Modal>
{/* Preview Modal */}
<Modal open={previewOpen} onClose={() => setPreviewOpen(false)} title={`Preview — ${previewTitle}`} width={480}>
<div style={{ background: '#EEEDFE', borderRadius: '8px', padding: '9px 12px', fontSize: '11px', color: '#3C3489', marginBottom: '14px' }}>
Preview mode interact freely. Nothing here is saved. This is exactly what the production team will see.
</div>
{previewSubtitle && <div style={{ fontSize: '11px', color: '#aaa', marginBottom: '12px' }}>{previewSubtitle}</div>}
{previewFields.length === 0 ? (
<EmptyState title="No fields yet" message="Add fields in the builder, then preview here."/>
) : (
<FormFieldList
fields={previewFields}
values={previewAnswers}
onChange={(fieldId, value) => setPreviewAnswers(a => ({ ...a, [fieldId]: value }))}
/>
)}
<div style={{ display: 'flex', gap: '8px', marginTop: '12px', paddingTop: '12px', borderTop: '0.5px solid #eee' }}>
<Btn variant="ghost" onClick={() => setPreviewAnswers({})}>Reset answers</Btn>
<Btn variant="ghost" onClick={() => setPreviewOpen(false)} style={{ marginLeft: 'auto' }}>Close preview</Btn>
</div>
</Modal>
</Layout>
)
}
@@ -0,0 +1,112 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'
import { requireAuth, logAction } from '@/lib/auth'
import { FormStatus } from '@prisma/client'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const user = await requireAuth(req, res)
if (!user) return
const { id } = req.query as { id: string }
if (req.method === 'GET') {
const form = await prisma.buildForm.findUnique({
where: { id },
include: { fields: { orderBy: { order: 'asc' } }, _count: { select: { submissions: true, fields: true } } },
})
if (!form) return res.status(404).json({ error: 'Not found' })
return res.json({ data: form })
}
if (req.method === 'PATCH') {
if (user.role !== 'ADMIN') return res.status(403).json({ error: 'Admin only' })
const before = await prisma.buildForm.findUnique({ where: { id }, include: { fields: true } })
if (!before) return res.status(404).json({ error: 'Not found' })
const { fields, status, ...rest } = req.body as {
fields?: Array<{ id?: string; label: string; type: string; hint?: string; options?: string[]; required?: boolean; trackStd?: boolean }>
status?: FormStatus
name?: string; product?: string; description?: string; minSubmissions?: number; publishedAt?: string
}
// ── Full field-level edit (name/product/description/minSubmissions + fields array) ──
if (fields !== undefined) {
const existingIds = new Set(before.fields.map(f => f.id))
const keepIds = new Set(fields.filter(f => f.id && existingIds.has(f.id)).map(f => f.id as string))
const toDeleteIds = before.fields.filter(f => !keepIds.has(f.id)).map(f => f.id)
const ops: any[] = []
if (toDeleteIds.length > 0) {
ops.push(prisma.formField.deleteMany({ where: { id: { in: toDeleteIds } } }))
}
fields.forEach((f, i) => {
const data = {
label: f.label,
type: f.type as any,
hint: f.hint || null,
options: f.options || [],
required: !!f.required,
trackStd: f.trackStd !== false,
order: i,
}
if (f.id && existingIds.has(f.id)) {
ops.push(prisma.formField.update({ where: { id: f.id }, data }))
} else {
ops.push(prisma.formField.create({ data: { ...data, formId: id } }))
}
})
const formUpdate: any = {}
if (rest.name !== undefined) formUpdate.name = rest.name
if (rest.product !== undefined) formUpdate.product = rest.product
if (rest.description !== undefined) formUpdate.description = rest.description
if (rest.minSubmissions !== undefined) formUpdate.minSubmissions = rest.minSubmissions
if (Object.keys(formUpdate).length > 0) {
ops.push(prisma.buildForm.update({ where: { id }, data: formUpdate }))
}
await prisma.$transaction(ops)
const updated = await prisma.buildForm.findUnique({
where: { id },
include: { fields: { orderBy: { order: 'asc' } }, _count: { select: { submissions: true, fields: true } } },
})
await logAction(user.id, 'UPDATE', 'BuildForm', id, before, updated)
return res.json({ data: updated })
}
// ── Status transitions (publish / suspend / reactivate / archive / restore) ──
const updateData: any = { ...rest }
if (status !== undefined) {
updateData.status = status
if (status === 'ACTIVE' && before.status === 'DRAFT') updateData.publishedAt = new Date()
if (status === 'ACTIVE' && before.status === 'SUSPENDED') updateData.suspendedAt = null
if (status === 'SUSPENDED') updateData.suspendedAt = new Date()
if (status === 'ARCHIVED') updateData.archivedAt = new Date()
if (status === 'DRAFT' && before.status === 'ARCHIVED') updateData.archivedAt = null
}
const updated = await prisma.buildForm.update({
where: { id },
data: updateData,
include: { fields: { orderBy: { order: 'asc' } }, _count: { select: { submissions: true, fields: true } } },
})
await logAction(user.id, 'UPDATE', 'BuildForm', id, { status: before.status }, { status: updated.status })
return res.json({ data: updated })
}
if (req.method === 'DELETE') {
if (user.role !== 'ADMIN') return res.status(403).json({ error: 'Admin only' })
await prisma.buildForm.delete({ where: { id } })
await logAction(user.id, 'DELETE', 'BuildForm', id, null, null)
return res.json({ ok: true })
}
res.status(405).end()
}
@@ -0,0 +1,49 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'
import { requireAuth, logAction } from '@/lib/auth'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const user = await requireAuth(req, res, ['ADMIN'])
if (!user) return
if (req.method !== 'POST') return res.status(405).end()
const { id } = req.query as { id: string }
const source = await prisma.buildForm.findUnique({
where: { id },
include: { fields: { orderBy: { order: 'asc' } } },
})
if (!source) return res.status(404).json({ error: 'Form not found' })
const clone = await prisma.buildForm.create({
data: {
name: `${source.name} (Copy)`,
product: source.product,
description: source.description,
minSubmissions: source.minSubmissions,
status: 'DRAFT',
createdById: user.id,
clonedFromId: source.id,
clonedFromName: source.name,
fields: {
create: source.fields.map(f => ({
label: f.label,
type: f.type,
hint: f.hint,
options: f.options,
required: f.required,
trackStd: f.trackStd,
order: f.order,
})),
},
},
include: {
fields: { orderBy: { order: 'asc' } },
_count: { select: { submissions: true, fields: true } },
},
})
await logAction(user.id, 'CREATE', 'BuildForm', clone.id, null, { clonedFrom: source.id, name: clone.name })
return res.status(201).json({ data: clone })
}
@@ -0,0 +1,148 @@
import { useState, useEffect } from 'react'
import Layout from '@/components/layout/Layout'
import { Card, EmptyState, Btn, Tag, showToast } from '@/components/ui'
import { FormFieldList } from '@/components/forms/FieldRenderer'
import { useApp } from '@/lib/context'
export default function FillPage() {
const { user } = useApp()
const [forms, setForms] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [selected, setSelected] = useState<any>(null)
const [answers, setAnswers] = useState<Record<string, any>>({})
const [submitting, setSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
useEffect(() => { loadForms() }, [])
async function loadForms() {
setLoading(true)
const res = await fetch('/api/forms?status=ACTIVE')
if (res.ok) {
const { data } = await res.json()
setForms(data || [])
}
setLoading(false)
}
function selectForm(form: any) {
setSelected(form)
setAnswers({})
setSubmitted(false)
}
async function submitForm() {
const required = selected.fields.filter((f: any) => f.required)
for (const f of required) {
if (!answers[f.id]) {
showToast(`"${f.label}" is required`, 'error'); return
}
}
setSubmitting(true)
const res = await fetch('/api/submissions', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ formId: selected.id, data: answers }),
})
setSubmitting(false)
if (res.ok) {
const { submissionCount } = await res.json()
setSubmitted(true)
showToast(`Submitted — you are contributor #${submissionCount}`)
} else {
showToast('Submission failed', 'error')
}
}
function setAnswer(fieldId: string, value: any) {
setAnswers(a => ({ ...a, [fieldId]: value }))
}
return (
<Layout title="My forms">
{!selected ? (
<>
<div style={{ marginBottom: '16px' }}>
<h2 style={{ fontSize: '16px', fontWeight: '500', margin: 0 }}>First build forms</h2>
<p style={{ fontSize: '11px', color: '#aaa', margin: '2px 0 0' }}>Select a form to fill out your data helps set quality standards</p>
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#aaa', fontSize: '12px' }}>Loading</div>
) : forms.length === 0 ? (
<EmptyState
title="No active forms"
message="Your admin hasn't published any data collection forms yet. Check back soon."
/>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '12px' }}>
{forms.map((form: any) => (
<div key={form.id} onClick={() => selectForm(form)} style={{
background: 'white', border: '0.5px solid #eee', borderRadius: '12px',
padding: '16px', cursor: 'pointer', transition: 'border-color 0.1s'
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '8px' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', background: '#EEEDFE', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#534AB7" strokeWidth="2">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
</div>
<Tag color="green">Active</Tag>
</div>
<div style={{ fontSize: '13px', fontWeight: '500', marginBottom: '4px' }}>{form.name}</div>
{form.product && <div style={{ fontSize: '11px', color: '#aaa', marginBottom: '8px' }}>{form.product}</div>}
<div style={{ fontSize: '11px', color: '#888' }}>{form.fields?.length || 0} fields · {form._count?.submissions || 0} submissions so far</div>
<div style={{ marginTop: '12px' }}>
<Btn size="sm" style={{ width: '100%', justifyContent: 'center' }}>Fill out </Btn>
</div>
</div>
))}
</div>
)}
</>
) : submitted ? (
<Card style={{ maxWidth: '480px', margin: '40px auto', textAlign: 'center', padding: '32px' }}>
<div style={{ width: '48px', height: '48px', borderRadius: '50%', background: '#EAF3DE', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#1D9E75" strokeWidth="2.5"><path d="M5 13l4 4L19 7"/></svg>
</div>
<div style={{ fontSize: '16px', fontWeight: '500', marginBottom: '6px' }}>Submitted</div>
<div style={{ fontSize: '12px', color: '#888', marginBottom: '20px' }}>Your data has been recorded and will contribute to setting quality standards for {selected.product || selected.name}.</div>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
<Btn variant="ghost" onClick={() => { setSubmitted(false); setAnswers({}) }}>Fill again</Btn>
<Btn onClick={() => setSelected(null)}>Back to forms</Btn>
</div>
</Card>
) : (
<div style={{ maxWidth: '560px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '20px' }}>
<button onClick={() => setSelected(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#aaa', padding: '4px', display: 'flex' }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 12H5m7-7l-7 7 7 7"/></svg>
</button>
<div>
<h2 style={{ fontSize: '15px', fontWeight: '500', margin: 0 }}>{selected.name}</h2>
{selected.product && <div style={{ fontSize: '11px', color: '#aaa' }}>{selected.product}</div>}
</div>
</div>
<Card>
<div style={{ background: '#EEEDFE', borderRadius: '8px', padding: '9px 12px', fontSize: '11px', color: '#3C3489', marginBottom: '16px' }}>
Fill in each field from your direct observation. Your submission is anonymous to QC it's just your data point.
</div>
<FormFieldList
fields={(selected.fields || []).slice().sort((a: any, b: any) => a.order - b.order)}
values={answers}
onChange={setAnswer}
/>
<div style={{ display: 'flex', gap: '8px', marginTop: '20px', paddingTop: '16px', borderTop: '0.5px solid #eee' }}>
<Btn variant="ghost" onClick={() => { setAnswers({}); }}>Clear</Btn>
<Btn onClick={submitForm} disabled={submitting} style={{ flex: 1, justifyContent: 'center' }}>
{submitting ? 'Submitting' : 'Submit build data'}
</Btn>
</div>
</Card>
</div>
)}
</Layout>
)
}