feat: ShiftDetailModal rework, Today cancellation events, shift details icon button
- Backend: add cancellation_events (row count) to _enrich_shift and shift summary - Backend: add cancellation_events field to current business day endpoint - Today tab: hero number now shows cancellation_events (distinct cancel actions), sub-label shows 'X παραγγελίες / Y είδη' - ShiftsOverview: Λεπτομέρειες button is now icon-only (Eye, light blue) matching the delete button style - ShiftDetailModal: modal widened to 1080px; ΠΑΡΑΔΟΘΗΚΑΝ KPI replaced with Ακυρώσεις showing 'X events / Y items'; all filter labels renamed (Πλήρης Παραγγελία, Μόνο Πληρώθηκε, Μόνο Παρήγγειλε, Ανοιχτό ακόμη); new Ακυρώθηκε filter added; cancelled items shown in red, Πληρώθηκε line hidden for cancelled; legend updated; classify() handles cancelled status before other checks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1263,12 +1263,17 @@ def current_business_day(
|
|||||||
# Count cancelled items (individual items cancelled, across all orders in this day)
|
# Count cancelled items (individual items cancelled, across all orders in this day)
|
||||||
all_order_ids = [o.id for o in orders]
|
all_order_ids = [o.id for o in orders]
|
||||||
cancelled_items_qty = 0
|
cancelled_items_qty = 0
|
||||||
|
cancellation_events = 0
|
||||||
if all_order_ids:
|
if all_order_ids:
|
||||||
from sqlalchemy import func as sqlfunc
|
from sqlalchemy import func as sqlfunc
|
||||||
cancelled_items_qty = db.query(sqlfunc.sum(OrderItem.quantity)).filter(
|
cancelled_items_qty = db.query(sqlfunc.sum(OrderItem.quantity)).filter(
|
||||||
OrderItem.order_id.in_(all_order_ids),
|
OrderItem.order_id.in_(all_order_ids),
|
||||||
OrderItem.status == "cancelled",
|
OrderItem.status == "cancelled",
|
||||||
).scalar() or 0
|
).scalar() or 0
|
||||||
|
cancellation_events = db.query(sqlfunc.count(OrderItem.id)).filter(
|
||||||
|
OrderItem.order_id.in_(all_order_ids),
|
||||||
|
OrderItem.status == "cancelled",
|
||||||
|
).scalar() or 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"business_day": {
|
"business_day": {
|
||||||
@@ -1283,6 +1288,7 @@ def current_business_day(
|
|||||||
"active_waiters": len(active_shifts),
|
"active_waiters": len(active_shifts),
|
||||||
"cancellations": len(cancelled_orders),
|
"cancellations": len(cancelled_orders),
|
||||||
"cancelled_items": int(cancelled_items_qty),
|
"cancelled_items": int(cancelled_items_qty),
|
||||||
|
"cancellation_events": int(cancellation_events),
|
||||||
"top_product": top_product,
|
"top_product": top_product,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
|
|||||||
if shift.ended_at:
|
if shift.ended_at:
|
||||||
cancelled_q = cancelled_q.filter(OrderItem.added_at <= shift.ended_at)
|
cancelled_q = cancelled_q.filter(OrderItem.added_at <= shift.ended_at)
|
||||||
cancelled_rows = cancelled_q.all()
|
cancelled_rows = cancelled_q.all()
|
||||||
|
cancellation_events = len(cancelled_rows)
|
||||||
cancellations = sum(r.quantity for r in cancelled_rows)
|
cancellations = sum(r.quantity for r in cancelled_rows)
|
||||||
cancellation_value = round(sum(r.unit_price * r.quantity for r in cancelled_rows), 2)
|
cancellation_value = round(sum(r.unit_price * r.quantity for r in cancelled_rows), 2)
|
||||||
return {
|
return {
|
||||||
@@ -101,6 +102,7 @@ def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
|
|||||||
"hourly_rate_snapshot": shift.hourly_rate_snapshot,
|
"hourly_rate_snapshot": shift.hourly_rate_snapshot,
|
||||||
"duration_hours": pay_data["duration_hours"],
|
"duration_hours": pay_data["duration_hours"],
|
||||||
"shift_pay": pay_data["shift_pay"],
|
"shift_pay": pay_data["shift_pay"],
|
||||||
|
"cancellation_events": cancellation_events,
|
||||||
"cancellations": cancellations,
|
"cancellations": cancellations,
|
||||||
"cancellation_value": cancellation_value,
|
"cancellation_value": cancellation_value,
|
||||||
# Phase 2E
|
# Phase 2E
|
||||||
@@ -417,7 +419,7 @@ def get_shift_summary(
|
|||||||
|
|
||||||
waiter_id = shift.waiter_id
|
waiter_id = shift.waiter_id
|
||||||
|
|
||||||
# Collect all relevant items: paid in this shift OR added by this waiter
|
# Collect all relevant items: paid in this shift OR added by this waiter (any status)
|
||||||
paid_items = db.query(OrderItem).options(
|
paid_items = db.query(OrderItem).options(
|
||||||
joinedload(OrderItem.product),
|
joinedload(OrderItem.product),
|
||||||
joinedload(OrderItem.order),
|
joinedload(OrderItem.order),
|
||||||
@@ -446,6 +448,22 @@ def get_shift_summary(
|
|||||||
items_by_id[item.id] = item
|
items_by_id[item.id] = item
|
||||||
all_items = list(items_by_id.values())
|
all_items = list(items_by_id.values())
|
||||||
|
|
||||||
|
# Also include items cancelled within this shift window (may not be in ordered_items
|
||||||
|
# if they were cancelled after the shift ended or deduped out)
|
||||||
|
cancelled_items_for_summary = db.query(OrderItem).options(
|
||||||
|
joinedload(OrderItem.product),
|
||||||
|
joinedload(OrderItem.order),
|
||||||
|
).filter(
|
||||||
|
OrderItem.added_by == waiter_id,
|
||||||
|
OrderItem.status == "cancelled",
|
||||||
|
OrderItem.added_at >= shift.started_at,
|
||||||
|
OrderItem.added_at <= upper_bound,
|
||||||
|
).all()
|
||||||
|
for item in cancelled_items_for_summary:
|
||||||
|
if item.id not in items_by_id:
|
||||||
|
items_by_id[item.id] = item
|
||||||
|
all_items = list(items_by_id.values())
|
||||||
|
|
||||||
# Build lookup maps
|
# Build lookup maps
|
||||||
all_waiter_ids = set()
|
all_waiter_ids = set()
|
||||||
for item in all_items:
|
for item in all_items:
|
||||||
|
|||||||
@@ -121,8 +121,12 @@ export default function Today() {
|
|||||||
)}
|
)}
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Ακυρώσεις"
|
label="Ακυρώσεις"
|
||||||
value={fmtNum(bd.cancellations)}
|
value={fmtNum(bd.cancellation_events ?? bd.cancellations)}
|
||||||
sub={bd.cancelled_items > 0 ? `${fmtNum(bd.cancelled_items)} είδη` : 'παραγγελίες'}
|
sub={
|
||||||
|
(bd.cancellations > 0 || bd.cancelled_items > 0)
|
||||||
|
? `${fmtNum(bd.cancellations)} παραγγελίες / ${fmtNum(bd.cancelled_items)} είδη`
|
||||||
|
: 'καμία ακύρωση'
|
||||||
|
}
|
||||||
icon={XCircle}
|
icon={XCircle}
|
||||||
/>
|
/>
|
||||||
{bd.total_cost > 0 && (
|
{bd.total_cost > 0 && (
|
||||||
|
|||||||
@@ -5,30 +5,41 @@ import client from '../../../api/client'
|
|||||||
import { fmtEUR, fmtDateTime, fmtTime, fmtDate } from './reportDesignTokens'
|
import { fmtEUR, fmtDateTime, fmtTime, fmtDate } from './reportDesignTokens'
|
||||||
|
|
||||||
// ── colour rules ────────────────────────────────────────────────────────────
|
// ── colour rules ────────────────────────────────────────────────────────────
|
||||||
// Given the shift's waiter_id and one item, return { key, label, colors }
|
|
||||||
function classify(item, shiftWaiterId) {
|
function classify(item, shiftWaiterId) {
|
||||||
|
if (item.status === 'cancelled')
|
||||||
|
return { key: 'cancelled', label: 'Ακυρώθηκε', dot: '#dc2626', bg: '#fef2f2', border: '#fecaca', text: '#991b1b' }
|
||||||
|
|
||||||
const orderedByMe = item.added_by_id === shiftWaiterId
|
const orderedByMe = item.added_by_id === shiftWaiterId
|
||||||
const paidToMe = item.paid_by_id === shiftWaiterId
|
const paidToMe = item.paid_by_id === shiftWaiterId
|
||||||
const isPaid = item.status === 'paid'
|
const isPaid = item.status === 'paid'
|
||||||
|
|
||||||
if (orderedByMe && paidToMe && isPaid)
|
if (orderedByMe && paidToMe && isPaid)
|
||||||
return { key: 'both', label: 'Παρήγγειλε + Πληρώθηκε', dot: '#16a34a', bg: '#f0fdf4', border: '#bbf7d0', text: '#15803d' }
|
return { key: 'both', label: 'Πλήρης Παραγγελία', dot: '#16a34a', bg: '#f0fdf4', border: '#bbf7d0', text: '#15803d' }
|
||||||
if (!orderedByMe && paidToMe && isPaid)
|
if (!orderedByMe && paidToMe && isPaid)
|
||||||
return { key: 'paid', label: 'Πληρώθηκε (άλλος παρήγγειλε)', dot: '#2563eb', bg: '#eff6ff', border: '#bfdbfe', text: '#1d4ed8' }
|
return { key: 'paid', label: 'Μόνο Πληρώθηκε', dot: '#2563eb', bg: '#eff6ff', border: '#bfdbfe', text: '#1d4ed8' }
|
||||||
if (orderedByMe && isPaid && !paidToMe)
|
if (orderedByMe && isPaid && !paidToMe)
|
||||||
return { key: 'ordered', label: 'Παρήγγειλε (Πληρώθηκε άλλος)', dot: '#ca8a04', bg: '#fefce8', border: '#fef08a', text: '#854d0e' }
|
return { key: 'ordered', label: 'Μόνο Παρήγγειλε', dot: '#ca8a04', bg: '#fefce8', border: '#fef08a', text: '#854d0e' }
|
||||||
if (orderedByMe && !isPaid)
|
if (orderedByMe && !isPaid)
|
||||||
return { key: 'unpaid', label: 'Παρήγγειλε (απλήρωτο)', dot: '#ea580c', bg: '#fff7ed', border: '#fed7aa', text: '#c2410c' }
|
return { key: 'unpaid', label: 'Ανοιχτό ακόμη', dot: '#ea580c', bg: '#fff7ed', border: '#fed7aa', text: '#c2410c' }
|
||||||
// fallback: paid to me but status not 'paid' — data anomaly
|
return { key: 'anomaly', label: 'Πρόβλημα', dot: '#dc2626', bg: '#fef2f2', border: '#fecaca', text: '#b91c1c' }
|
||||||
return { key: 'anomaly', label: 'Ανωμαλία δεδομένων', dot: '#dc2626', bg: '#fef2f2', border: '#fecaca', text: '#b91c1c' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FILTER_OPTIONS = [
|
const FILTER_OPTIONS = [
|
||||||
{ key: 'all', label: 'Όλα' },
|
{ key: 'all', label: 'Όλα' },
|
||||||
{ key: 'both', label: 'Παρήγγειλε + Πληρώθηκε' },
|
{ key: 'both', label: 'Πλήρης Παραγγελία' },
|
||||||
{ key: 'paid', label: 'Πληρώθηκε (ξένη παραγγελία)' },
|
{ key: 'paid', label: 'Μόνο Πληρώθηκε' },
|
||||||
{ key: 'ordered', label: 'Παρήγγειλε (Πληρώθηκε άλλος)' },
|
{ key: 'ordered', label: 'Μόνο Παρήγγειλε' },
|
||||||
{ key: 'unpaid', label: 'Απλήρωτα' },
|
{ key: 'unpaid', label: 'Ανοιχτό ακόμη' },
|
||||||
|
{ key: 'cancelled', label: 'Ακυρώθηκε' },
|
||||||
|
{ key: 'anomaly', label: 'Πρόβλημα' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const LEGEND = [
|
||||||
|
{ dot: '#16a34a', label: 'Πλήρης Παραγγελία' },
|
||||||
|
{ dot: '#2563eb', label: 'Μόνο Πληρώθηκε' },
|
||||||
|
{ dot: '#ca8a04', label: 'Μόνο Παρήγγειλε' },
|
||||||
|
{ dot: '#ea580c', label: 'Ανοιχτό ακόμη' },
|
||||||
|
{ dot: '#dc2626', label: 'Ακυρώθηκε / Πρόβλημα' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function fmtMins(mins) {
|
function fmtMins(mins) {
|
||||||
@@ -40,31 +51,35 @@ function fmtMins(mins) {
|
|||||||
|
|
||||||
function ItemRow({ item, shiftWaiterId }) {
|
function ItemRow({ item, shiftWaiterId }) {
|
||||||
const c = classify(item, shiftWaiterId)
|
const c = classify(item, shiftWaiterId)
|
||||||
|
const isCancelled = item.status === 'cancelled'
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'flex-start', gap: 10, padding: '7px 14px',
|
display: 'flex', alignItems: 'flex-start', gap: 10, padding: '7px 14px',
|
||||||
borderTop: '1px solid #f4f4f2', background: c.bg,
|
borderTop: '1px solid #f4f4f2', background: c.bg,
|
||||||
}}>
|
}}>
|
||||||
{/* colour dot */}
|
|
||||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: c.dot, flexShrink: 0, marginTop: 5 }} />
|
<div style={{ width: 8, height: 8, borderRadius: '50%', background: c.dot, flexShrink: 0, marginTop: 5 }} />
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
|
||||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#111315' }}>
|
<span style={{ fontSize: 13, fontWeight: 600, color: isCancelled ? '#991b1b' : '#111315' }}>
|
||||||
{item.product_name}
|
{item.product_name}
|
||||||
<span style={{ fontWeight: 400, color: '#8a9099', marginLeft: 4 }}>×{item.quantity}</span>
|
<span style={{ fontWeight: 400, color: '#8a9099', marginLeft: 4 }}>×{item.quantity}</span>
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#111315', flexShrink: 0 }}>{fmtEUR(item.subtotal)}</span>
|
<span style={{ fontSize: 13, fontWeight: 700, color: isCancelled ? '#b91c1c' : '#111315', flexShrink: 0, textDecoration: isCancelled ? 'line-through' : 'none' }}>
|
||||||
|
{fmtEUR(item.subtotal)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2px 12px', marginTop: 2 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2px 12px', marginTop: 2 }}>
|
||||||
<span style={{ fontSize: 11, color: '#5a6169' }}>
|
<span style={{ fontSize: 11, color: '#5a6169' }}>
|
||||||
Παρήγγειλε: <span style={{ fontWeight: 600, color: '#374151' }}>{item.added_by_name ?? '—'}</span>
|
Παρήγγειλε: <span style={{ fontWeight: 600, color: '#374151' }}>{item.added_by_name ?? '—'}</span>
|
||||||
{item.added_at ? <span style={{ color: '#9ca3af' }}> · {fmtDateTime(item.added_at)}</span> : null}
|
{item.added_at ? <span style={{ color: '#9ca3af' }}> · {fmtDateTime(item.added_at)}</span> : null}
|
||||||
</span>
|
</span>
|
||||||
|
{!isCancelled && (
|
||||||
<span style={{ fontSize: 11, color: '#5a6169' }}>
|
<span style={{ fontSize: 11, color: '#5a6169' }}>
|
||||||
Πληρώθηκε: <span style={{ fontWeight: 600, color: item.paid_by_name ? '#374151' : '#9ca3af' }}>{item.paid_by_name ?? '—'}</span>
|
Πληρώθηκε: <span style={{ fontWeight: 600, color: item.paid_by_name ? '#374151' : '#9ca3af' }}>{item.paid_by_name ?? '—'}</span>
|
||||||
{item.paid_at ? <span style={{ color: '#9ca3af' }}> · {fmtDateTime(item.paid_at)}</span> : null}
|
{item.paid_at ? <span style={{ color: '#9ca3af' }}> · {fmtDateTime(item.paid_at)}</span> : null}
|
||||||
{item.payment_method ? <span style={{ color: '#9ca3af' }}> ({item.payment_method})</span> : null}
|
{item.payment_method ? <span style={{ color: '#9ca3af' }}> ({item.payment_method})</span> : null}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 2 }}>
|
<div style={{ marginTop: 2 }}>
|
||||||
<span style={{
|
<span style={{
|
||||||
@@ -90,7 +105,6 @@ function OrderGroup({ order, shiftWaiterId, activeFilter }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ borderRadius: 10, border: '1px solid #edeff1', marginBottom: 8, overflow: 'hidden' }}>
|
<div style={{ borderRadius: 10, border: '1px solid #edeff1', marginBottom: 8, overflow: 'hidden' }}>
|
||||||
{/* Order header */}
|
|
||||||
<div style={{ padding: '8px 14px', background: '#f9fafb', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 4 }}>
|
<div style={{ padding: '8px 14px', background: '#f9fafb', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 4 }}>
|
||||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>
|
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>
|
||||||
Παραγγελία #{order.order_id}
|
Παραγγελία #{order.order_id}
|
||||||
@@ -130,8 +144,7 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) {
|
|||||||
staleTime: 30 * 1000,
|
staleTime: 30 * 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Count items per filter key for badge counts
|
const counts = { all: 0, both: 0, paid: 0, ordered: 0, unpaid: 0, cancelled: 0, anomaly: 0 }
|
||||||
const counts = { all: 0, both: 0, paid: 0, ordered: 0, unpaid: 0 }
|
|
||||||
const waiterIdToUse = shiftWaiterId ?? summary?.waiter_id
|
const waiterIdToUse = shiftWaiterId ?? summary?.waiter_id
|
||||||
if (summary?.orders) {
|
if (summary?.orders) {
|
||||||
for (const o of summary.orders) {
|
for (const o of summary.orders) {
|
||||||
@@ -151,7 +164,7 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) {
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'white', borderRadius: 20, width: '100%', maxWidth: 900,
|
background: 'white', borderRadius: 20, width: '100%', maxWidth: 1080,
|
||||||
maxHeight: '92vh', display: 'flex', flexDirection: 'column',
|
maxHeight: '92vh', display: 'flex', flexDirection: 'column',
|
||||||
boxShadow: '0 24px 64px rgba(0,0,0,0.25)',
|
boxShadow: '0 24px 64px rgba(0,0,0,0.25)',
|
||||||
}}
|
}}
|
||||||
@@ -180,8 +193,6 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) {
|
|||||||
{/* KPI grid — 2 rows × 4 cols */}
|
{/* KPI grid — 2 rows × 4 cols */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const totalOrders = summary.orders?.length ?? 0
|
const totalOrders = summary.orders?.length ?? 0
|
||||||
const tableSet = new Set(summary.orders?.map(o => o.table_name).filter(Boolean))
|
|
||||||
const tablesServed = tableSet.size
|
|
||||||
const durationHours = summary.duration_hours ?? (summary.duration_minutes != null ? summary.duration_minutes / 60 : null)
|
const durationHours = summary.duration_hours ?? (summary.duration_minutes != null ? summary.duration_minutes / 60 : null)
|
||||||
const durationLabel = durationHours != null
|
const durationLabel = durationHours != null
|
||||||
? (() => { const h = Math.floor(durationHours); const m = Math.round((durationHours - h) * 60); return h > 0 ? `${h}ω ${m}λ` : `${m}λ` })()
|
? (() => { const h = Math.floor(durationHours); const m = Math.round((durationHours - h) * 60); return h > 0 ? `${h}ω ${m}λ` : `${m}λ` })()
|
||||||
@@ -204,10 +215,19 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) {
|
|||||||
? undefined
|
? undefined
|
||||||
: Math.abs(discrepancy) < 0.005 ? '#16a34a' : '#dc2626'
|
: Math.abs(discrepancy) < 0.005 ? '#16a34a' : '#dc2626'
|
||||||
|
|
||||||
|
const cancelEvents = summary.cancellation_events ?? 0
|
||||||
|
const cancelItemsQty = summary.orders
|
||||||
|
? summary.orders.reduce((s, o) => s + o.items.filter(i => i.status === 'cancelled').reduce((ss, i) => ss + i.quantity, 0), 0)
|
||||||
|
: 0
|
||||||
|
|
||||||
const row2 = [
|
const row2 = [
|
||||||
{ label: 'Σύνολο Παραγγελιών', value: String(totalOrders) },
|
{ label: 'Σύνολο Παραγγελιών', value: String(totalOrders) },
|
||||||
{ label: 'Εισπράξεις', value: fmtEUR(summary.total_collected), accent: '#2f9e5e' },
|
{ label: 'Εισπράξεις', value: fmtEUR(summary.total_collected), accent: '#2f9e5e' },
|
||||||
{ label: 'Παραδόθηκαν', value: summary.counted_cash_end != null ? fmtEUR(summary.counted_cash_end) : '—', accent: '#3758c9' },
|
{
|
||||||
|
label: 'Ακυρώσεις',
|
||||||
|
value: cancelEvents > 0 ? `${cancelEvents} / ${cancelItemsQty} είδη` : '—',
|
||||||
|
accent: cancelEvents > 0 ? '#dc2626' : undefined,
|
||||||
|
},
|
||||||
{ label: 'Ταμείο', value: discLabel, accent: discAccent },
|
{ label: 'Ταμείο', value: discLabel, accent: discAccent },
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
@@ -228,14 +248,8 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) {
|
|||||||
|
|
||||||
{/* Colour legend */}
|
{/* Colour legend */}
|
||||||
<div style={{ display: 'flex', gap: 12, padding: '8px 24px', background: '#fafafa', borderBottom: '1px solid #edeff1', flexShrink: 0, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 12, padding: '8px 24px', background: '#fafafa', borderBottom: '1px solid #edeff1', flexShrink: 0, flexWrap: 'wrap' }}>
|
||||||
{[
|
{LEGEND.map(l => (
|
||||||
{ dot: '#16a34a', label: 'Παρήγγειλε + Πληρώθηκε' },
|
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 11, color: '#5a6169' }}>
|
||||||
{ dot: '#2563eb', label: 'Πληρώθηκε (παρήγγηλε άλλος)' },
|
|
||||||
{ dot: '#ca8a04', label: 'Παρήγγειλε (Πληρώθηκε άλλος)' },
|
|
||||||
{ dot: '#ea580c', label: 'Παρήγγειλε (απλήρωτο)' },
|
|
||||||
{ dot: '#dc2626', label: 'Πρόβλημα Δεδομένων' },
|
|
||||||
].map(l => (
|
|
||||||
<div key={l.dot} style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 11, color: '#5a6169' }}>
|
|
||||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.dot, flexShrink: 0 }} />
|
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.dot, flexShrink: 0 }} />
|
||||||
{l.label}
|
{l.label}
|
||||||
</div>
|
</div>
|
||||||
@@ -244,29 +258,41 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) {
|
|||||||
|
|
||||||
{/* Filter bar */}
|
{/* Filter bar */}
|
||||||
<div style={{ display: 'flex', gap: 6, padding: '10px 24px', borderBottom: '1px solid #edeff1', flexShrink: 0, overflowX: 'auto' }}>
|
<div style={{ display: 'flex', gap: 6, padding: '10px 24px', borderBottom: '1px solid #edeff1', flexShrink: 0, overflowX: 'auto' }}>
|
||||||
{FILTER_OPTIONS.map(opt => (
|
{FILTER_OPTIONS.map(opt => {
|
||||||
|
const isCancelOpt = opt.key === 'cancelled'
|
||||||
|
const isActive = activeFilter === opt.key
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={opt.key}
|
key={opt.key}
|
||||||
onClick={() => setActiveFilter(opt.key)}
|
onClick={() => setActiveFilter(opt.key)}
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 12px', borderRadius: 20, fontSize: 12, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
|
padding: '4px 12px', borderRadius: 20, fontSize: 12, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
|
||||||
border: activeFilter === opt.key ? '1.5px solid #3758c9' : '1.5px solid #e5e7eb',
|
border: isActive
|
||||||
background: activeFilter === opt.key ? '#eff6ff' : 'white',
|
? isCancelOpt ? '1.5px solid #dc2626' : '1.5px solid #3758c9'
|
||||||
color: activeFilter === opt.key ? '#3758c9' : '#5a6169',
|
: '1.5px solid #e5e7eb',
|
||||||
|
background: isActive
|
||||||
|
? isCancelOpt ? '#fef2f2' : '#eff6ff'
|
||||||
|
: 'white',
|
||||||
|
color: isActive
|
||||||
|
? isCancelOpt ? '#991b1b' : '#3758c9'
|
||||||
|
: '#5a6169',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
{counts[opt.key] > 0 && (
|
{counts[opt.key] > 0 && (
|
||||||
<span style={{
|
<span style={{
|
||||||
marginLeft: 5, padding: '1px 5px', borderRadius: 8, fontSize: 10,
|
marginLeft: 5, padding: '1px 5px', borderRadius: 8, fontSize: 10,
|
||||||
background: activeFilter === opt.key ? '#3758c9' : '#e5e7eb',
|
background: isActive
|
||||||
color: activeFilter === opt.key ? 'white' : '#5a6169',
|
? isCancelOpt ? '#dc2626' : '#3758c9'
|
||||||
|
: '#e5e7eb',
|
||||||
|
color: isActive ? 'white' : '#5a6169',
|
||||||
}}>
|
}}>
|
||||||
{counts[opt.key]}
|
{counts[opt.key]}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Orders + items */}
|
{/* Orders + items */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { UserRoundX, ChevronRight, ExternalLink, Trash2 } from 'lucide-react'
|
import { UserRoundX, ChevronRight, ExternalLink, Trash2, Eye } from 'lucide-react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import ShiftDetailModal from '../shared/ShiftDetailModal'
|
import ShiftDetailModal from '../shared/ShiftDetailModal'
|
||||||
import DeleteConfirmModal from '../../../ui/DeleteConfirmModal'
|
import DeleteConfirmModal from '../../../ui/DeleteConfirmModal'
|
||||||
@@ -194,9 +194,10 @@ export default function ShiftsOverview({ onNavigate } = {}) {
|
|||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={e => { e.stopPropagation(); setDetailShift({ id: s.id, waiter_id: s.waiter_id }) }}
|
onClick={e => { e.stopPropagation(); setDetailShift({ id: s.id, waiter_id: s.waiter_id }) }}
|
||||||
className="rounded border border-slate-200 bg-white px-2 py-0.5 text-[11px] font-medium text-slate-600 hover:bg-slate-50"
|
className="rounded border border-sky-200 bg-white p-0.5 text-sky-400 hover:bg-sky-50 hover:text-sky-600"
|
||||||
|
title="Λεπτομέρειες βάρδιας"
|
||||||
>
|
>
|
||||||
Λεπτομέρειες
|
<Eye size={13} />
|
||||||
</button>
|
</button>
|
||||||
{!s.is_active && (
|
{!s.is_active && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user