Assemble QMS app + SQLite refactor + Unraid single-container deploy
Build and Push Docker Image / build (push) Successful in 1m12s
Build and Push Docker Image / build (push) Successful in 1m12s
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>
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
import { useState, FormEvent } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useApp } from '@/lib/context'
|
||||
import Head from 'next/head'
|
||||
|
||||
export default function LoginPage() {
|
||||
const { login, user, loading } = useApp()
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
if (!loading && user) {
|
||||
const dest = { ADMIN: '/admin', QC: '/qc', PRODUCTION: '/fill', PRODUCTION_LEAD: '/fill', LOGISTICS_LEAD: '/qc/shipments', MANAGEMENT: '/management' }
|
||||
router.replace(dest[user.role] || '/qc')
|
||||
return null
|
||||
}
|
||||
|
||||
const submit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setBusy(true)
|
||||
setError('')
|
||||
const err = await login(email, password)
|
||||
if (err) { setError(err); setBusy(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head><title>Sign in — V11 QMS</title></Head>
|
||||
<div style={{
|
||||
minHeight: '100vh', background: '#F8F7FD',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px'
|
||||
}}>
|
||||
<div style={{ width: '100%', maxWidth: '380px' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
||||
<div style={{
|
||||
width: '44px', height: '44px', borderRadius: '12px',
|
||||
background: '#534AB7', display: 'inline-flex', alignItems: 'center',
|
||||
justifyContent: 'center', marginBottom: '12px'
|
||||
}}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2">
|
||||
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 style={{ fontSize: '20px', fontWeight: '600', color: '#1a1a1a', margin: '0 0 4px' }}>
|
||||
V11 Enterprise QMS
|
||||
</h1>
|
||||
<p style={{ fontSize: '13px', color: '#888', margin: 0 }}>Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} style={{
|
||||
background: 'white', borderRadius: '12px',
|
||||
border: '0.5px solid #e8e8e8', padding: '24px'
|
||||
}}>
|
||||
{error && (
|
||||
<div style={{
|
||||
background: '#FCEBEB', color: '#791F1F', border: '0.5px solid #F09595',
|
||||
borderRadius: '8px', padding: '9px 12px', fontSize: '12px', marginBottom: '16px'
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '14px' }}>
|
||||
<label style={{ display: 'block', fontSize: '11px', color: '#666', marginBottom: '4px', fontWeight: '500' }}>
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
type="email" required value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
style={{
|
||||
width: '100%', padding: '9px 11px', fontSize: '13px',
|
||||
border: '0.5px solid #ddd', borderRadius: '8px',
|
||||
outline: 'none', fontFamily: 'inherit', background: 'transparent'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', fontSize: '11px', color: '#666', marginBottom: '4px', fontWeight: '500' }}>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password" required value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
style={{
|
||||
width: '100%', padding: '9px 11px', fontSize: '13px',
|
||||
border: '0.5px solid #ddd', borderRadius: '8px',
|
||||
outline: 'none', fontFamily: 'inherit', background: 'transparent'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit" disabled={busy}
|
||||
style={{
|
||||
width: '100%', padding: '10px', fontSize: '13px', fontWeight: '500',
|
||||
background: busy ? '#AFA9EC' : '#534AB7', color: 'white',
|
||||
border: 'none', borderRadius: '8px', cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontFamily: 'inherit', transition: 'background 0.1s'
|
||||
}}
|
||||
>
|
||||
{busy ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style={{ textAlign: 'center', fontSize: '11px', color: '#aaa', marginTop: '16px' }}>
|
||||
No account? Ask your system administrator.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user