import { useState } from 'react'
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 Toggle({ checked, onChange, disabled }) {
return (
!disabled && onChange(!checked)}
style={{
width: 44, height: 24, borderRadius: 999, border: 'none', cursor: disabled ? 'not-allowed' : 'pointer',
background: checked ? '#16a34a' : '#d1d5db',
position: 'relative', transition: 'background 150ms', flexShrink: 0, opacity: disabled ? 0.5 : 1,
}}
>
)
}
const COMMON_TIMEZONES = [
'Europe/Athens', 'Europe/London', 'Europe/Berlin', 'Europe/Paris', 'Europe/Rome',
'Europe/Madrid', 'Europe/Amsterdam', 'Europe/Brussels', 'Europe/Bucharest',
'Europe/Helsinki', 'Europe/Istanbul', 'America/New_York', 'America/Chicago',
'America/Denver', 'America/Los_Angeles', 'UTC',
]
function TimezoneSection() {
const qc = useQueryClient()
const { data: settings, isLoading } = useQuery({
queryKey: ['pos-settings'],
queryFn: () => client.get('/api/settings/').then(r => r.data),
staleTime: 30_000,
})
const updateMut = useMutation({
mutationFn: ({ key, value }) => client.put(`/api/settings/${key}`, { value }),
onSuccess: () => { toast.success('Αποθηκεύτηκε'); qc.invalidateQueries({ queryKey: ['pos-settings'] }) },
onError: () => toast.error('Σφάλμα αποθήκευσης'),
})
const currentTz = settings?.['system.timezone']?.value ?? 'Europe/Athens'
const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone
return (
Ζώνη Ώρας
Η ζώνη ώρας που χρησιμοποιεί το backend για χρονοσφραγίδες. Αν οι ώρες έναρξης βάρδιας εμφανίζονται λανθασμένες, ρυθμίστε αυτό να ταιριάζει με την τοπική σας ζώνη.
{isLoading &&
Φόρτωση…
}
{!isLoading && (
updateMut.mutate({ key: 'system.timezone', value: e.target.value })}
disabled={updateMut.isPending}
className="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-800 focus:outline-none flex-1 max-w-xs"
>
{COMMON_TIMEZONES.map(tz => {tz} )}
{updateMut.isPending && Αποθήκευση… }
Ζώνη ώρας browser: {browserTz}
{browserTz !== currentTz && (
⚠ Διαφέρει από τη ρύθμιση backend
)}
Η αλλαγή ζώνης ώρας αποθηκεύεται και εφαρμόζεται στο frontend αμέσως. Για πλήρη εφαρμογή στον backend server (χρονοσφραγίδες), απαιτείται επανεκκίνηση του container.
)}
)
}
const LOCK_TIMEOUT_OPTIONS = [
{ label: 'Απενεργοποιημένο', value: 0 },
{ label: '1 λεπτό', value: 1 },
{ label: '5 λεπτά', value: 5 },
{ label: '10 λεπτά', value: 10 },
{ label: '15 λεπτά', value: 15 },
{ label: '30 λεπτά', value: 30 },
{ label: '60 λεπτά', value: 60 },
]
const LOCK_SETTINGS_KEY = 'manager_lock_timeout'
function AutoLockSection() {
const raw = parseInt(localStorage.getItem(LOCK_SETTINGS_KEY) || '0', 10)
const [timeout, setTimeout_] = useState(isNaN(raw) ? 0 : raw)
function handleChange(val) {
const n = parseInt(val, 10)
setTimeout_(n)
if (n > 0) {
localStorage.setItem(LOCK_SETTINGS_KEY, String(n))
} else {
localStorage.removeItem(LOCK_SETTINGS_KEY)
}
}
return (
Αυτόματο Κλείδωμα Διαχειριστή
Αν δεν υπάρξει δραστηριότητα για το παρακάτω διάστημα, η οθόνη κλειδώνει και ζητάει PIN.
Το 0 απενεργοποιεί το αυτόματο κλείδωμα.
handleChange(e.target.value)}
className="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-800 focus:outline-none w-52"
>
{LOCK_TIMEOUT_OPTIONS.map(o => (
{o.label}
))}
{timeout > 0 && (
Κλείδωμα μετά από {timeout} {timeout === 1 ? 'λεπτό' : 'λεπτά'} αδράνειας
)}
{timeout === 0 && (
Μόνο χειροκίνητο κλείδωμα (κουμπί 🔒)
)}
)
}
function ShiftSettingsSection() {
const qc = useQueryClient()
const { data: settings, isLoading } = useQuery({
queryKey: ['pos-settings'],
queryFn: () => client.get('/api/settings/').then(r => r.data),
staleTime: 30_000,
})
const updateMut = useMutation({
mutationFn: ({ key, value }) => client.put(`/api/settings/${key}`, { value }),
onSuccess: () => { toast.success('Αποθηκεύτηκε'); qc.invalidateQueries({ queryKey: ['pos-settings'] }) },
onError: () => toast.error('Σφάλμα αποθήκευσης'),
})
function toggle(key, current) {
updateMut.mutate({ key, value: current === 'true' ? 'false' : 'true' })
}
const selfStart = settings?.['shifts.waiter_self_start']?.value ?? 'true'
const selfEnd = settings?.['shifts.waiter_self_end']?.value ?? 'true'
return (
Ρυθμίσεις Βάρδιας
Έλεγχος του τι επιτρέπεται να κάνουν οι σερβιτόροι μόνοι τους
{isLoading &&
Φόρτωση…
}
{!isLoading && (
<>
Αυτόματη Έναρξη Βάρδιας
Οι σερβιτόροι μπορούν να ξεκινούν μόνοι τους τη βάρδια τους
toggle('shifts.waiter_self_start', selfStart)} disabled={updateMut.isPending} />
Αυτόματο Κλείσιμο Βάρδιας
Οι σερβιτόροι μπορούν να κλείνουν μόνοι τους τη βάρδια τους
toggle('shifts.waiter_self_end', selfEnd)} disabled={updateMut.isPending} />
>
)}
)
}
// ─── Flag definitions ─────────────────────────────────────────────────────────
const FLAG_COLORS = [
'#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6',
'#8b5cf6', '#ec4899', '#06b6d4', '#6b7280', '#dc2626',
]
const RESTAURANT_EMOJIS = [
'🍽️', '🥂', '🍾', '🎂', '🎉', '🍰', '🥳', '👶', '🐶', '🐱',
'♿', '🌿', '🥗', '⭐', '💎', '🔥', '❄️', '⏳', '🧹', '⚠️',
]
function EmojiPicker({ value, onChange }) {
const [open, setOpen] = useState(false)
return (
setOpen(o => !o)} style={{
width: 60, height: 36, borderRadius: 8, border: '1px solid #dfe2e6',
background: 'white', fontSize: 20, textAlign: 'center', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>{value || '+'}
{open && (
{RESTAURANT_EMOJIS.map(e => (
{ onChange(e); setOpen(false) }} style={{
fontSize: 20, background: value === e ? '#eff3ff' : 'none',
border: 'none', borderRadius: 6, padding: '4px 0', cursor: 'pointer',
}}>{e}
))}
{ onChange(''); setOpen(false) }} style={{
fontSize: 11, color: '#9ca3af', background: 'none', border: 'none', cursor: 'pointer', padding: '4px 0', borderRadius: 6,
}}>✕ clear
)}
)
}
function FlagDisplayModeSection() {
const qc = useQueryClient()
const { data: settings } = useQuery({
queryKey: ['pos-settings'],
queryFn: () => client.get('/api/settings/').then(r => r.data),
staleTime: 30_000,
})
const updateMut = useMutation({
mutationFn: ({ key, value }) => client.put(`/api/settings/${key}`, { value }),
onSuccess: () => { toast.success('Αποθηκεύτηκε'); qc.invalidateQueries({ queryKey: ['pos-settings'] }) },
onError: () => toast.error('Σφάλμα αποθήκευσης'),
})
const current = settings?.['flags.display_mode']?.value ?? 'both'
const options = [
{ value: 'icon', label: '😀 Μόνο εικονίδιο' },
{ value: 'text', label: 'Aa Μόνο κείμενο' },
{ value: 'both', label: '😀 Aa Και τα δύο' },
]
return (
Εμφάνιση σημαιών στις κάρτες τραπεζιών
{options.map(o => (
updateMut.mutate({ key: 'flags.display_mode', value: o.value })} style={{
height: 32, padding: '0 12px', borderRadius: 8, fontSize: 12, fontWeight: 600, cursor: 'pointer',
border: `1.5px solid ${current === o.value ? '#3758c9' : '#dfe2e6'}`,
background: current === o.value ? '#eff3ff' : 'white',
color: current === o.value ? '#3758c9' : '#374151',
}}>{o.label}
))}
)
}
function FlagDefsSection() {
const qc = useQueryClient()
const [editingId, setEditingId] = useState(null)
const [editForm, setEditForm] = useState({})
const [newForm, setNewForm] = useState({ name: '', emoji: '', color: '#6b7280', text_color: null })
const [showNew, setShowNew] = useState(false)
const { data: flags = [], isLoading } = useQuery({
queryKey: ['flag-defs'],
queryFn: () => client.get('/api/flags/defs?include_inactive=true').then(r => r.data),
staleTime: 30_000,
})
const createMut = useMutation({
mutationFn: (body) => client.post('/api/flags/defs', body),
onSuccess: () => { toast.success('Δημιουργήθηκε'); qc.invalidateQueries({ queryKey: ['flag-defs'] }); setShowNew(false); setNewForm({ name: '', emoji: '', color: '#6b7280', text_color: null }) },
onError: () => toast.error('Σφάλμα'),
})
const updateMut = useMutation({
mutationFn: ({ id, ...body }) => client.put(`/api/flags/defs/${id}`, body),
onSuccess: () => { toast.success('Αποθηκεύτηκε'); qc.invalidateQueries({ queryKey: ['flag-defs'] }); setEditingId(null) },
onError: () => toast.error('Σφάλμα αποθήκευσης'),
})
const deleteMut = useMutation({
mutationFn: (id) => client.delete(`/api/flags/defs/${id}`),
onSuccess: () => { toast.success('Απενεργοποιήθηκε'); qc.invalidateQueries({ queryKey: ['flag-defs'] }) },
onError: () => toast.error('Σφάλμα'),
})
function startEdit(flag) {
setEditingId(flag.id)
setEditForm({ name: flag.name, emoji: flag.emoji || '', color: flag.color || '#6b7280', text_color: flag.text_color || null, sort_order: flag.sort_order })
}
const rowStyle = { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 20px', borderBottom: '1px solid #f4f4f2' }
return (
Σημαίες Τραπεζιών
Χρησιμοποιούνται για να επισημαίνετε καταστάσεις στα τραπέζια
setShowNew(v => !v)} style={{
height: 32, padding: '0 14px', borderRadius: 8, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: '#374151',
}}>+ Νέα
{showNew && (
setNewForm(f => ({ ...f, emoji: v }))} />
setNewForm(f => ({ ...f, name: e.target.value }))}
style={{ flex: 1, minWidth: 160, height: 36, borderRadius: 8, border: '1px solid #dfe2e6', padding: '0 12px', fontSize: 13, fontFamily: 'inherit' }} />
{FLAG_COLORS.map(c => (
setNewForm(f => ({ ...f, color: c }))}
style={{ width: 24, height: 24, borderRadius: '50%', background: c, border: newForm.color === c ? '3px solid #111' : '2px solid transparent', cursor: 'pointer' }} />
))}
Χρώμα γραφής:
{[{ val: null, label: 'Α', bg: newForm.color || '#6b7280', text: '#ffffff' }, { val: '#000000', label: 'Α', bg: newForm.color || '#6b7280', text: '#000000' }].map(opt => (
setNewForm(f => ({ ...f, text_color: opt.val }))}
style={{ width: 28, height: 28, borderRadius: 6, background: opt.bg, color: opt.text, fontSize: 14, fontWeight: 700, border: newForm.text_color === opt.val ? '3px solid #111' : '2px solid #dfe2e6', cursor: 'pointer' }}>{opt.label}
))}
createMut.mutate(newForm)} disabled={!newForm.name.trim() || createMut.isPending}
style={{ height: 36, padding: '0 16px', borderRadius: 8, background: '#3758c9', color: 'white', border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>Αποθήκευση
setShowNew(false)} style={{ height: 36, padding: '0 14px', borderRadius: 8, border: '1px solid #dfe2e6', background: 'white', fontSize: 13, cursor: 'pointer' }}>Άκυρο
)}
{isLoading &&
Φόρτωση…
}
{!isLoading && flags.length === 0 && (
Δεν υπάρχουν σημαίες ακόμα.
)}
{flags.map(flag => (
{editingId === flag.id ? (
setEditForm(f => ({ ...f, emoji: v }))} />
setEditForm(f => ({ ...f, name: e.target.value }))}
style={{ flex: 1, minWidth: 120, height: 32, borderRadius: 6, border: '1px solid #dfe2e6', padding: '0 10px', fontSize: 13, fontFamily: 'inherit' }} />
{FLAG_COLORS.map(c => (
setEditForm(f => ({ ...f, color: c }))}
style={{ width: 20, height: 20, borderRadius: '50%', background: c, border: editForm.color === c ? '3px solid #111' : '2px solid transparent', cursor: 'pointer' }} />
))}
{[{ val: null, text: '#ffffff' }, { val: '#000000', text: '#000000' }].map(opt => (
setEditForm(f => ({ ...f, text_color: opt.val }))}
style={{ width: 24, height: 24, borderRadius: 6, background: editForm.color || '#6b7280', color: opt.text, fontSize: 13, fontWeight: 700, border: editForm.text_color === opt.val ? '3px solid #111' : '2px solid #dfe2e6', cursor: 'pointer' }}>Α
))}
updateMut.mutate({ id: flag.id, ...editForm })} disabled={updateMut.isPending}
style={{ height: 32, padding: '0 12px', borderRadius: 6, background: '#16a34a', color: 'white', border: 'none', fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>✓
setEditingId(null)}
style={{ height: 32, padding: '0 10px', borderRadius: 6, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, cursor: 'pointer' }}>✕
) : (
<>
{flag.emoji || '🏷️'}
{flag.name}
{!flag.is_active &&
Ανενεργή }
startEdit(flag)} style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, cursor: 'pointer', color: '#374151' }}>Επεξεργασία
{flag.is_active && (
deleteMut.mutate(flag.id)} style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #fee2e2', background: '#fff5f5', fontSize: 12, cursor: 'pointer', color: '#dc2626' }}>Διαγραφή
)}
>
)}
))}
)
}
// ─── Quick message templates ──────────────────────────────────────────────────
function QuickTemplatesSection() {
const qc = useQueryClient()
const [editingId, setEditingId] = useState(null)
const [editBody, setEditBody] = useState('')
const [newBody, setNewBody] = useState('')
const [showNew, setShowNew] = useState(false)
const { data: templates = [], isLoading } = useQuery({
queryKey: ['quick-templates'],
queryFn: () => client.get('/api/messages/templates').then(r => r.data),
staleTime: 30_000,
})
const createMut = useMutation({
mutationFn: (body) => client.post('/api/messages/templates', body),
onSuccess: () => { toast.success('Δημιουργήθηκε'); qc.invalidateQueries({ queryKey: ['quick-templates'] }); setShowNew(false); setNewBody('') },
onError: () => toast.error('Σφάλμα'),
})
const updateMut = useMutation({
mutationFn: ({ id, body }) => client.put(`/api/messages/templates/${id}`, { body }),
onSuccess: () => { toast.success('Αποθηκεύτηκε'); qc.invalidateQueries({ queryKey: ['quick-templates'] }); setEditingId(null) },
onError: () => toast.error('Σφάλμα αποθήκευσης'),
})
const deleteMut = useMutation({
mutationFn: (id) => client.delete(`/api/messages/templates/${id}`),
onSuccess: () => { toast.success('Διαγράφηκε'); qc.invalidateQueries({ queryKey: ['quick-templates'] }) },
onError: () => toast.error('Σφάλμα'),
})
return (
Γρήγορα Μηνύματα
Πρότυπα μηνυμάτων για γρήγορη αποστολή στο προσωπικό
setShowNew(v => !v)} style={{
height: 32, padding: '0 14px', borderRadius: 8, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: '#374151',
}}>+ Νέο
{showNew && (
setNewBody(e.target.value)}
style={{ flex: 1, height: 36, borderRadius: 8, border: '1px solid #dfe2e6', padding: '0 12px', fontSize: 13, fontFamily: 'inherit' }} />
createMut.mutate({ body: newBody, sort_order: templates.length + 1 })}
disabled={!newBody.trim() || createMut.isPending}
style={{ height: 36, padding: '0 16px', borderRadius: 8, background: '#3758c9', color: 'white', border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>Αποθήκευση
setShowNew(false)} style={{ height: 36, padding: '0 14px', borderRadius: 8, border: '1px solid #dfe2e6', background: 'white', fontSize: 13, cursor: 'pointer' }}>Άκυρο
)}
{isLoading &&
Φόρτωση…
}
{!isLoading && templates.length === 0 && (
Δεν υπάρχουν πρότυπα ακόμα.
)}
{templates.map((t, idx) => (
{idx + 1}.
{editingId === t.id ? (
<>
setEditBody(e.target.value)}
style={{ flex: 1, height: 32, borderRadius: 6, border: '1px solid #dfe2e6', padding: '0 10px', fontSize: 13, fontFamily: 'inherit' }} />
updateMut.mutate({ id: t.id, body: editBody })} disabled={updateMut.isPending}
style={{ height: 32, padding: '0 12px', borderRadius: 6, background: '#16a34a', color: 'white', border: 'none', fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>✓
setEditingId(null)}
style={{ height: 32, padding: '0 10px', borderRadius: 6, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, cursor: 'pointer' }}>✕
>
) : (
<>
{t.body}
{ setEditingId(t.id); setEditBody(t.body) }}
style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, cursor: 'pointer', color: '#374151' }}>Επεξεργασία
deleteMut.mutate(t.id)}
style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #fee2e2', background: '#fff5f5', fontSize: 12, cursor: 'pointer', color: '#dc2626' }}>Διαγραφή
>
)}
))}
)
}
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 AppInfoTab() {
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 Φόρτωση…
return (
{/* System info */}
Σύστημα
Uptime
{formatUptime(status?.uptime_seconds ?? 0)}
Άδεια χρήσης
{status?.licensed ? 'Ενεργή' : 'Ανενεργή'}
Κατάσταση
{status?.locked ? 'Κλειδωμένο' : 'Λειτουργικό'}
{status?.expires_at && (
<>
Λήξη άδειας
{new Date(status.expires_at).toLocaleDateString('el-GR')}
>
)}
{/* Printers */}
Εκτυπωτές
{(!status?.printers || status.printers.length === 0) && (
Δεν βρέθηκαν εκτυπωτές.
)}
{status?.printers?.map(p => (
{p.reachable ? 'Προσβάσιμος' : 'Μη προσβάσιμος'}
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
))}
{user?.role === 'sysadmin' && (
Sysadmin
Έλεγχος κλειδώματος συστήματος.
client.post('/api/system/unlock').then(() => { toast.success('Ξεκλειδώθηκε'); qc.invalidateQueries({ queryKey: ['system-status'] }) })}
className="btn btn-primary text-sm">Ξεκλείδωμα
client.post('/api/system/lock').then(() => { toast.success('Κλειδώθηκε'); qc.invalidateQueries({ queryKey: ['system-status'] }) })}
className="btn btn-danger text-sm">Κλείδωμα
)}
)
}