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