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.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.
)}
)
}
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}
Αυτή η ενέργεια δεν αναιρείται . Η βάση δεδομένων θα τροποποιηθεί μόνιμα.
Είστε απολύτως σίγουροι;
Άκυρο
{isPending ? 'Εισαγωγή…' : 'ΝΑΙ, ΕΙΣΑΓΩΓΗ'}
)}
)
}
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 (
Εισαγωγή / Εξαγωγή Δεδομένων
Εξάγετε τα δεδομένα σας σε αρχείο ή εισάγετε δεδομένα από άλλη εγκατάσταση.
Κατάλογος Προϊόντων
Κατηγορίες + Προϊόντα (χωρίς εκτυπωτές)
handleExport('catalog')} className="btn text-sm">
Εξαγωγή
Εισαγωγή
handleFileSelect('catalog', e)} />
Τραπέζια & Ζώνες
Ζώνες τραπεζιών + Τραπέζια
handleExport('tables')} className="btn text-sm">
Εξαγωγή
Εισαγωγή
handleFileSelect('tables', e)} />
{ 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 */}
Σύστημα
⭮
{refreshing ? 'Ανανέωση…' : 'Ανανέωση'}
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}
setQrOpen(true)}
className="flex items-center gap-1 h-6 px-2 rounded-md border border-gray-300 bg-white text-gray-600 text-xs font-medium hover:bg-gray-50 transition-colors flex-shrink-0"
>
QR Code
>
)}
{user?.role === 'superadmin' && (
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">Κλείδωμα
)}
{qrOpen && status?.waiter_domain && (
setQrOpen(false)} />
)}
)
}