Phase 3: scaffold Manager Dashboard — all pages, layout, routing
Includes: LoginPage (PIN pad), DashboardPage (30s polling table grid), OrderDetailPage (full actions), ProductsPage (CRUD + printer zone), WaitersPage (block/reset PIN/delete), TablesPage, ReportsPage (shift summary + order history + CSV export), SettingsPage (printers + test print + sysadmin lock/unlock). TailwindCSS, React Query, react-hot-toast. Docker Compose service on port 5174.
This commit is contained in:
113
manager_dashboard/src/pages/SettingsPage.jsx
Normal file
113
manager_dashboard/src/pages/SettingsPage.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
import client from '../api/client'
|
||||
import useAuthStore from '../store/authStore'
|
||||
|
||||
function formatUptime(seconds) {
|
||||
const h = Math.floor(seconds / 3600)
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
const s = seconds % 60
|
||||
return `${h}ω ${m}λ ${s}δ`
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const user = useAuthStore(s => s.user)
|
||||
const qc = useQueryClient()
|
||||
|
||||
const { data: status, isLoading } = useQuery({
|
||||
queryKey: ['system-status'],
|
||||
queryFn: () => client.get('/api/system/status').then(r => r.data),
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
const testPrint = useMutation({
|
||||
mutationFn: (id) => client.post(`/api/system/printers/test?printer_id=${id}`),
|
||||
onSuccess: (res) => {
|
||||
const d = res.data
|
||||
d.success ? toast.success('Test print στάλθηκε!') : toast.error(`Σφάλμα: ${d.error}`)
|
||||
},
|
||||
onError: () => toast.error('Σφάλμα επικοινωνίας'),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="flex items-center justify-center h-64 text-gray-400">Φόρτωση…</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<h1 className="text-xl font-bold text-gray-800">Ρυθμίσεις</h1>
|
||||
|
||||
{/* System info */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<h2 className="font-semibold text-gray-700">Σύστημα</h2>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="text-gray-500">Uptime</div>
|
||||
<div className="font-medium text-gray-800">{formatUptime(status?.uptime_seconds ?? 0)}</div>
|
||||
<div className="text-gray-500">Άδεια χρήσης</div>
|
||||
<div className={`font-medium ${status?.licensed ? 'text-green-700' : 'text-red-600'}`}>
|
||||
{status?.licensed ? 'Ενεργή' : 'Ανενεργή'}
|
||||
</div>
|
||||
<div className="text-gray-500">Κατάσταση</div>
|
||||
<div className={`font-medium ${status?.locked ? 'text-red-600' : 'text-green-700'}`}>
|
||||
{status?.locked ? 'Κλειδωμένο' : 'Λειτουργικό'}
|
||||
</div>
|
||||
{status?.expires_at && (
|
||||
<>
|
||||
<div className="text-gray-500">Λήξη άδειας</div>
|
||||
<div className="font-medium text-gray-800">{new Date(status.expires_at).toLocaleDateString('el-GR')}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Printers */}
|
||||
<div className="card divide-y divide-gray-100">
|
||||
<div className="px-5 py-4">
|
||||
<h2 className="font-semibold text-gray-700">Εκτυπωτές</h2>
|
||||
</div>
|
||||
|
||||
{(!status?.printers || status.printers.length === 0) && (
|
||||
<p className="px-5 py-6 text-center text-gray-400 text-sm">Δεν βρέθηκαν εκτυπωτές.</p>
|
||||
)}
|
||||
|
||||
{status?.printers?.map(p => (
|
||||
<div key={p.id} className="flex items-center gap-4 px-5 py-3">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-800">{p.name}</p>
|
||||
</div>
|
||||
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${p.reachable ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-600'}`}>
|
||||
{p.reachable ? 'Προσβάσιμος' : 'Μη προσβάσιμος'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => testPrint.mutate(p.id)}
|
||||
disabled={testPrint.isPending}
|
||||
className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9"
|
||||
>
|
||||
Test Print
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sysadmin-only section */}
|
||||
{user?.role === 'sysadmin' && (
|
||||
<div className="card p-5 space-y-3 border-amber-200 bg-amber-50">
|
||||
<h2 className="font-semibold text-amber-800">Sysadmin</h2>
|
||||
<p className="text-sm text-amber-700">Έλεγχος κλειδώματος συστήματος.</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => client.post('/api/system/unlock').then(() => { toast.success('Ξεκλειδώθηκε'); qc.invalidateQueries({ queryKey: ['system-status'] }) })}
|
||||
className="btn btn-primary text-sm"
|
||||
>
|
||||
Ξεκλείδωμα
|
||||
</button>
|
||||
<button
|
||||
onClick={() => client.post('/api/system/lock').then(() => { toast.success('Κλειδώθηκε'); qc.invalidateQueries({ queryKey: ['system-status'] }) })}
|
||||
className="btn btn-danger text-sm"
|
||||
>
|
||||
Κλείδωμα
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user