general fixes and ordering display overhaul
This commit is contained in:
@@ -766,6 +766,7 @@ function buildFormFromProduct(product) {
|
||||
sort_order: q.sort_order ?? 0,
|
||||
is_favorite: q.is_favorite ?? false,
|
||||
favorite_sort_order: q.favorite_sort_order ?? 0,
|
||||
is_compact: q.is_compact ?? false,
|
||||
})) ?? [],
|
||||
options: product.options?.map(o => ({
|
||||
name: o.name,
|
||||
@@ -906,7 +907,7 @@ function ProductFormModal({ product, categories, printers, onSave, onCopy, onClo
|
||||
}
|
||||
|
||||
// ── Quick Options ──
|
||||
function addQuickOption() { setForm(f => ({ ...f, quick_options: [...f.quick_options, { name: '', price: 0, allow_multiple: false, sort_order: f.quick_options.length, is_favorite: false, favorite_sort_order: 0 }] })) }
|
||||
function addQuickOption() { setForm(f => ({ ...f, quick_options: [...f.quick_options, { name: '', price: 0, allow_multiple: false, sort_order: f.quick_options.length, is_favorite: false, favorite_sort_order: 0, is_compact: false }] })) }
|
||||
function removeQuickOption(i) { setForm(f => ({ ...f, quick_options: f.quick_options.filter((_, idx) => idx !== i) })) }
|
||||
function setQuickOption(i, k, v) { setForm(f => ({ ...f, quick_options: f.quick_options.map((q, idx) => idx === i ? { ...q, [k]: v } : q) })) }
|
||||
function moveQuickOption(i, dir) { setForm(f => ({ ...f, quick_options: moveItem(f.quick_options, i, dir) })) }
|
||||
@@ -1083,6 +1084,7 @@ function ProductFormModal({ product, categories, printers, onSave, onCopy, onClo
|
||||
sort_order: i,
|
||||
is_favorite: q.is_favorite ?? false,
|
||||
favorite_sort_order: q.favorite_sort_order ?? 0,
|
||||
is_compact: q.is_compact ?? false,
|
||||
})),
|
||||
options: form.options.map(o => ({
|
||||
name: o.name,
|
||||
@@ -1346,6 +1348,12 @@ function ProductFormModal({ product, categories, printers, onSave, onCopy, onClo
|
||||
className="accent-primary-700 w-4 h-4" />
|
||||
Πολλαπλά
|
||||
</label>
|
||||
<label title="Μισό πλάτος στο PWA" className="flex items-center gap-1.5 text-sm cursor-pointer shrink-0 select-none" style={{ color: q.is_compact ? '#7c3aed' : '#6b7280' }}>
|
||||
<input type="checkbox" checked={q.is_compact ?? false}
|
||||
onChange={e => setQuickOption(i, 'is_compact', e.target.checked)}
|
||||
className="w-4 h-4" style={{ accentColor: '#7c3aed' }} />
|
||||
Compact
|
||||
</label>
|
||||
<button onClick={() => removeQuickOption(i)} className="btn btn-danger px-3 min-h-0 h-10">✕</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -2,10 +2,12 @@ import { useState } from 'react'
|
||||
import AppInfoTab from './tabs/AppInfoTab'
|
||||
import ColoursTab from './tabs/ColoursTab'
|
||||
import DevelopmentTab from './tabs/DevelopmentTab'
|
||||
import PrintFontsTab from './tabs/PrintFontsTab'
|
||||
|
||||
const TABS = [
|
||||
{ key: 'app-info', label: 'App Info' },
|
||||
{ key: 'colours', label: 'UI Personalization' },
|
||||
{ key: 'print-fonts', label: 'Εκτύπωση' },
|
||||
{ key: 'development', label: 'Development' },
|
||||
]
|
||||
|
||||
@@ -48,6 +50,7 @@ export default function SettingsPage() {
|
||||
{/* Tab content */}
|
||||
{activeTab === 'app-info' && <AppInfoTab />}
|
||||
{activeTab === 'colours' && <ColoursTab />}
|
||||
{activeTab === 'print-fonts' && <PrintFontsTab />}
|
||||
{activeTab === 'development' && <DevelopmentTab />}
|
||||
</div>
|
||||
)
|
||||
|
||||
493
manager_dashboard/src/pages/Settings/tabs/PrintFontsTab.jsx
Normal file
493
manager_dashboard/src/pages/Settings/tabs/PrintFontsTab.jsx
Normal file
@@ -0,0 +1,493 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
import client from '../../../api/client'
|
||||
|
||||
// ── Font option definitions ────────────────────────────────────────────────
|
||||
// Value encodes: "ESC_BANG_BYTE:BOLD" where BOLD is 0 or 1
|
||||
// ESC ! correct bit map for TP850UE:
|
||||
// bit3 (0x08) = bold
|
||||
// bit4 (0x10) = double-height
|
||||
// bit5 (0x20) = double-width
|
||||
const FONT_SIZE_OPTIONS = [
|
||||
{ size: '0', label: 'Μικρά' },
|
||||
{ size: '16', label: 'Ψηλά' },
|
||||
{ size: '32', label: 'Πλατιά' },
|
||||
{ size: '48', label: 'Ψηλά και Πλατιά' },
|
||||
]
|
||||
|
||||
// We store the value as "SIZE:BOLD" e.g. "16:1" or "0:0"
|
||||
function encodeFont(size, bold) { return `${size}:${bold ? '1' : '0'}` }
|
||||
function decodeFont(val) {
|
||||
if (!val) return { size: '0', bold: false }
|
||||
const [size, bold] = val.split(':')
|
||||
return { size: size ?? '0', bold: bold === '1' }
|
||||
}
|
||||
|
||||
const DIVIDER_OPTIONS = [
|
||||
{ value: 'dash', label: 'Παύλες ( - )', chars: '-------------------' },
|
||||
{ value: 'equals', label: 'Ίσον ( = )', chars: '===================' },
|
||||
{ value: 'star', label: 'Αστερίσκοι ( * )', chars: '*******************' },
|
||||
{ value: 'empty', label: 'Κενή γραμμή', chars: '' },
|
||||
]
|
||||
|
||||
const FONT_FIELDS = [
|
||||
{ key: 'print.font_item_name', label: 'Όνομα Αντικειμένου', sub: 'Κάθε πιάτο/ποτό στο ticket κουζίνας' },
|
||||
{ key: 'print.font_options', label: 'Επιλογές / Τροποποιητές', sub: 'Extras, αφαιρέσεις, σημειώσεις' },
|
||||
{ key: 'print.font_table', label: 'Τραπέζι & Σερβιτόρος', sub: 'Αριθμός τραπεζιού, όνομα σερβιτόρου, ώρα' },
|
||||
{ key: 'print.font_order_number', label: 'Αριθμός Παραγγελίας', sub: 'Η επικεφαλίδα "Παραγγελία #Χ"' },
|
||||
{ key: 'print.font_header', label: 'Κεφαλίδα / Τίτλος', sub: 'Τίτλοι ενοτήτων, κεφαλίδες αναφορών' },
|
||||
]
|
||||
|
||||
const FONT_DEFAULTS = {
|
||||
'print.font_item_name': '16:0',
|
||||
'print.font_options': '0:0',
|
||||
'print.font_table': '16:0',
|
||||
'print.font_order_number': '48:1',
|
||||
'print.font_header': '48:1',
|
||||
'print.divider_style': 'dash',
|
||||
}
|
||||
|
||||
// ── Preview box ────────────────────────────────────────────────────────────
|
||||
// Fixed height tall enough for the largest option (Ψηλά και Πλατιά).
|
||||
// All rows share the same height so columns stay aligned.
|
||||
const PREVIEW_W = 200
|
||||
const PREVIEW_H = 50
|
||||
|
||||
const sizeStyle = {
|
||||
'0': { fontSize: 13, scaleY: 1, scaleX: 1 },
|
||||
'16': { fontSize: 13, scaleY: 1.9, scaleX: 1 },
|
||||
'32': { fontSize: 13, scaleY: 1, scaleX: 1.9 },
|
||||
'48': { fontSize: 13, scaleY: 1.9, scaleX: 1.9 },
|
||||
}
|
||||
|
||||
function FontPreview({ size, bold }) {
|
||||
const s = sizeStyle[size] ?? sizeStyle['0']
|
||||
return (
|
||||
<div style={{
|
||||
background: '#1a1a1a', borderRadius: 8,
|
||||
width: PREVIEW_W, height: PREVIEW_H, flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<span style={{
|
||||
color: '#f5f5f5',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
fontSize: s.fontSize,
|
||||
fontWeight: bold ? 800 : 400,
|
||||
transform: `scaleX(${s.scaleX}) scaleY(${s.scaleY})`,
|
||||
transformOrigin: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'block',
|
||||
}}>
|
||||
SAMPLE
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Single font row ────────────────────────────────────────────────────────
|
||||
function FontRow({ field, value, onChange, isPending }) {
|
||||
const { size, bold } = decodeFont(value)
|
||||
|
||||
function handleSize(e) { onChange(field.key, encodeFont(e.target.value, bold)) }
|
||||
function handleBold() { onChange(field.key, encodeFont(size, !bold)) }
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
padding: '14px 20px', borderBottom: '1px solid #f4f4f2',
|
||||
}}>
|
||||
{/* Label */}
|
||||
<div style={{ flex: '1 1 160px', minWidth: 140 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: '#111315', display: 'block', marginBottom: 2 }}>
|
||||
{field.label}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: '#9ca3af' }}>{field.sub}</span>
|
||||
</div>
|
||||
|
||||
{/* Size dropdown */}
|
||||
<select
|
||||
value={size}
|
||||
onChange={handleSize}
|
||||
disabled={isPending}
|
||||
style={{
|
||||
height: 36, borderRadius: 8, border: '1px solid #dfe2e6',
|
||||
background: 'white', padding: '0 10px', fontSize: 13,
|
||||
color: '#111315', cursor: 'pointer', width: 160, flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{FONT_SIZE_OPTIONS.map(o => (
|
||||
<option key={o.size} value={o.size}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Bold toggle */}
|
||||
<button
|
||||
onClick={handleBold}
|
||||
disabled={isPending}
|
||||
style={{
|
||||
height: 36, padding: '0 14px', borderRadius: 8, flexShrink: 0,
|
||||
border: `1.5px solid ${bold ? '#3758c9' : '#dfe2e6'}`,
|
||||
background: bold ? '#eff3ff' : 'white',
|
||||
color: bold ? '#3758c9' : '#6b7280',
|
||||
fontSize: 13, fontWeight: 700, cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 16, height: 16, borderRadius: 4, flexShrink: 0,
|
||||
border: `2px solid ${bold ? '#3758c9' : '#9ca3af'}`,
|
||||
background: bold ? '#3758c9' : 'white',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{bold && <span style={{ color: 'white', fontSize: 10, lineHeight: 1 }}>✓</span>}
|
||||
</span>
|
||||
ΕΝΤΟΝΑ
|
||||
</button>
|
||||
|
||||
{/* Preview */}
|
||||
<FontPreview size={size} bold={bold} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Divider row ────────────────────────────────────────────────────────────
|
||||
function DividerRow({ value, onChange, isPending }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
padding: '14px 20px',
|
||||
}}>
|
||||
<div style={{ flex: '1 1 160px', minWidth: 140 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: '#111315', display: 'block', marginBottom: 2 }}>
|
||||
Στυλ Διαχωριστικού
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: '#9ca3af' }}>Ανάμεσα στις ενότητες κάθε ticket</span>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={value}
|
||||
onChange={e => onChange('print.divider_style', e.target.value)}
|
||||
disabled={isPending}
|
||||
style={{
|
||||
height: 36, borderRadius: 8, border: '1px solid #dfe2e6',
|
||||
background: 'white', padding: '0 10px', fontSize: 13,
|
||||
color: '#111315', cursor: 'pointer', width: 160, flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{DIVIDER_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* spacer to align with bold button column */}
|
||||
<div style={{ width: 87, flexShrink: 0 }} />
|
||||
|
||||
{/* Preview — same fixed size as font previews */}
|
||||
<div style={{
|
||||
background: '#1a1a1a', borderRadius: 8,
|
||||
width: PREVIEW_W, height: PREVIEW_H, flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{value === 'empty'
|
||||
? <span style={{ color: '#6b7280', fontSize: 12, fontFamily: 'Arial, Helvetica, sans-serif' }}>(κενή γραμμή)</span>
|
||||
: <span style={{ color: '#f5f5f5', fontSize: 12, fontFamily: 'Arial, Helvetica, sans-serif', letterSpacing: 2 }}>
|
||||
{DIVIDER_OPTIONS.find(o => o.value === value)?.chars}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Printers section ───────────────────────────────────────────────────────
|
||||
|
||||
const PROTOCOLS = [{ value: 'escpos_tcp', label: 'ESC/POS TCP (standard)' }]
|
||||
|
||||
const EMPTY_FORM = { name: '', ip_address: '', port: 9100, protocol: 'escpos_tcp', is_active: true }
|
||||
|
||||
function PrinterForm({ initial, onSave, onCancel, isPending }) {
|
||||
const [form, setForm] = useState(initial ?? EMPTY_FORM)
|
||||
function set(k, v) { setForm(f => ({ ...f, [k]: v })) }
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#f9fafb', border: '1px solid #e5e7eb', borderRadius: 10,
|
||||
padding: '16px 20px', display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'flex-end',
|
||||
}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: '2 1 160px' }}>
|
||||
<label style={{ fontSize: 11, fontWeight: 600, color: '#6b7280' }}>ΟΝΟΜΑ</label>
|
||||
<input value={form.name} onChange={e => set('name', e.target.value)}
|
||||
placeholder="π.χ. Κουζίνα" style={inputStyle} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: '2 1 130px' }}>
|
||||
<label style={{ fontSize: 11, fontWeight: 600, color: '#6b7280' }}>IP ADDRESS</label>
|
||||
<input value={form.ip_address} onChange={e => set('ip_address', e.target.value)}
|
||||
placeholder="10.98.20.25" style={inputStyle} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: '0 0 80px' }}>
|
||||
<label style={{ fontSize: 11, fontWeight: 600, color: '#6b7280' }}>PORT</label>
|
||||
<input value={form.port} onChange={e => set('port', parseInt(e.target.value) || 9100)}
|
||||
type="number" style={inputStyle} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: '1 1 160px' }}>
|
||||
<label style={{ fontSize: 11, fontWeight: 600, color: '#6b7280' }}>ΠΡΩΤΟΚΟΛΛΟ</label>
|
||||
<select value={form.protocol} onChange={e => set('protocol', e.target.value)} style={inputStyle}>
|
||||
{PROTOCOLS.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', paddingBottom: 2 }}>
|
||||
<button onClick={() => onSave(form)} disabled={isPending || !form.name.trim() || !form.ip_address.trim()}
|
||||
style={btnPrimary}>Αποθήκευση</button>
|
||||
<button onClick={onCancel} style={btnSecondary}>Άκυρο</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inputStyle = {
|
||||
height: 36, borderRadius: 8, border: '1px solid #dfe2e6', background: 'white',
|
||||
padding: '0 10px', fontSize: 13, color: '#111315', fontFamily: 'inherit', width: '100%',
|
||||
}
|
||||
const btnPrimary = {
|
||||
height: 36, padding: '0 16px', borderRadius: 8, background: '#3758c9', color: 'white',
|
||||
border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer',
|
||||
}
|
||||
const btnSecondary = {
|
||||
height: 36, padding: '0 14px', borderRadius: 8, border: '1px solid #dfe2e6',
|
||||
background: 'white', fontSize: 13, cursor: 'pointer', color: '#374151',
|
||||
}
|
||||
const btnDanger = {
|
||||
height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #fee2e2',
|
||||
background: '#fff5f5', fontSize: 12, cursor: 'pointer', color: '#dc2626',
|
||||
}
|
||||
|
||||
function PrinterRow({ printer, onEdit, onDelete, onTest, onToggle, testPending }) {
|
||||
const [reachable, setReachable] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
client.get('/api/system/status').then(r => {
|
||||
if (cancelled) return
|
||||
const match = r.data.printers?.find(p => p.id === printer.id)
|
||||
if (match) setReachable(match.reachable)
|
||||
}).catch(() => {})
|
||||
return () => { cancelled = true }
|
||||
}, [printer.id])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
padding: '12px 20px', borderBottom: '1px solid #f4f4f2',
|
||||
opacity: printer.is_active ? 1 : 0.5,
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
{/* Enable/disable toggle */}
|
||||
<button onClick={() => onToggle(printer)} title={printer.is_active ? 'Απενεργοποίηση' : 'Ενεργοποίηση'}
|
||||
style={{
|
||||
width: 40, height: 22, borderRadius: 999, border: 'none', cursor: 'pointer', flexShrink: 0,
|
||||
background: printer.is_active ? '#16a34a' : '#d1d5db', position: 'relative', transition: 'background 150ms',
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute', top: 3, left: printer.is_active ? 21 : 3,
|
||||
width: 16, height: 16, borderRadius: '50%', background: 'white',
|
||||
transition: 'left 150ms', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
}} />
|
||||
</button>
|
||||
|
||||
{/* Name + connection info */}
|
||||
<div style={{ flex: 1, minWidth: 120 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: '#111315' }}>{printer.name}</span>
|
||||
<span style={{ fontSize: 11, color: '#9ca3af', marginLeft: 8 }}>
|
||||
{printer.ip_address}:{printer.port}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: '#9ca3af', marginLeft: 6 }}>— {printer.protocol}</span>
|
||||
</div>
|
||||
|
||||
{/* Reachability badge */}
|
||||
<span style={{
|
||||
fontSize: 11, fontWeight: 700, padding: '2px 8px', borderRadius: 99, flexShrink: 0,
|
||||
background: reachable === null ? '#f3f4f6' : reachable ? '#dcfce7' : '#fee2e2',
|
||||
color: reachable === null ? '#9ca3af' : reachable ? '#16a34a' : '#dc2626',
|
||||
}}>
|
||||
{reachable === null ? 'Έλεγχος…' : reachable ? 'Προσβάσιμος' : 'Μη προσβάσιμος'}
|
||||
</span>
|
||||
|
||||
{/* Actions */}
|
||||
<button onClick={() => onTest(printer.id)} disabled={testPending}
|
||||
style={{ ...btnSecondary, height: 28, padding: '0 10px', fontSize: 12, flexShrink: 0 }}>
|
||||
Test Print
|
||||
</button>
|
||||
<button onClick={() => onEdit(printer)}
|
||||
style={{ ...btnSecondary, height: 28, padding: '0 10px', fontSize: 12, flexShrink: 0 }}>
|
||||
Επεξεργασία
|
||||
</button>
|
||||
<button onClick={() => onDelete(printer.id)} style={{ ...btnDanger, flexShrink: 0 }}>
|
||||
Διαγραφή
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PrintersSection() {
|
||||
const qc = useQueryClient()
|
||||
const [showNew, setShowNew] = useState(false)
|
||||
const [editingId, setEditingId] = useState(null)
|
||||
|
||||
const { data: printers = [], isLoading } = useQuery({
|
||||
queryKey: ['printers-all'],
|
||||
queryFn: () => client.get('/api/system/printers').then(r => r.data),
|
||||
staleTime: 15_000,
|
||||
})
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: body => client.post('/api/system/printers', body),
|
||||
onSuccess: () => { toast.success('Εκτυπωτής προστέθηκε'); qc.invalidateQueries({ queryKey: ['printers-all'] }); setShowNew(false) },
|
||||
onError: () => toast.error('Σφάλμα δημιουργίας'),
|
||||
})
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ id, ...body }) => client.put(`/api/system/printers/${id}`, body),
|
||||
onSuccess: () => { toast.success('Αποθηκεύτηκε'); qc.invalidateQueries({ queryKey: ['printers-all'] }); setEditingId(null) },
|
||||
onError: () => toast.error('Σφάλμα αποθήκευσης'),
|
||||
})
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: id => client.delete(`/api/system/printers/${id}`),
|
||||
onSuccess: () => { toast.success('Διαγράφηκε'); qc.invalidateQueries({ queryKey: ['printers-all'] }) },
|
||||
onError: () => toast.error('Σφάλμα διαγραφής'),
|
||||
})
|
||||
const testMut = useMutation({
|
||||
mutationFn: id => client.post(`/api/system/printers/test?printer_id=${id}`),
|
||||
onSuccess: res => res.data.success ? toast.success('Test print στάλθηκε!') : toast.error(`Σφάλμα: ${res.data.error}`),
|
||||
onError: () => toast.error('Σφάλμα επικοινωνίας'),
|
||||
})
|
||||
|
||||
function handleToggle(printer) {
|
||||
updateMut.mutate({ id: printer.id, is_active: !printer.is_active })
|
||||
}
|
||||
|
||||
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); setEditingId(null) }} style={btnSecondary}>
|
||||
+ Νέος εκτυπωτής
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showNew && (
|
||||
<div style={{ padding: '12px 20px' }}>
|
||||
<PrinterForm
|
||||
onSave={form => createMut.mutate(form)}
|
||||
onCancel={() => setShowNew(false)}
|
||||
isPending={createMut.isPending}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && <p style={{ padding: '16px 20px', color: '#9ca3af', fontSize: 13 }}>Φόρτωση…</p>}
|
||||
{!isLoading && printers.length === 0 && !showNew && (
|
||||
<p style={{ padding: '24px 20px', textAlign: 'center', color: '#b8bdc4', fontSize: 13 }}>
|
||||
Δεν υπάρχουν εκτυπωτές ακόμα.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{printers.map(printer => (
|
||||
editingId === printer.id ? (
|
||||
<div key={printer.id} style={{ padding: '12px 20px', borderBottom: '1px solid #f4f4f2' }}>
|
||||
<PrinterForm
|
||||
initial={printer}
|
||||
onSave={form => updateMut.mutate({ id: printer.id, ...form })}
|
||||
onCancel={() => setEditingId(null)}
|
||||
isPending={updateMut.isPending}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<PrinterRow
|
||||
key={printer.id}
|
||||
printer={printer}
|
||||
onEdit={p => { setEditingId(p.id); setShowNew(false) }}
|
||||
onDelete={id => deleteMut.mutate(id)}
|
||||
onTest={id => testMut.mutate(id)}
|
||||
onToggle={handleToggle}
|
||||
testPending={testMut.isPending}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main tab ───────────────────────────────────────────────────────────────
|
||||
export default function PrintFontsTab() {
|
||||
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 val(key) { return settings?.[key]?.value ?? FONT_DEFAULTS[key] }
|
||||
function handleChange(key, value) { updateMut.mutate({ key, value }) }
|
||||
|
||||
if (isLoading) {
|
||||
return <div style={{ padding: 40, textAlign: 'center', color: '#9ca3af', fontSize: 14 }}>Φόρτωση…</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
|
||||
<PrintersSection />
|
||||
|
||||
{/* Font sizes card */}
|
||||
<div className="card divide-y divide-gray-100">
|
||||
<div style={{ padding: '16px 20px' }}>
|
||||
<h2 className="font-semibold text-gray-700">Μεγέθη Γραμματοσειράς</h2>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
Οι αλλαγές εφαρμόζονται αμέσως στην επόμενη εκτύπωση.
|
||||
</p>
|
||||
</div>
|
||||
{FONT_FIELDS.map(field => (
|
||||
<FontRow
|
||||
key={field.key}
|
||||
field={field}
|
||||
value={val(field.key)}
|
||||
onChange={handleChange}
|
||||
isPending={updateMut.isPending}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider style card */}
|
||||
<div className="card divide-y divide-gray-100">
|
||||
<div style={{ padding: '16px 20px' }}>
|
||||
<h2 className="font-semibold text-gray-700">Διαχωριστικές Γραμμές</h2>
|
||||
</div>
|
||||
<DividerRow
|
||||
value={val('print.divider_style')}
|
||||
onChange={handleChange}
|
||||
isPending={updateMut.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: '#fffbeb', border: '1px solid #fde68a', borderRadius: 10,
|
||||
padding: '12px 16px', fontSize: 12, color: '#92400e', lineHeight: 1.6,
|
||||
}}>
|
||||
<strong>Σημείωση:</strong> Το "Πλατιά" και "Ψηλά και Πλατιά" χωράνε ~24 χαρακτήρες ανά γραμμή αντί για 48.
|
||||
Χρησιμοποιήστε τα μόνο για σύντομα κείμενα (αριθμοί παραγγελίας, επικεφαλίδες).
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user