fix: report cancellation bugs + missing OrderItem model columns

Backend:
- OrderItem model: add cancelled_at, cancelled_by, cancel_reason columns
  (migration existed but model was missing them — caused 500 on cancellations_log)
- shifts.py _enrich_shift: count cancellations by quantity sum (not row count),
  add cancellation_value (sum of unit_price * quantity)
- reports.py current business day: add cancelled_items count (per-item quantity sum,
  across all orders in the day, not just fully-cancelled orders)

Manager dashboard:
- PrintFontsTab: fix SSE auth token key (access_token → manager_token, was causing 403)
- Today: show cancelled_items count as sub-label on Ακυρώσεις stat card
- OrderHistory: items/cancellations columns now use quantity sum, not row count
- WorkDaySummary drill-down: same quantity-sum fix
- ShiftsOverview: add Αξία Ακυρ. column next to Ακυρώσεις

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 17:15:55 +03:00
parent 36e9044972
commit 8533a1452b
8 changed files with 51 additions and 15 deletions

View File

@@ -804,7 +804,7 @@ function FindPrintersModal({ onClose, onSelect }) {
setError(null)
setScanning(true)
const token = localStorage.getItem('access_token') || ''
const token = localStorage.getItem('manager_token') || ''
const params = new URLSearchParams({ subnet, port })
// Use fetch with ReadableStream to consume SSE (EventSource doesn't support auth headers)
fetch(`/api/system/printers/scan?${params}`, {

View File

@@ -95,8 +95,11 @@ export default function OrderHistory({ initialBusinessDayId } = {}) {
</THead>
<tbody>
{orders.slice(0, 200).map(o => {
const activeItems = (o.items || []).filter(i => i.status !== 'cancelled')
const cancelledItems = (o.items || []).filter(i => i.status === 'cancelled')
const allItems = o.items || []
const activeItems = allItems.filter(i => i.status !== 'cancelled')
const cancelledItems = allItems.filter(i => i.status === 'cancelled')
const totalQty = allItems.reduce((s, i) => s + (i.quantity || 1), 0)
const cancelledQty = cancelledItems.reduce((s, i) => s + (i.quantity || 1), 0)
const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0)
const isCancelled = o.status === 'cancelled'
return (
@@ -106,10 +109,10 @@ export default function OrderHistory({ initialBusinessDayId } = {}) {
<TD mono>{fmtDateTime(o.opened_at)}</TD>
<TD mono>{fmtDateTime(o.closed_at)}</TD>
<TD><StatusBadge status={o.status} /></TD>
<TD mono align="right">{activeItems.length}</TD>
<TD mono align="right">{totalQty}</TD>
<TD mono align="right">
{cancelledItems.length > 0
? <span className="text-red-500 font-semibold">{cancelledItems.length}</span>
{cancelledQty > 0
? <span className="text-red-500 font-semibold">{cancelledQty}</span>
: <span className="text-slate-300"></span>}
</TD>
<TD mono align="right" className="font-semibold text-slate-900">{fmtEUR(total)}</TD>

View File

@@ -119,7 +119,12 @@ export default function Today() {
{bd.top_product && (
<StatCard label="Κορυφαίο Προϊόν" value={bd.top_product.name} sub={`${bd.top_product.qty} τεμ.`} icon={Trophy} />
)}
<StatCard label="Ακυρώσεις" value={fmtNum(bd.cancellations)} icon={XCircle} />
<StatCard
label="Ακυρώσεις"
value={fmtNum(bd.cancellations)}
sub={bd.cancelled_items > 0 ? `${fmtNum(bd.cancelled_items)} είδη` : 'παραγγελίες'}
icon={XCircle}
/>
{bd.total_cost > 0 && (
<StatCard label="Κόστος Πωλήσεων" value={fmtEUR(bd.total_cost)} sub="κόστος ειδών" icon={ShoppingBag} />
)}

View File

@@ -75,8 +75,11 @@ function WorkDayModal({ day, onClose, onDeleteShift }) {
</THead>
<tbody>
{orders.map(o => {
const activeItems = (o.items || []).filter(i => i.status !== 'cancelled')
const cancelledItems = (o.items || []).filter(i => i.status === 'cancelled')
const allItems = o.items || []
const activeItems = allItems.filter(i => i.status !== 'cancelled')
const cancelledItems = allItems.filter(i => i.status === 'cancelled')
const totalQty = allItems.reduce((s, i) => s + (i.quantity || 1), 0)
const cancelledQty = cancelledItems.reduce((s, i) => s + (i.quantity || 1), 0)
const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0)
return (
<TR key={o.id} striped onClick={() => setDrillOrder(o)} className="cursor-pointer">
@@ -84,10 +87,10 @@ function WorkDayModal({ day, onClose, onDeleteShift }) {
<TD>{o.table_name ?? o.table_id}</TD>
<TD mono>{fmtDateTime(o.opened_at)}</TD>
<TD mono>{o.closed_at ? fmtDateTime(o.closed_at) : '—'}</TD>
<TD mono align="right">{activeItems.length}</TD>
<TD mono align="right">{totalQty}</TD>
<TD mono align="right">
{cancelledItems.length > 0
? <span className="text-red-500 font-semibold">{cancelledItems.length}</span>
{cancelledQty > 0
? <span className="text-red-500 font-semibold">{cancelledQty}</span>
: <span className="text-slate-300"></span>}
</TD>
<TD mono align="right" className="font-semibold text-slate-900">{fmtEUR(total)}</TD>

View File

@@ -126,6 +126,7 @@ export default function ShiftsOverview({ onNavigate } = {}) {
<TH align="right">Οφείλει</TH>
<TH align="right">Ταμείο</TH>
<TH align="right">Ακυρώσεις</TH>
<TH align="right">Αξία Ακυρ.</TH>
<TH>Κατάσταση</TH>
<TH className="w-28" />
</THead>
@@ -183,6 +184,11 @@ export default function ShiftsOverview({ onNavigate } = {}) {
? <span className="text-red-600 font-semibold">{s.cancellations}</span>
: <span className="text-slate-300"></span>}
</TD>
<TD mono align="right">
{s.cancellation_value > 0
? <span className="text-red-500">{fmtEUR(s.cancellation_value)}</span>
: <span className="text-slate-300"></span>}
</TD>
<TD><StatusBadge status={s.is_active ? 'active' : 'closed'} pulse /></TD>
<TD align="right">
<div className="flex items-center justify-end gap-2">
@@ -207,7 +213,7 @@ export default function ShiftsOverview({ onNavigate } = {}) {
</TR>,
isOpen && (
<tr key={`${s.id}-detail`}>
<td colSpan={13} className="border-b border-slate-100 bg-slate-50/60 px-6 py-3">
<td colSpan={14} className="border-b border-slate-100 bg-slate-50/60 px-6 py-3">
<div className="flex items-center gap-4 text-[12px] text-slate-500">
<span>
{'Εργάσιμη Μέρα: '}