from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from database import get_db from models.manager_account import ManagerAccount from models.online_order import OnlineOrder from models.stats_snapshot import StatsSnapshot from schemas.manager import StatsSnapshotOut, StatsSnapshotRequest from routers.manager_auth import _get_current_manager router = APIRouter() # ── Sites the manager can access ────────────────────────────────────────────── @router.get("/sites") def list_sites(manager: ManagerAccount = Depends(_get_current_manager)): return [{"id": s.id, "site_id": s.site_id, "name": s.name} for s in manager.sites] # ── Stats snapshot ──────────────────────────────────────────────────────────── @router.get("/sites/{site_id}/snapshot", response_model=StatsSnapshotOut) def get_stats_snapshot( site_id: int, manager: ManagerAccount = Depends(_get_current_manager), db: Session = Depends(get_db), ): _check_site_access(manager, site_id) snap = db.query(StatsSnapshot).filter(StatsSnapshot.site_id == site_id).first() if not snap: raise HTTPException(status_code=404, detail="No snapshot available yet") return snap # ── Orders ──────────────────────────────────────────────────────────────────── @router.get("/sites/{site_id}/orders") def list_orders( site_id: int, order_status: str | None = None, limit: int = 50, offset: int = 0, manager: ManagerAccount = Depends(_get_current_manager), db: Session = Depends(get_db), ): _check_site_access(manager, site_id) q = db.query(OnlineOrder).filter(OnlineOrder.site_id == site_id) if order_status: q = q.filter(OnlineOrder.status == order_status) orders = q.order_by(OnlineOrder.created_at.desc()).offset(offset).limit(limit).all() return _serialize_orders(orders) @router.get("/sites/{site_id}/orders/pending") def list_pending_orders( site_id: int, manager: ManagerAccount = Depends(_get_current_manager), db: Session = Depends(get_db), ): _check_site_access(manager, site_id) orders = ( db.query(OnlineOrder) .filter(OnlineOrder.site_id == site_id, OnlineOrder.status == "pending_acceptance") .order_by(OnlineOrder.created_at) .all() ) return _serialize_orders(orders) # ── Stats snapshot push (site API key — called by local_backend) ────────────── # Imported and registered from main.py under a site-key-protected sub-path. # Defined here as a plain function so remote_dashboard router stays manager-JWT only. from fastapi import Header from passlib.context import CryptContext from models.site import Site _pwd = CryptContext(schemes=["bcrypt"], deprecated="auto") @router.post("/snapshot", status_code=status.HTTP_204_NO_CONTENT) def push_stats_snapshot( body: StatsSnapshotRequest, x_site_id: str = Header(..., alias="X-Site-ID"), x_site_key: str = Header(..., alias="X-Site-Key"), db: Session = Depends(get_db), ): """Local backend pushes stats every 5 minutes. Auth: site API key.""" site = db.query(Site).filter(Site.site_id == x_site_id).first() if not site or not _pwd.verify(x_site_key, site.secret_key_hash): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid site credentials") snap = db.query(StatsSnapshot).filter(StatsSnapshot.site_id == body.site_id).first() if snap: snap.snapshot_json = body.snapshot_json else: snap = StatsSnapshot(site_id=body.site_id, snapshot_json=body.snapshot_json) db.add(snap) db.commit() # ── Manager can also update order status (goes via cloud, local polls) ───────── from schemas.online_order import OrderStatusUpdateRequest _TRANSITIONS: dict[str, set[str]] = { "pending_acceptance": {"accepted", "rejected"}, "accepted": {"preparing"}, "preparing": {"ready", "out_for_delivery"}, "ready": {"delivered"}, "out_for_delivery": {"delivered"}, } @router.patch("/sites/{site_id}/orders/{order_id}/status", status_code=status.HTTP_204_NO_CONTENT) def manager_update_order_status( site_id: int, order_id: int, body: OrderStatusUpdateRequest, manager: ManagerAccount = Depends(_get_current_manager), db: Session = Depends(get_db), ): _check_site_access(manager, site_id) order = db.query(OnlineOrder).filter( OnlineOrder.id == order_id, OnlineOrder.site_id == site_id ).first() if not order: raise HTTPException(status_code=404, detail="Order not found") allowed = _TRANSITIONS.get(order.status, set()) if body.status not in allowed: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Cannot transition from '{order.status}' to '{body.status}'", ) order.status = body.status if body.rejection_reason: order.rejection_reason = body.rejection_reason db.commit() # ── Helpers ─────────────────────────────────────────────────────────────────── def _check_site_access(manager: ManagerAccount, site_id: int): if not any(s.id == site_id for s in manager.sites): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No access to this site") def _serialize_orders(orders): return [ { "id": o.id, "public_ref": o.public_ref, "order_type": o.order_type, "customer_name": o.customer_name, "customer_phone": o.customer_phone, "customer_address": o.customer_address, "customer_notes": o.customer_notes, "subtotal": o.subtotal, "delivery_fee": o.delivery_fee, "total": o.total, "status": o.status, "rejection_reason": o.rejection_reason, "synced_to_local": o.synced_to_local, "local_order_id": o.local_order_id, "created_at": o.created_at.isoformat() if o.created_at else None, "updated_at": o.updated_at.isoformat() if o.updated_at else None, } for o in orders ]