Files
simple-pos-system/manager_dashboard/src/pages/Settings/tabs/AppInfoTab.jsx

555 lines
30 KiB
JavaScript
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 } 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 (
<button
role="switch"
aria-checked={checked}
onClick={() => !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,
}}
>
<span style={{
position: 'absolute', top: 3, left: checked ? 23 : 3,
width: 18, height: 18, borderRadius: '50%', background: 'white',
transition: 'left 150ms', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
}} />
</button>
)
}
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 (
<div className="card divide-y divide-gray-100">
<div className="px-5 py-4">
<h2 className="font-semibold text-gray-700">Ζώνη Ώρας</h2>
<p className="text-xs text-gray-400 mt-0.5">
Η ζώνη ώρας που χρησιμοποιεί το backend για χρονοσφραγίδες. Αν οι ώρες έναρξης βάρδιας εμφανίζονται λανθασμένες, ρυθμίστε αυτό να ταιριάζει με την τοπική σας ζώνη.
</p>
</div>
{isLoading && <p className="px-5 py-4 text-sm text-gray-400">Φόρτωση</p>}
{!isLoading && (
<div className="px-5 py-4 space-y-3">
<div className="flex items-center gap-3">
<select
value={currentTz}
onChange={e => 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 => <option key={tz} value={tz}>{tz}</option>)}
</select>
{updateMut.isPending && <span className="text-xs text-gray-400">Αποθήκευση</span>}
</div>
<p className="text-xs text-gray-400">
Ζώνη ώρας browser: <span className="font-medium text-gray-600">{browserTz}</span>
{browserTz !== currentTz && (
<span className="ml-2 text-amber-600 font-medium"> Διαφέρει από τη ρύθμιση backend</span>
)}
</p>
<p className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
Η αλλαγή ζώνης ώρας αποθηκεύεται και εφαρμόζεται στο frontend αμέσως. Για πλήρη εφαρμογή στον backend server (χρονοσφραγίδες), απαιτείται επανεκκίνηση του container.
</p>
</div>
)}
</div>
)
}
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 (
<div className="card divide-y divide-gray-100">
<div className="px-5 py-4">
<h2 className="font-semibold text-gray-700">Αυτόματο Κλείδωμα Διαχειριστή</h2>
<p className="text-xs text-gray-400 mt-0.5">
Αν δεν υπάρξει δραστηριότητα για το παρακάτω διάστημα, η οθόνη κλειδώνει και ζητάει PIN.
Το 0 απενεργοποιεί το αυτόματο κλείδωμα.
</p>
</div>
<div className="px-5 py-4 flex items-center gap-4">
<select
value={timeout}
onChange={e => 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 => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
{timeout > 0 && (
<span className="text-xs text-green-700 font-medium bg-green-50 border border-green-200 rounded-lg px-3 py-1.5">
Κλείδωμα μετά από {timeout} {timeout === 1 ? 'λεπτό' : 'λεπτά'} αδράνειας
</span>
)}
{timeout === 0 && (
<span className="text-xs text-gray-500">Μόνο χειροκίνητο κλείδωμα (κουμπί 🔒)</span>
)}
</div>
</div>
)
}
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 (
<div className="card divide-y divide-gray-100">
<div className="px-5 py-4">
<h2 className="font-semibold text-gray-700">Ρυθμίσεις Βάρδιας</h2>
<p className="text-xs text-gray-400 mt-0.5">Έλεγχος του τι επιτρέπεται να κάνουν οι σερβιτόροι μόνοι τους</p>
</div>
{isLoading && <p className="px-5 py-4 text-sm text-gray-400">Φόρτωση</p>}
{!isLoading && (
<>
<div className="flex items-center justify-between px-5 py-4">
<div>
<p className="text-sm font-medium text-gray-800">Αυτόματη Έναρξη Βάρδιας</p>
<p className="text-xs text-gray-500 mt-0.5">Οι σερβιτόροι μπορούν να ξεκινούν μόνοι τους τη βάρδια τους</p>
</div>
<Toggle checked={selfStart === 'true'} onChange={() => toggle('shifts.waiter_self_start', selfStart)} disabled={updateMut.isPending} />
</div>
<div className="flex items-center justify-between px-5 py-4">
<div>
<p className="text-sm font-medium text-gray-800">Αυτόματο Κλείσιμο Βάρδιας</p>
<p className="text-xs text-gray-500 mt-0.5">Οι σερβιτόροι μπορούν να κλείνουν μόνοι τους τη βάρδια τους</p>
</div>
<Toggle checked={selfEnd === 'true'} onChange={() => toggle('shifts.waiter_self_end', selfEnd)} disabled={updateMut.isPending} />
</div>
</>
)}
</div>
)
}
// ─── 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 (
<div style={{ position: 'relative' }}>
<button type="button" onClick={() => 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 || ''}</button>
{open && (
<div style={{
position: 'absolute', top: '110%', left: 0, zIndex: 200,
background: 'white', border: '1px solid #e2e8f0', borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.12)', padding: 8,
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 2, width: 180,
}}>
{RESTAURANT_EMOJIS.map(e => (
<button key={e} type="button" onClick={() => { onChange(e); setOpen(false) }} style={{
fontSize: 20, background: value === e ? '#eff3ff' : 'none',
border: 'none', borderRadius: 6, padding: '4px 0', cursor: 'pointer',
}}>{e}</button>
))}
<button type="button" onClick={() => { onChange(''); setOpen(false) }} style={{
fontSize: 11, color: '#9ca3af', background: 'none', border: 'none', cursor: 'pointer', padding: '4px 0', borderRadius: 6,
}}> clear</button>
</div>
)}
</div>
)
}
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 (
<div style={{ padding: '14px 20px', borderTop: '1px solid #f4f4f2' }}>
<div style={{ fontSize: 12, fontWeight: 600, color: '#5a6169', marginBottom: 8, textTransform: 'uppercase', letterSpacing: 0.5 }}>
Εμφάνιση σημαιών στις κάρτες τραπεζιών
</div>
<div style={{ display: 'flex', gap: 6 }}>
{options.map(o => (
<button key={o.value} onClick={() => 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}</button>
))}
</div>
</div>
)
}
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 (
<div className="card divide-y divide-gray-100">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px' }}>
<div>
<h2 className="font-semibold text-gray-700">Σημαίες Τραπεζιών</h2>
<p className="text-xs text-gray-400 mt-0.5">Χρησιμοποιούνται για να επισημαίνετε καταστάσεις στα τραπέζια</p>
</div>
<button onClick={() => 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',
}}>+ Νέα</button>
</div>
<FlagDisplayModeSection />
{showNew && (
<div style={{ padding: '14px 20px', background: '#f9fafb', display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'flex-end' }}>
<EmojiPicker value={newForm.emoji} onChange={v => setNewForm(f => ({ ...f, emoji: v }))} />
<input placeholder="Όνομα σημαίας" value={newForm.name} onChange={e => 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' }} />
<div style={{ display: 'flex', gap: 4 }}>
{FLAG_COLORS.map(c => (
<button key={c} onClick={() => 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' }} />
))}
</div>
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
<span style={{ fontSize: 11, color: '#6b7280', fontWeight: 600 }}>Χρώμα γραφής:</span>
{[{ val: null, label: 'Α', bg: newForm.color || '#6b7280', text: '#ffffff' }, { val: '#000000', label: 'Α', bg: newForm.color || '#6b7280', text: '#000000' }].map(opt => (
<button key={opt.label + opt.text} onClick={() => 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}</button>
))}
</div>
<button onClick={() => 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' }}>Αποθήκευση</button>
<button onClick={() => setShowNew(false)} style={{ height: 36, padding: '0 14px', borderRadius: 8, border: '1px solid #dfe2e6', background: 'white', fontSize: 13, cursor: 'pointer' }}>Άκυρο</button>
</div>
)}
{isLoading && <p style={{ padding: '16px 20px', color: '#9ca3af', fontSize: 13 }}>Φόρτωση</p>}
{!isLoading && flags.length === 0 && (
<p style={{ padding: '24px 20px', textAlign: 'center', color: '#b8bdc4', fontSize: 13 }}>Δεν υπάρχουν σημαίες ακόμα.</p>
)}
{flags.map(flag => (
<div key={flag.id} style={{ ...rowStyle, opacity: flag.is_active ? 1 : 0.45 }}>
{editingId === flag.id ? (
<div style={{ display: 'flex', flex: 1, flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
<EmojiPicker value={editForm.emoji} onChange={v => setEditForm(f => ({ ...f, emoji: v }))} />
<input value={editForm.name} onChange={e => 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' }} />
<div style={{ display: 'flex', gap: 3 }}>
{FLAG_COLORS.map(c => (
<button key={c} onClick={() => 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' }} />
))}
</div>
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
{[{ val: null, text: '#ffffff' }, { val: '#000000', text: '#000000' }].map(opt => (
<button key={opt.text} onClick={() => 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' }}>Α</button>
))}
</div>
<button onClick={() => 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' }}></button>
<button onClick={() => setEditingId(null)}
style={{ height: 32, padding: '0 10px', borderRadius: 6, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, cursor: 'pointer' }}></button>
</div>
) : (
<>
<div style={{ width: 32, height: 32, borderRadius: '50%', background: flag.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, flexShrink: 0 }}>
{flag.emoji || '🏷️'}
</div>
<span style={{ flex: 1, fontSize: 14, fontWeight: 500, color: '#111315' }}>{flag.name}</span>
{!flag.is_active && <span style={{ fontSize: 11, color: '#9ca3af', fontStyle: 'italic' }}>Ανενεργή</span>}
<button onClick={() => startEdit(flag)} style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, cursor: 'pointer', color: '#374151' }}>Επεξεργασία</button>
{flag.is_active && (
<button onClick={() => deleteMut.mutate(flag.id)} style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #fee2e2', background: '#fff5f5', fontSize: 12, cursor: 'pointer', color: '#dc2626' }}>Διαγραφή</button>
)}
</>
)}
</div>
))}
</div>
)
}
// ─── 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 (
<div className="card divide-y divide-gray-100">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px' }}>
<div>
<h2 className="font-semibold text-gray-700">Γρήγορα Μηνύματα</h2>
<p className="text-xs text-gray-400 mt-0.5">Πρότυπα μηνυμάτων για γρήγορη αποστολή στο προσωπικό</p>
</div>
<button onClick={() => 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',
}}>+ Νέο</button>
</div>
{showNew && (
<div style={{ padding: '14px 20px', background: '#f9fafb', display: 'flex', gap: 10, alignItems: 'center' }}>
<input placeholder="Κείμενο μηνύματος…" value={newBody} onChange={e => setNewBody(e.target.value)}
style={{ flex: 1, height: 36, borderRadius: 8, border: '1px solid #dfe2e6', padding: '0 12px', fontSize: 13, fontFamily: 'inherit' }} />
<button onClick={() => 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' }}>Αποθήκευση</button>
<button onClick={() => setShowNew(false)} style={{ height: 36, padding: '0 14px', borderRadius: 8, border: '1px solid #dfe2e6', background: 'white', fontSize: 13, cursor: 'pointer' }}>Άκυρο</button>
</div>
)}
{isLoading && <p style={{ padding: '16px 20px', color: '#9ca3af', fontSize: 13 }}>Φόρτωση</p>}
{!isLoading && templates.length === 0 && (
<p style={{ padding: '24px 20px', textAlign: 'center', color: '#b8bdc4', fontSize: 13 }}>Δεν υπάρχουν πρότυπα ακόμα.</p>
)}
{templates.map((t, idx) => (
<div key={t.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 20px', borderBottom: '1px solid #f4f4f2' }}>
<span style={{ width: 22, fontSize: 12, color: '#9ca3af', fontWeight: 600, flexShrink: 0 }}>{idx + 1}.</span>
{editingId === t.id ? (
<>
<input value={editBody} onChange={e => setEditBody(e.target.value)}
style={{ flex: 1, height: 32, borderRadius: 6, border: '1px solid #dfe2e6', padding: '0 10px', fontSize: 13, fontFamily: 'inherit' }} />
<button onClick={() => 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' }}></button>
<button onClick={() => setEditingId(null)}
style={{ height: 32, padding: '0 10px', borderRadius: 6, border: '1px solid #dfe2e6', background: 'white', fontSize: 12, cursor: 'pointer' }}></button>
</>
) : (
<>
<span style={{ flex: 1, fontSize: 14, color: '#111315' }}>{t.body}</span>
<button onClick={() => { 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' }}>Επεξεργασία</button>
<button onClick={() => deleteMut.mutate(t.id)}
style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #fee2e2', background: '#fff5f5', fontSize: 12, cursor: 'pointer', color: '#dc2626' }}>Διαγραφή</button>
</>
)}
</div>
))}
</div>
)
}
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 <div className="flex items-center justify-center h-64 text-gray-400">Φόρτωση</div>
return (
<div className="space-y-6">
{/* System info */}
<div className="card p-5 space-y-3">
<h2 className="font-semibold text-gray-700">Σύστημα</h2>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="text-gray-500">Uptime</div>
<div className="font-medium text-gray-800">{formatUptime(status?.uptime_seconds ?? 0)}</div>
<div className="text-gray-500">Άδεια χρήσης</div>
<div className={`font-medium ${status?.licensed ? 'text-green-700' : 'text-red-600'}`}>
{status?.licensed ? 'Ενεργή' : 'Ανενεργή'}
</div>
<div className="text-gray-500">Κατάσταση</div>
<div className={`font-medium ${status?.locked ? 'text-red-600' : 'text-green-700'}`}>
{status?.locked ? 'Κλειδωμένο' : 'Λειτουργικό'}
</div>
{status?.expires_at && (
<>
<div className="text-gray-500">Λήξη άδειας</div>
<div className="font-medium text-gray-800">{new Date(status.expires_at).toLocaleDateString('el-GR')}</div>
</>
)}
</div>
</div>
{/* Printers */}
<div className="card divide-y divide-gray-100">
<div className="px-5 py-4">
<h2 className="font-semibold text-gray-700">Εκτυπωτές</h2>
</div>
{(!status?.printers || status.printers.length === 0) && (
<p className="px-5 py-6 text-center text-gray-400 text-sm">Δεν βρέθηκαν εκτυπωτές.</p>
)}
{status?.printers?.map(p => (
<div key={p.id} className="flex items-center gap-4 px-5 py-3">
<div className="flex-1">
<p className="font-medium text-gray-800">{p.name}</p>
</div>
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${p.reachable ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-600'}`}>
{p.reachable ? 'Προσβάσιμος' : 'Μη προσβάσιμος'}
</span>
<button onClick={() => 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
</button>
</div>
))}
</div>
<ShiftSettingsSection />
<AutoLockSection />
<TimezoneSection />
<FlagDefsSection />
<QuickTemplatesSection />
{user?.role === 'sysadmin' && (
<div className="card p-5 space-y-3 border-amber-200 bg-amber-50">
<h2 className="font-semibold text-amber-800">Sysadmin</h2>
<p className="text-sm text-amber-700">Έλεγχος κλειδώματος συστήματος.</p>
<div className="flex gap-3">
<button onClick={() => client.post('/api/system/unlock').then(() => { toast.success('Ξεκλειδώθηκε'); qc.invalidateQueries({ queryKey: ['system-status'] }) })}
className="btn btn-primary text-sm">Ξεκλείδωμα</button>
<button onClick={() => client.post('/api/system/lock').then(() => { toast.success('Κλειδώθηκε'); qc.invalidateQueries({ queryKey: ['system-status'] }) })}
className="btn btn-danger text-sm">Κλείδωμα</button>
</div>
</div>
)}
</div>
)
}