Files
xenia-pos-local/manager_dashboard/src/pages/Management/PrepZonesConfigPage.jsx
T
bonaminandClaude Sonnet 5 34ae328b0d feat: bump client-services (accumulated feature work + deploy fixes)
Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:00:14 +03:00

811 lines
38 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import {
Plus, Pencil, Trash2, Printer, X, Check, ChevronDown,
Zap, Settings2, ArrowUpDown, Layers, CheckSquare, SkipForward,
GripVertical, ChevronUp, AlertTriangle, FlameKindling,
} from 'lucide-react'
import client from '../../api/client'
// ── API ───────────────────────────────────────────────────────────────────────
const fetchZones = () => client.get('/api/prep-zones').then(r => r.data)
const fetchPrinters = () => client.get('/api/system/printers').then(r => r.data)
const fetchCategories = () => client.get('/api/products/categories').then(r => r.data)
// ── Constants ─────────────────────────────────────────────────────────────────
const AUTO_PRINT_OPTIONS = [
{ value: 'none', label: 'Χωρίς εκτύπωση', desc: 'Μη αυτόματη εκτύπωση' },
{ value: 'master', label: 'Μόνο κύριος', desc: 'Εκτύπωση στον κύριο' },
{ value: 'all', label: 'Όλοι εκτυπωτές', desc: 'Master + δευτερεύοντες' },
]
const SORT_OPTIONS = [
{ value: 'order_time', label: 'Σειρά παραγγελίας', desc: 'Όπως προστέθηκαν στο καλάθι' },
{ value: 'item_count', label: 'Πλήθος (φθίνον)', desc: 'Πρώτα τα αντικείμενα με τη μεγαλύτερη ποσότητα' },
{ value: 'alpha', label: 'Αλφαβητικά', desc: 'Ταξινόμηση κατά όνομα' },
]
const EMPTY_ZONE = {
name: '', description: '', notification_name: '',
printer_ids: [], master_printer_id: null,
auto_print: 'none', master_copies: 1, secondary_copies: 1,
sort_items_by: 'order_time', group_by_category: false, category_order: [], print_checkboxes: false,
bypass_pending: false, bypass_kds: false, auto_ready_to_served: false,
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function Toggle({ value, onChange, disabled }) {
return (
<button
type="button"
onClick={() => !disabled && onChange(!value)}
disabled={disabled}
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors duration-200 focus:outline-none
${value ? 'bg-primary-600' : 'bg-gray-200'} ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}`}
>
<span className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform duration-200
${value ? 'translate-x-4' : 'translate-x-0.5'}`} />
</button>
)
}
function CopySpinner({ value, onChange, min = 1, max = 9 }) {
return (
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => onChange(Math.max(min, value - 1))}
className="w-8 h-8 rounded-lg border border-gray-300 flex items-center justify-center text-gray-600 hover:bg-gray-100 hover:border-gray-400 transition-colors text-base font-medium"
>−</button>
<span className="w-9 text-center text-sm font-bold text-gray-800 tabular-nums">{value}×</span>
<button
type="button"
onClick={() => onChange(Math.min(max, value + 1))}
className="w-8 h-8 rounded-lg border border-gray-300 flex items-center justify-center text-gray-600 hover:bg-gray-100 hover:border-gray-400 transition-colors text-base font-medium"
>+</button>
</div>
)
}
// ── Click-outside hook for dropdowns ─────────────────────────────────────────
function useClickOutside(ref, onClose) {
useEffect(() => {
function handle(e) {
if (ref.current && !ref.current.contains(e.target)) onClose()
}
document.addEventListener('mousedown', handle)
return () => document.removeEventListener('mousedown', handle)
}, [ref, onClose])
}
// ── Master printer single-select ──────────────────────────────────────────────
function MasterPrinterPicker({ printers, value, onChange }) {
const [open, setOpen] = useState(false)
const ref = useRef(null)
useClickOutside(ref, () => setOpen(false))
const selected = printers.find(p => p.id === value)
return (
<div className="relative" ref={ref}>
<button
type="button"
onClick={() => setOpen(v => !v)}
className="w-full flex items-center justify-between gap-2 px-3 py-2 border border-gray-300 rounded-lg bg-white text-sm text-left hover:border-gray-400 transition-colors"
>
<span className="flex items-center gap-2 flex-1 min-w-0">
{selected ? (
<>
<span className="w-2 h-2 rounded-full bg-green-500 shrink-0" />
<span className="font-medium text-gray-800 truncate">{selected.name}</span>
<span className="text-xs text-gray-400 font-mono ml-auto shrink-0">{selected.ip_address}:{selected.port}</span>
</>
) : (
<span className="text-gray-400">— Χωρίς κύριο —</span>
)}
</span>
<ChevronDown size={13} className={`text-gray-400 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div className="absolute z-50 top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-xl shadow-xl overflow-hidden">
<button
type="button"
onClick={() => { onChange(null); setOpen(false) }}
className={`w-full flex items-center gap-3 px-3 py-2.5 text-sm text-left transition-colors hover:bg-gray-50 ${!value ? 'bg-gray-50' : ''}`}
>
<span className="w-4 h-4 rounded border border-gray-300 flex items-center justify-center shrink-0">
{!value && <Check size={10} className="text-gray-600" />}
</span>
<span className="text-gray-400 italic text-xs">Χωρίς κύριο εκτυπωτή</span>
</button>
{printers.map(p => (
<button
key={p.id}
type="button"
onClick={() => { onChange(p.id); setOpen(false) }}
className={`w-full flex items-center gap-3 px-3 py-2.5 text-sm text-left transition-colors hover:bg-gray-50 ${value === p.id ? 'bg-primary-50' : ''}`}
>
<span className={`w-4 h-4 rounded border flex items-center justify-center shrink-0 ${value === p.id ? 'bg-primary-600 border-primary-600' : 'border-gray-300'}`}>
{value === p.id && <Check size={10} className="text-white" />}
</span>
<Printer size={13} className="text-gray-400 shrink-0" />
<span className={`flex-1 ${value === p.id ? 'font-semibold text-primary-700' : 'text-gray-700'}`}>{p.name}</span>
<span className="text-xs text-gray-400 font-mono">{p.ip_address}:{p.port}</span>
</button>
))}
</div>
)}
</div>
)
}
// ── Secondary printers multi-select ──────────────────────────────────────────
function SecondaryPrinterPicker({ printers, selected, masterId, onChange }) {
const [open, setOpen] = useState(false)
const ref = useRef(null)
useClickOutside(ref, () => setOpen(false))
const selectedSet = new Set(selected)
const eligible = printers.filter(p => p.id !== masterId)
const selectedPrinters = eligible.filter(p => selectedSet.has(p.id))
const toggle = (id) => {
const next = new Set(selectedSet)
if (next.has(id)) next.delete(id); else next.add(id)
onChange([...next])
}
return (
<div className="relative" ref={ref}>
<button
type="button"
onClick={() => setOpen(v => !v)}
className="w-full flex items-center justify-between gap-2 px-3 py-2 border border-gray-300 rounded-lg bg-white text-sm text-left hover:border-gray-400 transition-colors min-h-[38px]"
>
<span className="flex flex-wrap gap-1 flex-1 min-w-0">
{selectedPrinters.length === 0 ? (
<span className="text-gray-400">— Χωρίς δευτερεύοντες —</span>
) : (
selectedPrinters.map(p => (
<span key={p.id} className="inline-flex items-center gap-1 bg-gray-100 text-gray-600 rounded px-1.5 py-0.5 text-xs font-medium">
<Printer size={10} />{p.name}
</span>
))
)}
</span>
<ChevronDown size={13} className={`text-gray-400 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div className="absolute z-50 top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-xl shadow-xl overflow-hidden">
{eligible.length === 0 ? (
<div className="px-3 py-4 text-sm text-gray-400 text-center italic">
{printers.length <= 1 ? 'Δεν υπάρχουν άλλοι εκτυπωτές' : 'Ο μόνος εκτυπωτής είναι ο κύριος'}
</div>
) : eligible.map(p => {
const on = selectedSet.has(p.id)
return (
<button
key={p.id}
type="button"
onClick={() => toggle(p.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 text-sm text-left transition-colors hover:bg-gray-50 ${on ? 'bg-gray-50' : ''}`}
>
<span className={`w-4 h-4 rounded border flex items-center justify-center shrink-0 ${on ? 'bg-gray-700 border-gray-700' : 'border-gray-300'}`}>
{on && <Check size={10} className="text-white" />}
</span>
<Printer size={13} className="text-gray-400 shrink-0" />
<span className={`flex-1 ${on ? 'font-semibold text-gray-800' : 'text-gray-700'}`}>{p.name}</span>
<span className="text-xs text-gray-400 font-mono">{p.ip_address}:{p.port}</span>
</button>
)
})}
<div className="border-t border-gray-100 px-3 py-2 text-xs text-gray-400 italic">
Κλικ έξω για κλείσιμο
</div>
</div>
)}
</div>
)
}
// ── Category order modal ──────────────────────────────────────────────────────
function CategoryOrderModal({ categories, order, onSave, onClose }) {
const ranked = order.map(id => categories.find(c => c.id === id)).filter(Boolean)
const unranked = categories.filter(c => !order.includes(c.id))
const [list, setList] = useState([...ranked, ...unranked])
const dragIdx = useRef(null)
const onDragStart = (i) => { dragIdx.current = i }
const onDragOver = (e, i) => {
e.preventDefault()
if (dragIdx.current === null || dragIdx.current === i) return
const next = [...list]
const [moved] = next.splice(dragIdx.current, 1)
next.splice(i, 0, moved)
dragIdx.current = i
setList(next)
}
const onDragEnd = () => { dragIdx.current = null }
const move = (i, dir) => {
const j = i + dir
if (j < 0 || j >= list.length) return
const next = [...list];
[next[i], next[j]] = [next[j], next[i]]
setList(next)
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
<div>
<h2 className="text-sm font-bold text-gray-900">Σειρά Κατηγοριών Εκτύπωσης</h2>
<p className="text-xs text-gray-500 mt-0.5">Σύρετε για αλλαγή σειράς</p>
</div>
<button type="button" onClick={onClose} className="p-1.5 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors">
<X size={16} />
</button>
</div>
<div className="px-3 py-3 max-h-80 overflow-y-auto space-y-1">
{list.map((cat, i) => (
<div
key={cat.id}
draggable
onDragStart={() => onDragStart(i)}
onDragOver={e => onDragOver(e, i)}
onDragEnd={onDragEnd}
className="flex items-center gap-2.5 px-3 py-2.5 bg-gray-50 rounded-xl border border-gray-100 cursor-grab active:cursor-grabbing select-none group hover:bg-white hover:border-gray-200 hover:shadow-sm transition-all"
>
<GripVertical size={14} className="text-gray-300 group-hover:text-gray-400 shrink-0" />
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: cat.color || '#94a3b8' }} />
<span className="text-sm text-gray-700 font-medium flex-1 min-w-0 truncate">{cat.name}</span>
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button type="button" onClick={() => move(i, -1)} disabled={i === 0}
className="p-1 rounded text-gray-400 hover:text-gray-600 disabled:opacity-20">
<ChevronUp size={13} />
</button>
<button type="button" onClick={() => move(i, 1)} disabled={i === list.length - 1}
className="p-1 rounded text-gray-400 hover:text-gray-600 disabled:opacity-20">
<ChevronDown size={13} />
</button>
</div>
<span className="text-xs text-gray-300 font-mono w-5 text-right shrink-0">{i + 1}</span>
</div>
))}
{list.length === 0 && (
<div className="py-8 text-center text-sm text-gray-400">Δεν υπάρχουν κατηγορίες</div>
)}
</div>
<div className="px-5 py-4 border-t border-gray-100 flex gap-2 justify-end">
<button type="button" onClick={onClose} className="btn btn-secondary">Ακύρωση</button>
<button
type="button"
onClick={() => { onSave(list.map(c => c.id)); onClose() }}
className="btn btn-primary flex items-center gap-2"
>
<Check size={14} /> Αποθήκευση σειράς
</button>
</div>
</div>
</div>
)
}
// ── Section header ────────────────────────────────────────────────────────────
function Section({ icon: Icon_, label, children }) {
return (
<div className="space-y-3">
<div className="flex items-center gap-2 pb-1.5 border-b border-gray-100">
<Icon_ size={13} className="text-gray-400" />
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-widest">{label}</span>
</div>
{children}
</div>
)
}
// ── Toggle row ────────────────────────────────────────────────────────────────
function ToggleRow({ label, desc, value, onChange, disabled, children, warning }) {
return (
<div className={`rounded-xl border transition-colors ${value ? 'border-primary-200 bg-primary-50/40' : 'border-gray-200 bg-gray-50/40'} ${disabled ? 'opacity-60' : ''}`}>
<div className="flex items-center gap-3 px-4 py-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="text-sm font-semibold text-gray-800">{label}</span>
{warning && <AlertTriangle size={12} className="text-amber-500 shrink-0" />}
</div>
{desc && <p className="text-xs text-gray-500 mt-0.5 leading-relaxed">{desc}</p>}
</div>
<Toggle value={value} onChange={onChange} disabled={disabled} />
</div>
{value && children && (
<div className="px-4 pb-3">{children}</div>
)}
</div>
)
}
// ── Zone Form ─────────────────────────────────────────────────────────────────
function ZoneForm({ initial, printers, categories, onSave, onCancel, saving }) {
const z = initial || EMPTY_ZONE
const [name, setName] = useState(z.name)
const [description, setDesc] = useState(z.description || '')
const [notifName, setNotifName] = useState(z.notification_name || '')
const [allPrinterIds, setAllPrinterIds] = useState(z.printer_ids || [])
const [masterId, setMasterId] = useState(z.master_printer_id ?? null)
const [autoPrint, setAutoPrint] = useState(z.auto_print || 'none')
const [masterCopies, setMasterCopies] = useState(z.master_copies || 1)
const [secondaryCopies, setSecondaryCopies] = useState(z.secondary_copies || 1)
const [sortBy, setSortBy] = useState(z.sort_items_by || 'order_time')
const [groupCat, setGroupCat] = useState(z.group_by_category || false)
const [catOrder, setCatOrder] = useState(z.category_order || [])
const [checkboxes, setCheckboxes] = useState(z.print_checkboxes || false)
const [bypassPending, setBypassPending] = useState(z.bypass_pending || false)
const [bypassKds, setBypassKds] = useState(z.bypass_kds || false)
const [autoReadyServed, setAutoReadyServed] = useState(z.auto_ready_to_served || false)
const [catModal, setCatModal] = useState(false)
const secondaryIds = allPrinterIds.filter(id => id !== masterId)
const handleMasterChange = (id) => {
setMasterId(id)
if (id && !allPrinterIds.includes(id)) setAllPrinterIds(prev => [...prev, id])
}
const handleSecondaryChange = (ids) => {
const next = new Set(ids)
if (masterId) next.add(masterId)
setAllPrinterIds([...next])
}
const handleBypassKds = (v) => { setBypassKds(v); if (v) setBypassPending(true) }
const handleBypassPending = (v) => { setBypassPending(v); if (!v) setBypassKds(false) }
const valid = name.trim().length > 0
const submit = (e) => {
e.preventDefault()
if (!valid) return
onSave({
name: name.trim(),
description: description.trim() || null,
notification_name: notifName.trim() || null,
printer_ids: allPrinterIds,
master_printer_id: masterId || null,
auto_print: autoPrint,
master_copies: masterCopies,
secondary_copies: secondaryCopies,
sort_items_by: sortBy,
group_by_category: groupCat,
category_order: catOrder,
print_checkboxes: checkboxes,
bypass_pending: bypassPending,
bypass_kds: bypassKds,
auto_ready_to_served: autoReadyServed,
})
}
return (
<>
<form onSubmit={submit} className="space-y-5">
{/* Identity */}
<Section icon={Settings2} label="Στοιχεία Ζώνης">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="label">Όνομα ζώνης *</label>
<input className="input" value={name} onChange={e => setName(e.target.value)}
placeholder="π.χ. Kitchen – Grill" autoFocus />
</div>
<div>
<label className="label">Όνομα ειδοποίησης</label>
<input className="input" value={notifName} onChange={e => setNotifName(e.target.value)}
placeholder="π.χ. Κουζίνα" />
</div>
</div>
<div>
<label className="label">Περιγραφή</label>
<input className="input" value={description} onChange={e => setDesc(e.target.value)}
placeholder="Προαιρετική περιγραφή" />
</div>
</Section>
{/* Printers */}
<Section icon={Printer} label="Εκτυπωτές">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="label">Κύριος εκτυπωτής</label>
<MasterPrinterPicker printers={printers} value={masterId} onChange={handleMasterChange} />
<p className="mt-1 text-xs text-gray-400">Ένας μόνο, υψηλή προτεραιότητα.</p>
</div>
<div>
<label className="label">Δευτερεύοντες εκτυπωτές</label>
<SecondaryPrinterPicker
printers={printers} selected={secondaryIds}
masterId={masterId} onChange={handleSecondaryChange}
/>
<p className="mt-1 text-xs text-gray-400">Μόνο στη λειτουργία «Όλοι».</p>
</div>
</div>
<div>
<label className="label">Αυτόματη εκτύπωση</label>
<div className="grid grid-cols-3 gap-2">
{AUTO_PRINT_OPTIONS.map(opt => (
<button key={opt.value} type="button" onClick={() => setAutoPrint(opt.value)}
className={`px-3 py-2.5 rounded-xl border text-left transition-all
${autoPrint === opt.value
? 'border-primary-500 bg-primary-50 ring-1 ring-primary-500'
: 'border-gray-200 hover:border-gray-300 bg-white'}`}>
<div className={`text-xs font-bold leading-tight ${autoPrint === opt.value ? 'text-primary-700' : 'text-gray-700'}`}>{opt.label}</div>
<div className="text-[11px] text-gray-400 mt-0.5">{opt.desc}</div>
</button>
))}
</div>
</div>
{autoPrint !== 'none' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Αντίτυπα κύριου</label>
<CopySpinner value={masterCopies} onChange={setMasterCopies} />
</div>
{autoPrint === 'all' && (
<div>
<label className="label">Αντίτυπα δευτερευόντων</label>
<CopySpinner value={secondaryCopies} onChange={setSecondaryCopies} />
</div>
)}
</div>
)}
</Section>
{/* Ticket formatting */}
<Section icon={ArrowUpDown} label="Μορφοποίηση Δελτίου">
<div>
<label className="label">Ταξινόμηση αντικειμένων</label>
<div className="grid grid-cols-3 gap-2">
{SORT_OPTIONS.map(opt => (
<button key={opt.value} type="button" onClick={() => setSortBy(opt.value)}
className={`px-3 py-2.5 rounded-xl border text-left transition-all
${sortBy === opt.value
? 'border-primary-500 bg-primary-50 ring-1 ring-primary-500'
: 'border-gray-200 hover:border-gray-300 bg-white'}`}>
<div className={`text-xs font-bold leading-tight ${sortBy === opt.value ? 'text-primary-700' : 'text-gray-700'}`}>{opt.label}</div>
<div className="text-[11px] text-gray-400 mt-0.5">{opt.desc}</div>
</button>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<ToggleRow
label="Ομαδοποίηση κατά κατηγορία"
desc="Αντικείμενα ομαδοποιούνται ανά κατηγορία."
value={groupCat} onChange={setGroupCat}
>
<button type="button" onClick={() => setCatModal(true)}
className="mt-1 inline-flex items-center gap-1.5 text-xs font-semibold text-primary-600 hover:text-primary-700 bg-white border border-primary-200 rounded-lg px-3 py-1.5 hover:bg-primary-50 transition-colors">
<Layers size={12} />
Σειρά κατηγοριών
{catOrder.length > 0 && (
<span className="bg-primary-100 text-primary-700 rounded px-1 text-[10px] font-bold">{catOrder.length}</span>
)}
</button>
</ToggleRow>
<ToggleRow
label="Checkboxes"
desc="Προσθέτει [ ] πριν από κάθε αντικείμενο."
value={checkboxes} onChange={setCheckboxes}
/>
</div>
</Section>
{/* KDS Behavior */}
<Section icon={Zap} label="Συμπεριφορά KDS">
<div className="grid grid-cols-1 gap-3">
<ToggleRow
label="Παράκαμψη Pending & Prep"
desc="Αντικείμενα ξεκινούν απευθείας σε READY. Χρήσιμο για ποτά ή μπύρες."
value={bypassPending} onChange={handleBypassPending}
/>
<ToggleRow
label="Παράκαμψη KDS Status"
desc="Αντικείμενα πηγαίνουν κατευθείαν σε SERVED. Ενεργοποιεί αυτόματα και την Παράκαμψη Pending."
value={bypassKds} onChange={handleBypassKds} warning
/>
<ToggleRow
label="Αυτόματο Ready → Served"
desc="Όταν ο σεφ επισημαίνει READY στο KDS, αυτόματα αναβαθμίζεται και σε SERVED."
value={autoReadyServed} onChange={setAutoReadyServed}
/>
</div>
</Section>
{/* Actions */}
<div className="flex gap-3 justify-end pt-3 border-t border-gray-100">
<button type="button" onClick={onCancel} className="btn btn-secondary">
Ακύρωση
</button>
<button type="submit" disabled={!valid || saving} className="btn btn-primary flex items-center gap-2">
<Check size={15} />
{saving ? 'Αποθήκευση…' : 'Αποθήκευση'}
</button>
</div>
</form>
{catModal && (
<CategoryOrderModal
categories={categories} order={catOrder}
onSave={setCatOrder} onClose={() => setCatModal(false)}
/>
)}
</>
)
}
// ── Zone Card ─────────────────────────────────────────────────────────────────
function Badge({ children, variant = 'default' }) {
const cls = {
default: 'bg-gray-100 text-gray-600',
primary: 'bg-primary-100 text-primary-700',
green: 'bg-green-100 text-green-700',
amber: 'bg-amber-100 text-amber-700',
red: 'bg-red-100 text-red-700',
}[variant] || 'bg-gray-100 text-gray-600'
return (
<span className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-semibold ${cls}`}>
{children}
</span>
)
}
function ZoneCard({ zone, printers, onEdit, onDelete }) {
const [confirmDelete, setConfirmDelete] = useState(false)
const printerMap = Object.fromEntries(printers.map(p => [p.id, p]))
const masterPrinter = printerMap[zone.master_printer_id]
const secondaryPrinters = (zone.secondary_printer_ids || []).map(id => printerMap[id]).filter(Boolean)
const printLabel = {
none: { text: 'Χωρίς εκτύπωση', variant: 'default' },
master: { text: 'Κύριος μόνο', variant: 'primary' },
all: { text: 'Όλοι', variant: 'primary' },
}[zone.auto_print] || { text: '—', variant: 'default' }
const sortLabel = {
order_time: 'Σειρά παρ.',
item_count: 'Κατά πλήθος',
alpha: 'Αλφαβητικά',
}[zone.sort_items_by] || zone.sort_items_by
return (
<div className="bg-white border border-gray-200 rounded-2xl shadow-sm overflow-hidden hover:shadow-md hover:border-gray-300 transition-all group">
<div className="px-4 pt-4 pb-3 flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-bold text-gray-900 text-sm leading-tight">{zone.name}</h3>
{zone.bypass_kds && <Badge variant="red"><Zap size={10} />Bypass KDS</Badge>}
{!zone.bypass_kds && zone.bypass_pending && <Badge variant="amber"><SkipForward size={10} />Bypass Pending</Badge>}
{zone.auto_ready_to_served && <Badge variant="green"><Check size={10} />Auto Served</Badge>}
</div>
{zone.notification_name && (
<p className="text-xs text-primary-600 font-medium mt-0.5">{zone.notification_name}</p>
)}
{zone.description && <p className="text-xs text-gray-400 mt-0.5">{zone.description}</p>}
</div>
<div className="flex gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => onEdit(zone)}
className="p-1.5 rounded-lg text-gray-400 hover:text-primary-600 hover:bg-primary-50 transition-colors">
<Pencil size={13} />
</button>
{!confirmDelete ? (
<button onClick={() => setConfirmDelete(true)}
className="p-1.5 rounded-lg text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors">
<Trash2 size={13} />
</button>
) : (
<div className="flex gap-1 items-center">
<button onClick={() => setConfirmDelete(false)}
className="p-1.5 rounded-lg text-gray-400 hover:bg-gray-100 transition-colors">
<X size={13} />
</button>
<button onClick={() => onDelete(zone.id)}
className="px-2 py-1 rounded-lg text-xs font-bold bg-red-600 text-white hover:bg-red-700 transition-colors">
Διαγραφή
</button>
</div>
)}
</div>
</div>
<div className="px-4 pb-3 space-y-1.5">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wide w-14 shrink-0">Κύριος</span>
{masterPrinter ? (
<>
<span className="inline-flex items-center gap-1 bg-primary-50 text-primary-700 rounded-md px-2 py-0.5 text-xs font-medium">
<Printer size={10} />{masterPrinter.name}
</span>
{zone.auto_print !== 'none' && (
<span className="text-xs text-gray-400 font-mono">{zone.master_copies}×</span>
)}
</>
) : (
<span className="text-xs text-gray-400 italic">Χωρίς</span>
)}
</div>
{secondaryPrinters.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wide w-14 shrink-0">Δευτ.</span>
{secondaryPrinters.map(p => (
<span key={p.id} className="inline-flex items-center gap-1 bg-gray-100 text-gray-600 rounded-md px-2 py-0.5 text-xs font-medium">
<Printer size={10} />{p.name}
</span>
))}
{zone.auto_print === 'all' && (
<span className="text-xs text-gray-400 font-mono">{zone.secondary_copies}×</span>
)}
</div>
)}
</div>
<div className="px-4 py-2.5 border-t border-gray-100 bg-gray-50/60 flex flex-wrap gap-1.5 items-center">
<Badge variant={zone.auto_print !== 'none' ? 'primary' : 'default'}>{printLabel.text}</Badge>
<Badge>{sortLabel}</Badge>
{zone.group_by_category && <Badge><Layers size={10} />Κατά κατηγορία</Badge>}
{zone.print_checkboxes && <Badge><CheckSquare size={10} />Checkboxes</Badge>}
</div>
</div>
)
}
// ── Main Page ─────────────────────────────────────────────────────────────────
export default function PrepZonesConfigPage() {
const qc = useQueryClient()
const [showForm, setShowForm] = useState(false)
const [editing, setEditing] = useState(null)
const { data: zones = [], isLoading: lZ } = useQuery({ queryKey: ['prep-zones'], queryFn: fetchZones })
const { data: printers = [], isLoading: lP } = useQuery({ queryKey: ['printers'], queryFn: fetchPrinters })
const { data: categories = [], isLoading: lC } = useQuery({ queryKey: ['categories'], queryFn: fetchCategories })
const invalidate = () => qc.invalidateQueries({ queryKey: ['prep-zones'] })
const create = useMutation({
mutationFn: body => client.post('/api/prep-zones', body).then(r => r.data),
onSuccess: () => { invalidate(); setShowForm(false); toast.success('Ζώνη δημιουργήθηκε') },
onError: () => toast.error('Σφάλμα κατά τη δημιουργία'),
})
const update = useMutation({
mutationFn: ({ id, ...body }) => client.put(`/api/prep-zones/${id}`, body).then(r => r.data),
onSuccess: () => { invalidate(); setEditing(null); toast.success('Ζώνη ενημερώθηκε') },
onError: () => toast.error('Σφάλμα κατά την ενημέρωση'),
})
const remove = useMutation({
mutationFn: id => client.delete(`/api/prep-zones/${id}`),
onSuccess: () => { invalidate(); toast.success('Ζώνη διαγράφηκε') },
onError: () => toast.error('Σφάλμα κατά τη διαγραφή'),
})
if (lZ || lP || lC) {
return (
<div className="p-8 flex items-center gap-3 text-sm text-gray-400">
<div className="w-4 h-4 rounded-full border-2 border-gray-300 border-t-primary-500 animate-spin" />
Φόρτωση…
</div>
)
}
const flatCategories = categories.flatMap(c => [c, ...(c.subcategories || [])])
const formOpen = showForm || !!editing
return (
// h-full + overflow-y-auto makes this page scroll within the fixed AppLayout <main>
<div className="h-full overflow-y-auto">
<div className="p-6">
{/* Page header */}
<div className="flex items-start justify-between mb-6">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-orange-100 flex items-center justify-center shrink-0">
<FlameKindling size={16} className="text-orange-600" />
</div>
<div>
<h1 className="text-xl font-bold text-gray-900 leading-tight">Ζώνες Προετοιμασίας</h1>
<p className="text-sm text-gray-500">Ζώνες, εκτυπωτές και συμπεριφορά KDS</p>
</div>
</div>
{!formOpen && (
<button onClick={() => setShowForm(true)} className="btn btn-primary flex items-center gap-2 shrink-0">
<Plus size={15} />
Νέα Ζώνη
</button>
)}
</div>
{/* Two-column layout when form is open */}
{formOpen ? (
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-6 items-start xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
{/* Form panel */}
<div className={`bg-white rounded-2xl p-6 shadow-sm border ${showForm ? 'border-primary-200' : 'border-amber-200'}`}>
<div className="flex items-center gap-2 mb-5">
<div className={`w-1 h-5 rounded-full ${showForm ? 'bg-primary-500' : 'bg-amber-400'}`} />
<h2 className="text-sm font-bold text-gray-800">
{showForm ? 'Νέα Ζώνη Προετοιμασίας' : `Επεξεργασία: ${editing?.name}`}
</h2>
</div>
<ZoneForm
initial={editing}
printers={printers}
categories={flatCategories}
onSave={showForm
? (body) => create.mutate(body)
: (body) => update.mutate({ id: editing.id, ...body })}
onCancel={() => { setShowForm(false); setEditing(null) }}
saving={create.isPending || update.isPending}
/>
</div>
{/* Zone list sidebar */}
<div className="space-y-3">
{zones.length === 0 ? (
<div className="text-center py-10 text-gray-400 bg-white rounded-2xl border border-gray-200">
<FlameKindling size={24} className="mx-auto mb-2 text-gray-300" />
<p className="text-sm">Δεν υπάρχουν ζώνες ακόμα</p>
</div>
) : zones.map(zone => (
<ZoneCard
key={zone.id}
zone={zone}
printers={printers}
onEdit={z => { setShowForm(false); setEditing(z) }}
onDelete={id => remove.mutate(id)}
/>
))}
</div>
</div>
) : (
/* Full-width zone grid when no form open */
zones.length === 0 ? (
<div className="text-center py-20 text-gray-400">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-100 flex items-center justify-center">
<FlameKindling size={28} className="text-gray-300" />
</div>
<div className="font-semibold text-gray-600 mb-1">Δεν υπάρχουν ζώνες ακόμα</div>
<p className="text-sm">Δημιουργήστε την πρώτη ζώνη για να αρχίσετε.</p>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{zones.map(zone => (
<ZoneCard
key={zone.id}
zone={zone}
printers={printers}
onEdit={z => { setShowForm(false); setEditing(z) }}
onDelete={id => remove.mutate(id)}
/>
))}
</div>
)
)}
</div>
</div>
)
}