import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import toast from 'react-hot-toast' import { QRCodeSVG } from 'qrcode.react' import client from '../../../api/client' import useAuthStore from '../../../store/authStore' 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.isPending && Αποθήκευση…}

Ζώνη ώρας browser: {browserTz} {browserTz !== currentTz && ( ⚠ Διαφέρει από τη ρύθμιση backend )}

Η αλλαγή ζώνης ώρας αποθηκεύεται και εφαρμόζεται στο frontend αμέσως. Για πλήρη εφαρμογή στον backend server (χρονοσφραγίδες), απαιτείται επανεκκίνηση του container.

)}
) } function StatsSection() { const { data: stats, isLoading } = useQuery({ queryKey: ['system-stats'], queryFn: () => client.get('/api/system/stats').then(r => r.data), staleTime: 60_000, }) const rows = [ { label: 'Κατηγορίες', value: stats?.categories }, { label: 'Προϊόντα (ενεργά)', value: stats?.products }, { label: 'Τραπέζια (ενεργά)', value: stats?.tables }, { label: 'Ζώνες Τραπεζιών', value: stats?.table_groups }, { label: 'Managers', value: stats?.managers }, { label: 'Σερβιτόροι', value: stats?.waiters }, ] return (

Στατιστικά Συστήματος

{isLoading &&

Φόρτωση…

} {!isLoading && (
{rows.map(({ label, value }) => ( <>
{label}
{value ?? '—'}
))}
)}
) } function DoubleConfirmImport({ isOpen, onClose, onConfirm, title, summary, isPending }) { const [step, setStep] = useState(1) function handleClose() { setStep(1) onClose() } function handleFirst() { setStep(2) } async function handleFinal() { await onConfirm() setStep(1) } if (!isOpen) return null return (
{step === 1 && (

Επιβεβαίωση Εισαγωγής

{summary}

Τα υπάρχοντα δεδομένα θα συγχωνευτούν — δεν θα διαγραφεί τίποτα.

)} {step === 2 && (

⚠️ ΤΕΛΕΥΤΑΙΑ ΠΡΟΕΙΔΟΠΟΙΗΣΗ

{title}

Αυτή η ενέργεια δεν αναιρείται. Η βάση δεδομένων θα τροποποιηθεί μόνιμα. Είστε απολύτως σίγουροι;

)}
) } function DataTransferSection() { const qc = useQueryClient() const [catalogModal, setCatalogModal] = useState(false) const [tablesModal, setTablesModal] = useState(false) const [catalogPayload, setCatalogPayload] = useState(null) const [tablesPayload, setTablesPayload] = useState(null) const [catalogSummary, setCatalogSummary] = useState('') const [tablesSummary, setTablesSummary] = useState('') const catalogImportMut = useMutation({ mutationFn: (payload) => client.post('/api/data-transfer/import/catalog', payload).then(r => r.data), onSuccess: () => { toast.success('Κατάλογος εισήχθη επιτυχώς') setCatalogModal(false) setCatalogPayload(null) qc.invalidateQueries({ queryKey: ['system-stats'] }) qc.invalidateQueries({ queryKey: ['products'] }) qc.invalidateQueries({ queryKey: ['categories'] }) }, onError: (err) => toast.error(err?.response?.data?.detail ?? 'Σφάλμα εισαγωγής'), }) const tablesImportMut = useMutation({ mutationFn: (payload) => client.post('/api/data-transfer/import/tables', payload).then(r => r.data), onSuccess: () => { toast.success('Τραπέζια εισήχθησαν επιτυχώς') setTablesModal(false) setTablesPayload(null) qc.invalidateQueries({ queryKey: ['system-stats'] }) qc.invalidateQueries({ queryKey: ['tables'] }) }, onError: (err) => toast.error(err?.response?.data?.detail ?? 'Σφάλμα εισαγωγής'), }) function handleExport(bundle) { client.get(`/api/data-transfer/export/${bundle}`, { responseType: 'blob' }) .then(res => { const disposition = res.headers['content-disposition'] ?? '' const match = disposition.match(/filename="([^"]+)"/) const filename = match ? match[1] : `xenia-${bundle}.json` const url = URL.createObjectURL(res.data) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) }) .catch(() => toast.error('Σφάλμα εξαγωγής')) } function handleFileSelect(bundle, e) { const file = e.target.files?.[0] e.target.value = '' if (!file) return const reader = new FileReader() reader.onload = (ev) => { try { const parsed = JSON.parse(ev.target.result) if (parsed.bundle !== bundle) { toast.error(`Λάθος αρχείο. Αναμένεται αρχείο τύπου '${bundle}'.`) return } if (bundle === 'catalog') { const cats = parsed.data?.categories ?? [] const orphans = parsed.data?.uncategorized_products ?? [] const prodCount = cats.reduce((acc, c) => acc + (c.products?.length ?? 0), 0) + orphans.length setCatalogSummary(`Πρόκειται να εισαγάγετε ${cats.length} κατηγορίες και ${prodCount} προϊόντα.`) setCatalogPayload(parsed) setCatalogModal(true) } else { const groups = parsed.data?.table_groups ?? [] const ungrouped = parsed.data?.ungrouped_tables ?? [] const tableCount = groups.reduce((acc, g) => acc + (g.tables?.length ?? 0), 0) + ungrouped.length setTablesSummary(`Πρόκειται να εισαγάγετε ${groups.length} ζώνες και ${tableCount} τραπέζια.`) setTablesPayload(parsed) setTablesModal(true) } } catch { toast.error('Μη έγκυρο αρχείο JSON') } } reader.readAsText(file) } return (

Εισαγωγή / Εξαγωγή Δεδομένων

Εξάγετε τα δεδομένα σας σε αρχείο ή εισάγετε δεδομένα από άλλη εγκατάσταση.

Κατάλογος Προϊόντων

Κατηγορίες + Προϊόντα (χωρίς εκτυπωτές)

Τραπέζια & Ζώνες

Ζώνες τραπεζιών + Τραπέζια

{ setCatalogModal(false); setCatalogPayload(null) }} onConfirm={() => catalogImportMut.mutateAsync(catalogPayload)} title="Εισαγωγή Καταλόγου Προϊόντων" summary={catalogSummary} isPending={catalogImportMut.isPending} /> { setTablesModal(false); setTablesPayload(null) }} onConfirm={() => tablesImportMut.mutateAsync(tablesPayload)} title="Εισαγωγή Τραπεζιών & Ζωνών" summary={tablesSummary} isPending={tablesImportMut.isPending} />
) } function QRModal({ url, onClose }) { return (
e.stopPropagation()} >

QR Σύνδεσης

{url}

Σαρώστε με το κινητό για σύνδεση στο σύστημα.

) } 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 [refreshing, setRefreshing] = useState(false) const [qrOpen, setQrOpen] = useState(false) const { data: status, isLoading } = useQuery({ queryKey: ['system-status'], queryFn: () => client.get('/api/system/status').then(r => r.data), refetchInterval: 30_000, }) async function handleRefresh() { setRefreshing(true) try { await client.post('/api/system/sync-license') await Promise.all([ qc.invalidateQueries({ queryKey: ['system-status'] }), qc.invalidateQueries({ queryKey: ['license-status'] }), ]) } catch { // sync-license failure just means cloud was unreachable — still refresh local caches await Promise.all([ qc.invalidateQueries({ queryKey: ['system-status'] }), qc.invalidateQueries({ queryKey: ['license-status'] }), ]) } finally { setRefreshing(false) } } if (isLoading) return
Φόρτωση…
return (
{/* System info */}

Σύστημα

Uptime
{formatUptime(status?.uptime_seconds ?? 0)}
Έκδοση
{status?.version ?? '—'} {status?.latest_version && status.latest_version !== status.version && ( Διαθέσιμη {status.latest_version} )} {status?.latest_version && status.latest_version === status.version && ( Ενημερωμένο )}
Άδεια χρήσης
{status?.licensed ? 'Ενεργή' : 'Ανενεργή'}
Κατάσταση
{status?.locked ? 'Κλειδωμένο' : status?.lock_pending ? 'Εκκρεμεί Κλείδωμα' : 'Λειτουργικό'}
{status?.expires_at && ( <>
Λήξη άδειας
{new Date(status.expires_at).toLocaleDateString('el-GR')}
)} {status?.waiter_domain && ( <>
Waiter Domain
{status.waiter_domain}
)}
{user?.role === 'superadmin' && (

Sysadmin

Έλεγχος κλειδώματος συστήματος.

)} {qrOpen && status?.waiter_domain && ( setQrOpen(false)} /> )}
) }