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

@@ -120,14 +120,83 @@ def list_active_orders(db: Session = Depends(get_db), user: User = Depends(get_c
]
@router.get("/{order_id}", response_model=OrderOut)
@router.get("/{order_id}")
def get_order(order_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(status_code=404, detail="Order not found")
if not _can_access_order(order, user, db):
raise HTTPException(status_code=403, detail="Access denied")
return order
# Resolve all user IDs referenced by this order in one query
user_ids = set()
user_ids.add(order.opened_by)
if order.closed_by:
user_ids.add(order.closed_by)
for item in order.items:
user_ids.add(item.added_by)
if item.paid_by:
user_ids.add(item.paid_by)
for log in order.audit_logs:
if log.waiter_id:
user_ids.add(log.waiter_id)
users = db.query(User).filter(User.id.in_(user_ids)).all()
name_map = {u.id: u.nickname or u.full_name or u.username for u in users}
def fmt_item(i):
return {
"id": i.id,
"order_id": i.order_id,
"product_id": i.product_id,
"product": {"id": i.product.id, "name": i.product.name} if i.product else None,
"added_by": i.added_by,
"added_by_name": name_map.get(i.added_by),
"quantity": i.quantity,
"unit_price": i.unit_price,
"selected_options": i.selected_options,
"removed_ingredients": i.removed_ingredients,
"notes": i.notes,
"status": i.status,
"added_at": i.added_at,
"printed": i.printed,
"paid_by": i.paid_by,
"paid_by_name": name_map.get(i.paid_by) if i.paid_by else None,
"paid_at": i.paid_at,
"payment_method": i.payment_method,
"paid_in_shift_id": i.paid_in_shift_id,
}
def fmt_log(l):
return {
"id": l.id,
"order_id": l.order_id,
"event_type": l.event_type,
"waiter_id": l.waiter_id,
"waiter_name": name_map.get(l.waiter_id) if l.waiter_id else None,
"item_ids": l.item_ids,
"amount": l.amount,
"payment_method": l.payment_method,
"note": l.note,
"created_at": l.created_at,
"offline_at": l.offline_at,
"is_duplicate": l.is_duplicate,
}
return {
"id": order.id,
"table_id": order.table_id,
"opened_by": order.opened_by,
"opened_at": order.opened_at,
"status": order.status,
"closed_at": order.closed_at,
"closed_by": order.closed_by,
"notes": order.notes,
"business_day_id": order.business_day_id,
"items": [fmt_item(i) for i in order.items],
"waiters": [{"waiter_id": w.waiter_id} for w in order.waiters],
"audit_logs": [fmt_log(l) for l in order.audit_logs],
}
@router.post("/", response_model=OrderOut, status_code=status.HTTP_201_CREATED)
@@ -310,10 +379,26 @@ def close_order(order_id: int, db: Session = Depends(get_db), user: User = Depen
raise HTTPException(status_code=403, detail="Access denied")
if order.status not in ("paid", "open", "partially_paid"):
raise HTTPException(status_code=400, detail="Cannot close order in current status")
now = datetime.now(timezone.utc)
# Mark all still-active items as 'closed' — unpaid, closed by manager
active_items = db.query(OrderItem).filter(
OrderItem.order_id == order_id,
OrderItem.status == "active",
).all()
closed_item_ids = []
for item in active_items:
item.status = "closed"
closed_item_ids.append(item.id)
order.status = "closed"
order.closed_at = datetime.now(timezone.utc)
order.closed_at = now
order.closed_by = user.id
_audit(db, order_id, "ORDER_CLOSED", waiter_id=user.id)
note = f"Κλείσιμο από manager — {len(closed_item_ids)} απλήρωτα αντικείμενα" if closed_item_ids else None
_audit(db, order_id, "ORDER_CLOSED", waiter_id=user.id,
item_ids=closed_item_ids if closed_item_ids else None, note=note)
db.commit()
broadcast_sync("order_closed", {"order_id": order_id, "table_id": order.table_id})
return {"status": "closed"}
@@ -419,10 +504,26 @@ def cancel_order(order_id: int, db: Session = Depends(get_db), user: User = Depe
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(status_code=404, detail="Order not found")
now = datetime.now(timezone.utc)
# Cancel all still-active items
active_items = db.query(OrderItem).filter(
OrderItem.order_id == order_id,
OrderItem.status == "active",
).all()
cancelled_item_ids = []
for item in active_items:
item.status = "cancelled"
cancelled_item_ids.append(item.id)
order.status = "cancelled"
order.closed_at = datetime.now(timezone.utc)
order.closed_at = now
order.closed_by = user.id
_audit(db, order_id, "ORDER_CANCELLED", waiter_id=user.id)
note = f"Ακύρωση από manager — {len(cancelled_item_ids)} αντικείμενα ακυρώθηκαν" if cancelled_item_ids else None
_audit(db, order_id, "ORDER_CANCELLED", waiter_id=user.id,
item_ids=cancelled_item_ids if cancelled_item_ids else None, note=note)
db.commit()
broadcast_sync("order_closed", {"order_id": order_id, "table_id": order.table_id})