feat: bump client-services (accumulated feature work + deploy fixes)

Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 10:00:14 +03:00
parent 02ec1aa28f
commit 34ae328b0d
182 changed files with 34874 additions and 3556 deletions

View File

@@ -6,18 +6,29 @@ import toast from 'react-hot-toast'
// ─── Colour slot metadata ────────────────────────────────────────────────────
const SLOTS = [
{ key: 'cardBg', label: 'Κύριο Φόντο', hint: 'Φόντο κάρτας' },
{ key: 'badgeBg', label: 'Δευτερεύον Φόντο', hint: 'Φόντο badge κατάστασης' },
{ key: 'nameText', label: 'Κύριο Κείμενο', hint: 'Όνομα τραπεζιού' },
{ key: 'badgeText', label: 'Δευτερεύον Κείμενο', hint: 'Ετικέτα badge' },
{ key: 'cardBg', key2: 'cardBg2', label: 'Κύριο Φόντο', hint: 'Φόντο κάρτας' },
{ key: 'badgeBg', key2: 'badgeBg2', label: 'Δευτερεύον Φόντο', hint: 'Φόντο badge κατάστασης' },
{ key: 'nameText', key2: 'nameText2', label: 'Κύριο Κείμενο', hint: 'Όνομα τραπεζιού' },
{ key: 'badgeText', key2: 'badgeText2', label: 'Δευτερεύον Κείμενο', hint: 'Ετικέτα badge' },
]
function useFlashToggle(active, ms = 700) {
const [frame, setFrame] = useState(false)
useEffect(() => {
if (!active) { setFrame(false); return }
const id = setInterval(() => setFrame(f => !f), ms)
return () => clearInterval(id)
}, [active, ms])
return frame
}
const STATUSES = [
{ key: 'free', label: 'Ελεύθερο' },
{ key: 'open', label: 'Ανοιχτό (όχι δικό μου)' },
{ key: 'mine', label: 'Ανοιχτό (δικό μου)' },
{ key: 'partially_paid', label: 'Μερικώς Πληρωμένο' },
{ key: 'paid', label: 'Πληρωμένο' },
{ key: 'kds_ready', label: 'Έτοιμο για Παράδοση (KDS)', hasFlash: true },
]
const STATUS_LABELS_MOCK = {
@@ -26,14 +37,19 @@ const STATUS_LABELS_MOCK = {
mine: 'ΔΙΚΟ ΜΟΥ',
partially_paid: 'ΜΕΡ. ΠΛHΡ.',
paid: 'ΠΛΗΡΩΜΕΝΟ',
kds_ready: 'READY',
}
// Quick-suggest palettes per slot type
const QUICK_SWATCHES = {
cardBg: ['#dde5ef', '#243044', '#FF8F60', '#e8610a', '#FFDC67', '#81D264', '#a78bfa', '#38bdf8', '#f43f5e', '#1e293b'],
badgeBg: ['rgba(255,255,255,0.92)', 'rgba(0,0,0,0.55)', 'rgba(255,255,255,0.6)', 'rgba(30,41,59,0.85)', '#ffffff', '#000000'],
nameText: ['#ffffff', '#1e293b', '#3d5270', '#94b8d4', '#f8fafc', '#111827', '#fef3c7', '#dcfce7'],
badgeText: ['#3d5270', '#94b8d4', '#e8610a', '#FF8F60', '#FFDC67', '#d4a800', '#81D264', '#ffffff', '#1e293b'],
cardBg: ['#dde5ef', '#243044', '#FF8F60', '#e8610a', '#FFDC67', '#81D264', '#a78bfa', '#38bdf8', '#f43f5e', '#1e293b'],
cardBg2: ['rgba(0,0,0,0)', '#ffffff', '#dde5ef', '#243044', '#FF8F60', '#e8610a', '#22c55e', '#3b82f6'],
badgeBg: ['rgba(255,255,255,0.92)', 'rgba(0,0,0,0.55)', 'rgba(255,255,255,0.6)', 'rgba(30,41,59,0.85)', '#ffffff', '#000000'],
badgeBg2: ['rgba(0,0,0,0)', 'rgba(255,255,255,0.92)', 'rgba(0,0,0,0.55)', '#ffffff', '#000000'],
nameText: ['#ffffff', '#1e293b', '#3d5270', '#94b8d4', '#f8fafc', '#111827', '#fef3c7', '#dcfce7'],
nameText2: ['rgba(0,0,0,0)', '#ffffff', '#1e293b', '#3d5270', '#94b8d4', '#15803d', '#bbf7d0'],
badgeText: ['#3d5270', '#94b8d4', '#e8610a', '#FF8F60', '#FFDC67', '#d4a800', '#81D264', '#ffffff', '#1e293b'],
badgeText2: ['rgba(0,0,0,0)', '#3d5270', '#94b8d4', '#15803d', '#bbf7d0', '#22c55e', '#ffffff'],
}
// ─── Color picker modal ──────────────────────────────────────────────────────
@@ -106,7 +122,9 @@ function ColourPickerModal({ value, onClose, onChange, slot }) {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
<div>
<div style={{ fontSize: 16, fontWeight: 700, color: '#111827' }}>Επιλογή Χρώματος</div>
<div style={{ fontSize: 12, color: '#6b7280', marginTop: 2 }}>{SLOTS.find(s => s.key === slot)?.label}</div>
<div style={{ fontSize: 12, color: '#6b7280', marginTop: 2 }}>
{SLOTS.find(s => s.key === slot)?.label || (SLOTS.find(s => s.key2 === slot)?.label + ' (χρώμα 2)')}
</div>
</div>
<button onClick={onClose} style={{ background: 'none', border: 'none', fontSize: 22, cursor: 'pointer', color: '#6b7280', lineHeight: 1 }}>×</button>
</div>
@@ -259,34 +277,43 @@ function ColourSlotRow({ mode, status, slotKey, label, value, onOpen }) {
// ─── Mini mock table card (for preview) ──────────────────────────────────────
function MockCard({ cfg, label, mockName, groupName = 'ΜΕΣΑ' }) {
function MockCard({ cfg, label, mockName, groupName = 'ΜΕΣΑ', flashFrame = false }) {
return (
<div style={{
width: '100%', height: 90, borderRadius: 12, background: cfg.cardBg,
width: '100%', height: 90, borderRadius: 12,
background: flashFrame ? (cfg.cardBg2 ?? cfg.cardBg) : cfg.cardBg,
position: 'relative', flexShrink: 0,
boxShadow: '0 2px 8px rgba(0,0,0,0.18)',
overflow: 'hidden',
transition: 'background 0.15s',
}}>
{/* Table name + group */}
<div style={{ position: 'absolute', top: 8, left: 10, display: 'flex', flexDirection: 'column', gap: 1 }}>
<span style={{
fontSize: 17, fontWeight: 800, color: cfg.nameText,
fontSize: 17, fontWeight: 800,
color: flashFrame ? (cfg.nameText2 ?? cfg.nameText) : cfg.nameText,
lineHeight: 1, letterSpacing: -0.5,
transition: 'color 0.15s',
}}>{mockName}</span>
<span style={{
fontSize: 7, fontWeight: 600, letterSpacing: 0.8,
color: cfg.nameText + '80',
color: (flashFrame ? (cfg.nameText2 ?? cfg.nameText) : cfg.nameText) + '80',
textTransform: 'uppercase',
}}>{groupName}</span>
</div>
{/* Status badge — tight equal padding on all sides */}
{/* Status badge */}
<div style={{
position: 'absolute', bottom: 7, left: 7,
background: cfg.badgeBg,
background: flashFrame ? (cfg.badgeBg2 ?? cfg.badgeBg) : cfg.badgeBg,
borderRadius: 4, padding: '2px 5px',
lineHeight: 1,
lineHeight: 1, transition: 'background 0.15s',
}}>
<span style={{ fontSize: 7, fontWeight: 700, color: cfg.badgeText, whiteSpace: 'nowrap', lineHeight: 1 }}>
<span style={{
fontSize: 7, fontWeight: 700,
color: flashFrame ? (cfg.badgeText2 ?? cfg.badgeText) : cfg.badgeText,
whiteSpace: 'nowrap', lineHeight: 1,
transition: 'color 0.15s',
}}>
{label}
</span>
</div>
@@ -336,30 +363,82 @@ function PreviewPanel({ colours, mode }) {
// ─── Status block (one status, showing all 4 slots) ──────────────────────────
function StatusBlock({ mode, status, label, colours, onOpen }) {
function StatusBlock({ mode, status, label, hasFlash, colours, onOpen, onSetFlash }) {
const cfg = colours[mode][status]
const flashFrame = useFlashToggle(hasFlash && !!cfg.flash)
return (
<div style={{ background: '#f9fafb', borderRadius: 12, padding: '14px 16px', border: '1px solid #f0f0f0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
<div style={{ width: 88, flexShrink: 0 }}>
<MockCard cfg={cfg} label={STATUS_LABELS_MOCK[status]} mockName="T1" />
<MockCard cfg={cfg} label={STATUS_LABELS_MOCK[status]} mockName="T1" flashFrame={flashFrame} />
</div>
<div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700, color: '#111827' }}>{label}</div>
<div style={{ fontSize: 11, color: '#9ca3af', marginTop: 2 }}>Πατήστε ένα χρώμα για επεξεργασία</div>
{hasFlash && (
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
<div
onClick={() => onSetFlash(mode, !cfg.flash)}
style={{
width: 36, height: 20, borderRadius: 10,
background: cfg.flash ? '#22c55e' : '#d1d5db',
position: 'relative', cursor: 'pointer', transition: 'background 0.2s',
flexShrink: 0,
}}
>
<div style={{
position: 'absolute', top: 2, left: cfg.flash ? 18 : 2,
width: 16, height: 16, borderRadius: '50%',
background: '#fff', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
transition: 'left 0.2s',
}} />
</div>
<span style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>
Αναλαμπή (Flashing)
</span>
</div>
)}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, borderTop: '1px solid #ebebeb', paddingTop: 8 }}>
{SLOTS.map(slot => (
<ColourSlotRow
key={slot.key}
mode={mode}
status={status}
slotKey={slot.key}
label={slot.label}
value={cfg[slot.key]}
onOpen={onOpen}
/>
<div key={slot.key}>
<ColourSlotRow
mode={mode}
status={status}
slotKey={slot.key}
label={slot.label}
value={cfg[slot.key]}
onOpen={onOpen}
/>
{hasFlash && cfg.flash && slot.key2 && (
<div style={{ paddingLeft: 20, marginTop: -4, marginBottom: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 12, height: 12, borderRadius: 3, background: cfg[slot.key2] || 'rgba(0,0,0,0)', border: '1px solid #e5e7eb', flexShrink: 0 }} />
<button
onClick={() => onOpen(mode, status, slot.key2, cfg[slot.key2] || cfg[slot.key])}
style={{
width: 36, height: 22, borderRadius: 6,
background: cfg[slot.key2] || 'rgba(0,0,0,0)',
border: '1.5px solid #e5e7eb', cursor: 'pointer', flexShrink: 0,
boxShadow: '0 1px 4px rgba(0,0,0,0.08)',
backgroundImage: !cfg[slot.key2] || cfg[slot.key2] === 'rgba(0,0,0,0)'
? 'linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%)'
: 'none',
backgroundSize: '6px 6px',
backgroundPosition: '0 0,0 3px,3px -3px,-3px 0',
}}
/>
<div>
<div style={{ fontSize: 11, color: '#6b7280' }}>{slot.label} (χρώμα 2)</div>
<div style={{ fontSize: 10, color: '#9ca3af', fontFamily: 'monospace' }}>
{cfg[slot.key2] || 'διάφανο'}
</div>
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>
@@ -368,7 +447,7 @@ function StatusBlock({ mode, status, label, colours, onOpen }) {
// ─── Mode section (light or dark) ────────────────────────────────────────────
function ModeSection({ mode, colours, onOpen }) {
function ModeSection({ mode, colours, onOpen, onSetFlash }) {
const label = mode === 'light' ? '☀️ Φωτεινό θέμα' : '🌙 Σκοτεινό θέμα'
return (
<div>
@@ -380,8 +459,10 @@ function ModeSection({ mode, colours, onOpen }) {
mode={mode}
status={s.key}
label={s.label}
hasFlash={s.hasFlash}
colours={colours}
onOpen={onOpen}
onSetFlash={onSetFlash}
/>
))}
</div>
@@ -402,7 +483,17 @@ export default function ColoursTab() {
client.get('/api/settings/').then(r => {
const raw = r.data?.['ui.table_colours']?.value
if (raw) {
try { setColours(JSON.parse(raw)) } catch {}
try {
const loaded = JSON.parse(raw)
// Deep-merge with defaults so any missing statuses/slots are backfilled
const merged = { light: {}, dark: {} }
for (const mode of ['light', 'dark']) {
for (const [status, defaults] of Object.entries(DEFAULT_COLOURS[mode])) {
merged[mode][status] = { ...defaults, ...(loaded[mode]?.[status] ?? {}) }
}
}
setColours(merged)
} catch {}
}
})
}, [])
@@ -432,6 +523,20 @@ export default function ColoursTab() {
})
}
function setFlash(mode, value) {
setColours(prev => {
const next = {
...prev,
[mode]: {
...prev[mode],
kds_ready: { ...prev[mode].kds_ready, flash: value },
},
}
saveToBackend(next)
return next
})
}
function openModal(mode, status, slot, value) {
setModal({ mode, status, slot, value })
}
@@ -461,8 +566,8 @@ export default function ColoursTab() {
{/* Light + Dark mode settings */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 32 }}>
<ModeSection mode="light" colours={colours} onOpen={openModal} />
<ModeSection mode="dark" colours={colours} onOpen={openModal} />
<ModeSection mode="light" colours={colours} onOpen={openModal} onSetFlash={setFlash} />
<ModeSection mode="dark" colours={colours} onOpen={openModal} onSetFlash={setFlash} />
</div>
</div>