Files
jason ad499f6782
Build and Push Docker Image / build (push) Successful in 1m12s
Assemble QMS app + SQLite refactor + Unraid single-container deploy
Reconstruct the full app from init-source overlays (base + fix-1..6 +
update-1..3, last-wins) at the repo root, complete the missing pieces so it
builds and runs, and stage the Unraid deployment.

App completion:
- types/index.ts: former Prisma enums as string-literal unions + AppUser
- pages/_app.tsx + styles/globals.css (mount AppProvider/ToastProvider)
- API routes: auth/login, auth/me, users, submissions (+REVIEW_READY notify),
  forms (list/create), notifications
- scripts/create-admin.js: idempotent first-admin bootstrap
- 14 unbuilt nav targets stubbed via ComingSoon placeholder

SQLite refactor (single-container, no external DB):
- schema provider -> sqlite; enums -> String; Json -> String;
  FormField.options String[] -> JSON-encoded String
- lib/forms.ts (de)serialises options at the DB boundary
- drop mode:"insensitive" (unsupported on SQLite)
- enum imports repointed from @prisma/client to @/types

Deploy:
- multi-stage Dockerfile (next build -> prod runner), docker-entrypoint.sh
  (prisma db push -> create-admin -> next start), .dockerignore
- docker-compose.yml: br0 10.2.0.x, /mnt/user/appdata/qms -> /data volume
- README rewritten for the Unraid/Gitea Actions flow; .env scrubbed of the
  live Supabase credential; vercel.json removed

Verified: next build clean (41 routes); live SQLite round-trip of
login/session, form options array, and submission -> REVIEW_READY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:58:47 -05:00

97 lines
3.4 KiB
TypeScript

import React from 'react'
import { Input, Textarea } from '@/components/ui'
import { FieldType } from '@/types'
export interface FormFieldDef {
id: string
label: string
type: FieldType
hint?: string | null
options: string[]
required: boolean
}
export function FieldRenderer({ field, value, onChange }: {
field: FormFieldDef
value: any
onChange: (v: any) => void
}) {
switch (field.type) {
case 'SHORT_TEXT':
return <Input value={value || ''} onChange={e => onChange(e.target.value)} placeholder={field.hint || ''}/>
case 'LONG_TEXT':
return <Textarea value={value || ''} onChange={e => onChange(e.target.value)} placeholder={field.hint || ''}/>
case 'NUMBER':
return <Input type="number" value={value ?? ''} onChange={e => onChange(e.target.value)} style={{ maxWidth: '160px' }}/>
case 'DATE':
return <Input type="date" value={value || ''} onChange={e => onChange(e.target.value)} style={{ maxWidth: '180px' }}/>
case 'SINGLE_CHOICE':
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{field.options.map(opt => (
<label key={opt} style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', cursor: 'pointer' }}>
<input type="radio" name={field.id} checked={value === opt} onChange={() => onChange(opt)} style={{ accentColor: '#534AB7' }}/>
{opt}
</label>
))}
</div>
)
case 'MULTI_CHOICE':
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{field.options.map(opt => (
<label key={opt} style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', cursor: 'pointer' }}>
<input type="checkbox" checked={(value || []).includes(opt)}
onChange={e => {
const cur = value || []
onChange(e.target.checked ? [...cur, opt] : cur.filter((x: string) => x !== opt))
}} style={{ accentColor: '#534AB7' }}/>
{opt}
</label>
))}
</div>
)
case 'RATING':
return (
<div style={{ display: 'flex', gap: '4px' }}>
{[1, 2, 3, 4, 5].map(n => (
<span key={n} onClick={() => onChange(n)} style={{ fontSize: '24px', cursor: 'pointer', color: (value || 0) >= n ? '#EF9F27' : '#ddd' }}></span>
))}
</div>
)
case 'PHOTO':
return <Input type="file" accept="image/*" onChange={e => onChange(e.target.files?.[0]?.name || '')}/>
default:
return null
}
}
export function FormFieldList({ fields, values, onChange }: {
fields: FormFieldDef[]
values: Record<string, any>
onChange: (fieldId: string, value: any) => void
}) {
return (
<>
{fields.map(field => (
<div key={field.id} style={{ marginBottom: '14px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
{field.label}
{field.required && <span style={{ color: '#E24B4A', marginLeft: '2px' }}>*</span>}
</label>
{field.hint && <div style={{ fontSize: '10px', color: '#aaa', marginBottom: '4px' }}>{field.hint}</div>}
<FieldRenderer field={field} value={values[field.id]} onChange={v => onChange(field.id, v)}/>
</div>
))}
</>
)
}