import { useState, useRef, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import toast from 'react-hot-toast' import client from '../api/client' import { ConfirmModal } from '../ui/Modal' import Button from '../ui/Button' function avatarColor(name) { const palette = ['#3758c9', '#7a44c9', '#2f9e5e', '#d94b26', '#8a6d2b', '#0d7a8a', '#c93775'] let h = 0 for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0 return palette[h % palette.length] } function WaiterAvatar({ waiter, size = 40 }) { const displayName = waiter.full_name || waiter.nickname || waiter.username if (waiter.avatar_url) { return ( {displayName} ) } const parts = displayName.trim().split(' ') const initials = (parts[0][0] + (parts[1]?.[0] || '')).toUpperCase() return (
{initials}
) } const DIGITS = ['1','2','3','4','5','6','7','8','9','','0','⌫'] function PinInput({ value, onChange }) { function press(d) { if (d === '⌫') { onChange(value.slice(0, -1)); return } if (d === '' || value.length >= 6) return onChange(value + d) } return (
{Array.from({ length: 4 }).map((_, i) => (
))}
{DIGITS.map((d, i) => ( ))}
) } // ── Actions dropdown menu ───────────────────────────────────────────────────── function ActionsMenu({ waiter, onEdit, onZones, onResetPin, onBlock, onDelete }) { const [open, setOpen] = useState(false) const ref = useRef(null) useEffect(() => { if (!open) return function handler(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false) } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [open]) const items = [ { label: 'Επεξεργασία', icon: editIcon, onClick: onEdit }, waiter.role === 'waiter' && { label: 'Πρόσβαση σε Ζώνες', icon: zonesIcon, onClick: onZones }, { label: 'Επαναφορά PIN', icon: pinIcon, onClick: onResetPin }, { label: waiter.is_active ? 'Μπλοκάρισμα' : 'Ξεμπλοκάρισμα', icon: blockIcon, onClick: onBlock, color: waiter.is_active ? '#f97316' : '#22c55e' }, { label: 'Διαγραφή', icon: trashIcon, onClick: onDelete, color: '#ef4444' }, ].filter(Boolean) return (
{open && (
{items.map((item, i) => ( ))}
)}
) } const editIcon = ( ) const zonesIcon = ( ) const pinIcon = ( ) const blockIcon = ( ) const trashIcon = ( ) // ── ZoneModal ───────────────────────────────────────────────────────────────── function ZoneModal({ waiter, groups, onClose }) { const qc = useQueryClient() const hasAllZones = waiter.zone_assignments.some(z => z.group_id === null) const assignedIds = new Set(waiter.zone_assignments.map(z => z.group_id).filter(id => id !== null)) const [allZones, setAllZones] = useState(hasAllZones) const [selected, setSelected] = useState(new Set(assignedIds)) const saveZones = useMutation({ mutationFn: (body) => client.put(`/api/waiters/${waiter.id}/zones`, body), onSuccess: () => { toast.success('Zones ενημερώθηκαν'); qc.invalidateQueries({ queryKey: ['waiters'] }); onClose() }, onError: () => toast.error('Σφάλμα'), }) function toggleGroup(gid) { setSelected(prev => { const next = new Set(prev); next.has(gid) ? next.delete(gid) : next.add(gid); return next }) } return (

Ζώνες — {waiter.username}

