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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user