feat: major dashboard & waiter PWA overhaul

- Manager dashboard: replaced monolithic DashboardTab/OperationsPage with new
  DashboardPage; added OrderDetailModal, ShiftDetailModal, DeleteConfirmModal,
  PaymentMethodModal; updated Sidebar routing and App navigation
- Reports: reworked WorkDaySummary, OrderHistory, ShiftsOverview with detail modals
- Backend routers: extended orders, reports, shifts, products, business_day endpoints;
  updated cloud_sync service
- Waiter PWA: refreshed app icons, improved ConnectionLostModal UX, updated
  TableCard, SSEContext, connectionStore; added useProductCache hook; vite config tweaks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 15:24:54 +03:00
parent aa92623802
commit 5de89a722c
40 changed files with 1906 additions and 1171 deletions

View File

@@ -178,7 +178,7 @@ def shift_orders_summary(
return {"from": start.isoformat(), "to": end.isoformat(), "waiters": result}
@router.get("/orders/history", response_model=List[OrderOut])
@router.get("/orders/history")
def order_history(
from_date: Optional[str] = Query(default=None, alias="from"),
to_date: Optional[str] = Query(default=None, alias="to"),
@@ -191,7 +191,14 @@ def order_history(
db: Session = Depends(get_db),
user: User = Depends(require_manager),
):
q = db.query(Order)
from sqlalchemy.orm import joinedload
from models.table import Table as TableModel
q = db.query(Order).options(
joinedload(Order.items).joinedload(OrderItem.product),
joinedload(Order.waiters),
joinedload(Order.audit_logs),
)
if business_day_id:
q = q.filter(Order.business_day_id == business_day_id)
elif from_date or to_date:
@@ -205,7 +212,109 @@ def order_history(
q = q.filter(Order.status == order_status)
if table_id:
q = q.filter(Order.table_id == table_id)
return q.order_by(Order.opened_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
orders = q.order_by(Order.opened_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
# Collect all waiter IDs and table IDs to resolve in bulk
waiter_ids: set[int] = set()
table_ids: set[int] = set()
item_waiter_ids: set[int] = set()
for o in orders:
if o.opened_by:
waiter_ids.add(o.opened_by)
if o.closed_by:
waiter_ids.add(o.closed_by)
if o.table_id:
table_ids.add(o.table_id)
for item in o.items:
if item.added_by:
item_waiter_ids.add(item.added_by)
if item.paid_by:
item_waiter_ids.add(item.paid_by)
for log in o.audit_logs:
if log.waiter_id:
waiter_ids.add(log.waiter_id)
all_waiter_ids = waiter_ids | item_waiter_ids
waiters_map: dict[int, str] = {}
if all_waiter_ids:
for w in db.query(User).filter(User.id.in_(all_waiter_ids)).all():
waiters_map[w.id] = w.full_name or w.username
tables_map: dict[int, str] = {}
if table_ids:
for t in db.query(TableModel).filter(TableModel.id.in_(table_ids)).all():
prefix = (t.group.prefix if t.group and t.group.prefix else "") if t.group else ""
tables_map[t.id] = t.label if t.label else f"{prefix}{t.number}"
def _wname(wid):
if wid is None:
return None
return waiters_map.get(wid, f"#{wid}")
def _dt_local(dt):
if dt is None:
return None
return (dt.isoformat() + "Z") if dt.tzinfo is None else dt.isoformat()
result = []
for o in orders:
items_out = []
for item in o.items:
items_out.append({
"id": item.id,
"order_id": item.order_id,
"product_id": item.product_id,
"product": {"id": item.product.id, "name": item.product.name} if item.product else None,
"added_by": item.added_by,
"added_by_name": _wname(item.added_by),
"added_at": _dt_local(item.added_at),
"quantity": item.quantity,
"unit_price": float(item.unit_price),
"status": item.status,
"paid_by": item.paid_by,
"paid_by_name": _wname(item.paid_by),
"paid_at": _dt_local(item.paid_at),
"payment_method": item.payment_method,
"paid_in_shift_id": item.paid_in_shift_id,
"notes": item.notes,
"printed": item.printed,
"selected_options": item.selected_options,
"removed_ingredients": item.removed_ingredients,
})
audit_out = []
for log in o.audit_logs:
audit_out.append({
"id": log.id,
"order_id": log.order_id,
"event_type": log.event_type,
"waiter_id": log.waiter_id,
"waiter_name": _wname(log.waiter_id),
"item_ids": log.item_ids,
"amount": log.amount,
"payment_method": log.payment_method,
"note": log.note,
"created_at": _dt_local(log.created_at),
"offline_at": log.offline_at,
"is_duplicate": log.is_duplicate,
})
result.append({
"id": o.id,
"table_id": o.table_id,
"table_name": tables_map.get(o.table_id) if o.table_id else None,
"opened_by": o.opened_by,
"opened_by_name": _wname(o.opened_by),
"opened_at": _dt_local(o.opened_at),
"closed_by": o.closed_by,
"closed_by_name": _wname(o.closed_by),
"closed_at": _dt_local(o.closed_at),
"status": o.status,
"notes": o.notes,
"business_day_id": o.business_day_id,
"items": items_out,
"waiters": [{"waiter_id": w.waiter_id} for w in o.waiters],
"audit_logs": audit_out,
})
return result
@router.get("/tables/summary")
@@ -689,9 +798,8 @@ def business_days_list(
orders = db.query(Order).filter(Order.business_day_id == d.id).all()
closed_orders = [o for o in orders if o.status in ("closed", "paid")]
cancelled_orders = [o for o in orders if o.status == "cancelled"]
waiter_ids = set()
for s in (db.query(WaiterShift).filter(WaiterShift.business_day_id == d.id).all()):
waiter_ids.add(s.waiter_id)
day_shifts = db.query(WaiterShift).filter(WaiterShift.business_day_id == d.id).all()
waiter_ids = {s.waiter_id for s in day_shifts}
revenue = sum(
sum(i.unit_price * i.quantity for i in o.items if i.status in ("active", "paid"))
for o in closed_orders
@@ -710,6 +818,7 @@ def business_days_list(
"closed_order_count": len(closed_orders),
"cancellation_count": len(cancelled_orders),
"waiter_count": len(waiter_ids),
"shift_count": len(day_shifts),
"revenue": round(revenue, 2),
})
return {"business_days": result}