{!allZones && (
{groups.length === 0 &&

Δεν υπάρχουν ομάδες τραπεζιών.

} {groups.map(g => ( ))}
)} {!allZones && selected.size === 0 && (

Χωρίς επιλογή ο σερβιτόρος δεν βλέπει κανένα τραπέζι.

)}
) } // ── Shared form primitives ──────────────────────────────────────────────────── function FormField({ label, required, desc, children }) { return (
{children} {desc &&

{desc}

}
) } function SectionLabel({ children }) { return (
{children}
) } function InfoBlock({ label, value, valueColor }) { return (
{label} {value || '—'}
) } const inputStyle = { width: '100%', padding: '7px 10px', border: '1px solid #e5e7eb', borderRadius: 7, fontSize: 13, outline: 'none', color: '#111827', background: '#fff', boxSizing: 'border-box', } const EMPTY_FORM = { username: '', full_name: '', nickname: '', mobile_phone: '', email: '', note: '', role: 'waiter', pin: '', hourly_rate: '' } // ── Main page ───────────────────────────────────────────────────────────────── export default function WaitersPage() { const qc = useQueryClient() const [addModal, setAddModal] = useState(false) const [pinModal, setPinModal] = useState(null) const [zoneModal, setZoneModal] = useState(null) const [confirmDelete, setConfirmDelete] = useState(null) const [newPin, setNewPin] = useState('') const [newForm, setNewForm] = useState(EMPTY_FORM) const [newAvatarFile, setNewAvatarFile] = useState(null) const [newAvatarPreview, setNewAvatarPreview] = useState(null) const [editModal, setEditModal] = useState(null) const [editForm, setEditForm] = useState({ username: '', full_name: '', nickname: '', mobile_phone: '', email: '', note: '', role: 'waiter', hourly_rate: '' }) const avatarInputRef = useRef(null) const newAvatarInputRef = useRef(null) const { data: waiters = [], isLoading } = useQuery({ queryKey: ['waiters'], queryFn: () => client.get('/api/waiters/').then(r => r.data), }) const { data: groups = [] } = useQuery({ queryKey: ['table-groups'], queryFn: () => client.get('/api/tables/groups').then(r => r.data), }) const invalidate = () => qc.invalidateQueries({ queryKey: ['waiters'] }) const createWaiter = useMutation({ mutationFn: async (body) => { const res = await client.post('/api/waiters/', body) if (newAvatarFile) { const fd = new FormData() fd.append('file', newAvatarFile) await client.post(`/api/waiters/${res.data.id}/avatar`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }) } return res }, onSuccess: () => { toast.success('Σερβιτόρος δημιουργήθηκε') setAddModal(false); setNewForm(EMPTY_FORM); setNewAvatarFile(null); setNewAvatarPreview(null) invalidate() }, onError: (err) => toast.error(err.response?.data?.detail || 'Σφάλμα'), }) const updateWaiter = useMutation({ mutationFn: ({ id, ...body }) => client.put(`/api/waiters/${id}`, body), onSuccess: () => { toast.success('Στοιχεία ενημερώθηκαν'); setEditModal(null); invalidate() }, onError: (err) => toast.error(err.response?.data?.detail || 'Σφάλμα'), }) const uploadAvatar = useMutation({ mutationFn: ({ id, file }) => { const fd = new FormData(); fd.append('file', file) return client.post(`/api/waiters/${id}/avatar`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }) }, onSuccess: (res) => { toast.success('Avatar ανέβηκε'); setEditModal(res.data); invalidate() }, onError: () => toast.error('Σφάλμα μεταφόρτωσης'), }) const deleteAvatar = useMutation({ mutationFn: (id) => client.delete(`/api/waiters/${id}/avatar`), onSuccess: (res) => { toast.success('Avatar αφαιρέθηκε'); setEditModal(res.data); invalidate() }, onError: () => toast.error('Σφάλμα'), }) const toggleBlock = useMutation({ mutationFn: (id) => client.put(`/api/waiters/${id}/block`), onSuccess: () => invalidate(), onError: () => toast.error('Σφάλμα'), }) const resetPin = useMutation({ mutationFn: ({ id, pin }) => client.put(`/api/waiters/${id}/reset-pin?pin=${encodeURIComponent(pin)}`), onSuccess: () => { toast.success('PIN ανανεώθηκε'); setPinModal(null); setNewPin('') }, onError: () => toast.error('Σφάλμα'), }) const deleteWaiter = useMutation({ mutationFn: (id) => client.delete(`/api/waiters/${id}`), onSuccess: () => { toast.success('Διαγράφηκε'); setConfirmDelete(null); invalidate() }, onError: () => toast.error('Σφάλμα'), }) if (isLoading) return
Φόρτωση…
function openEdit(w) { setEditModal(w) setEditForm({ username: w.username || '', full_name: w.full_name || '', nickname: w.nickname || '', mobile_phone: w.mobile_phone || '', email: w.email || '', note: w.note || '', role: w.role || 'waiter', hourly_rate: w.hourly_rate ?? '' }) } return (
{waiters.length === 0 && (

Δεν υπάρχουν σερβιτόροι.

)} {waiters.map(w => (

{w.full_name || w.username}

{w.nickname && ({w.nickname})}

{w.username} · {w.role}

{w.mobile_phone &&

{w.mobile_phone}

} {w.role === 'waiter' && (

{w.zone_assignments.length === 0 ? 'Χωρίς ζώνες' : w.zone_assignments.some(z => z.group_id === null) ? 'Όλες οι ζώνες' : `${w.zone_assignments.length} ζών${w.zone_assignments.length === 1 ? 'η' : 'ες'}`}

)}
{/* Active status badge — only this stays inline */} {w.is_active ? 'Ενεργός' : 'Αποκλεισμένος'} {/* Single actions dropdown */} openEdit(w)} onZones={() => setZoneModal(w)} onResetPin={() => setPinModal(w.id)} onBlock={() => toggleBlock.mutate(w.id)} onDelete={() => setConfirmDelete(w.id)} />
))}
{/* Add modal */} {addModal && ( { setAddModal(false); setNewForm(EMPTY_FORM); setNewAvatarFile(null); setNewAvatarPreview(null) }} onSubmit={() => createWaiter.mutate({ username: newForm.username, full_name: newForm.full_name || null, nickname: newForm.nickname || null, mobile_phone: newForm.mobile_phone || null, email: newForm.email || null, note: newForm.note || null, pin: newForm.pin, role: newForm.role, is_active: true, hourly_rate: newForm.hourly_rate !== '' ? parseFloat(newForm.hourly_rate) : null, })} /> )} {/* Edit modal */} {editModal && ( setEditModal(null)} onSubmit={() => updateWaiter.mutate({ id: editModal.id, username: editForm.username.trim() || undefined, full_name: editForm.full_name || null, nickname: editForm.nickname || null, mobile_phone: editForm.mobile_phone || null, email: editForm.email || null, note: editForm.note || null, role: editForm.role, hourly_rate: editForm.hourly_rate !== '' ? parseFloat(editForm.hourly_rate) : null, })} onUploadAvatar={file => uploadAvatar.mutate({ id: editModal.id, file })} onDeleteAvatar={() => deleteAvatar.mutate(editModal.id)} /> )} {/* Reset PIN modal */} {pinModal !== null && (

Επαναφορά PIN — {waiters.find(w => w.id === pinModal)?.username}

)} {confirmDelete !== null && ( deleteWaiter.mutate(confirmDelete)} onCancel={() => setConfirmDelete(null)} /> )} {zoneModal && ( setZoneModal(null)} /> )}
) } // ── Add Waiter Modal — two-column ───────────────────────────────────────────── function AddWaiterModal({ form, setForm, avatarFile, avatarPreview, setAvatarFile, setAvatarPreview, avatarInputRef, isPending, onClose, onSubmit }) { const f = (field, val) => setForm(p => ({ ...p, [field]: val })) const canSubmit = form.username.trim() && form.full_name.trim() && form.nickname.trim() && form.pin.length >= 4 return (
{/* Header */}
{avatarPreview ? preview :
👤
}

Νέος σερβιτόρος

Συμπληρώστε τα στοιχεία του εργαζόμενου

{ const file = e.target.files?.[0] if (file) { setAvatarFile(file); setAvatarPreview(URL.createObjectURL(file)) } e.target.value = '' }} /> {avatarPreview && ( )}
{/* Body — three columns, scrollable */}
{/* Column 1: Identity + Account */}
Ταυτότητα
f('full_name', e.target.value)} autoFocus /> f('nickname', e.target.value)} />
Λογαριασμός
f('username', e.target.value)} />
{/* Column 2: Contact info */}
Επικοινωνία
f('mobile_phone', e.target.value)} /> f('email', e.target.value)} />