- 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>
1270 lines
48 KiB
Python
1270 lines
48 KiB
Python
import csv
|
|
import io
|
|
import json
|
|
|
|
from fastapi import APIRouter, Depends, Query, HTTPException, BackgroundTasks
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func
|
|
from datetime import date, datetime, timedelta
|
|
from typing import Optional, List
|
|
|
|
from database import get_db
|
|
from models.order import Order, OrderItem, OrderWaiter, PrintLog
|
|
from models.user import User
|
|
from models.table import Table
|
|
from models.printer import Printer
|
|
from models.shift import WaiterShift
|
|
from models.business_day import BusinessDay
|
|
from schemas.order import OrderOut
|
|
from schemas.table import TableOut
|
|
from routers.deps import require_manager
|
|
from services.printer_service import print_waiter_report, print_printer_report, print_order_receipt
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _dt(dt):
|
|
if dt is None:
|
|
return None
|
|
return (dt.isoformat() + "Z") if dt.tzinfo is None else dt.isoformat()
|
|
|
|
|
|
@router.get("/shift")
|
|
def shift_summary(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
report_date: Optional[date] = Query(default=None, alias="date"),
|
|
waiter_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
"""Payments collected per waiter — based on paid_by on order items."""
|
|
if from_dt and to_dt:
|
|
start = datetime.fromisoformat(from_dt)
|
|
end = datetime.fromisoformat(to_dt)
|
|
else:
|
|
target = report_date or date.today()
|
|
start = datetime.combine(target, datetime.min.time())
|
|
end = start + timedelta(days=1)
|
|
|
|
q = db.query(OrderItem).filter(
|
|
OrderItem.status == "paid",
|
|
OrderItem.paid_at >= start,
|
|
OrderItem.paid_at < end,
|
|
)
|
|
if waiter_id:
|
|
q = q.filter(OrderItem.paid_by == waiter_id)
|
|
items = q.all()
|
|
|
|
waiters_db = {u.id: u for u in db.query(User).all()}
|
|
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
|
|
# Build per-waiter summary keyed by waiter_id
|
|
summary: dict[int, dict] = {}
|
|
for item in items:
|
|
wid = item.paid_by
|
|
if wid not in summary:
|
|
w = waiters_db.get(wid)
|
|
wname = (w.full_name or w.username) if w else f"#{wid}"
|
|
summary[wid] = {
|
|
"waiter_id": wid,
|
|
"waiter_name": wname,
|
|
"items": 0,
|
|
"total": 0.0,
|
|
"order_data": {},
|
|
}
|
|
summary[wid]["items"] += item.quantity
|
|
val = item.unit_price * item.quantity
|
|
summary[wid]["total"] += val
|
|
|
|
oid = item.order_id
|
|
if oid not in summary[wid]["order_data"]:
|
|
order = db.query(Order).filter(Order.id == oid).first()
|
|
summary[wid]["order_data"][oid] = {
|
|
"id": oid,
|
|
"time_open": order.opened_at.strftime("%H:%M") if order else "",
|
|
"time_close": order.closed_at.strftime("%H:%M") if order and order.closed_at else "",
|
|
"table": tables_db.get(order.table_id, f"#{oid}") if order else f"#{oid}",
|
|
"total": 0.0,
|
|
"items": [],
|
|
}
|
|
summary[wid]["order_data"][oid]["total"] += val
|
|
product_name = item.product.name if item.product else f"#{item.product_id}"
|
|
summary[wid]["order_data"][oid]["items"].append(
|
|
{"name": product_name, "quantity": item.quantity}
|
|
)
|
|
|
|
result = []
|
|
for entry in summary.values():
|
|
entry["orders"] = len(entry["order_data"])
|
|
entry["order_data"] = list(entry["order_data"].values())
|
|
result.append(entry)
|
|
|
|
return {"from": start.isoformat(), "to": end.isoformat(), "waiters": result}
|
|
|
|
|
|
@router.get("/shift/orders")
|
|
def shift_orders_summary(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
report_date: Optional[date] = Query(default=None, alias="date"),
|
|
waiter_id: Optional[int] = None,
|
|
business_day_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
"""Items sent (added) per waiter — regardless of payment status."""
|
|
q = db.query(OrderItem).filter(OrderItem.status.in_(["active", "paid"]))
|
|
if business_day_id:
|
|
q = q.join(Order).filter(Order.business_day_id == business_day_id)
|
|
else:
|
|
if from_dt and to_dt:
|
|
start = datetime.fromisoformat(from_dt)
|
|
end = datetime.fromisoformat(to_dt)
|
|
else:
|
|
target = report_date or date.today()
|
|
start = datetime.combine(target, datetime.min.time())
|
|
end = start + timedelta(days=1)
|
|
q = q.filter(OrderItem.added_at >= start, OrderItem.added_at < end)
|
|
if waiter_id:
|
|
q = q.filter(OrderItem.added_by == waiter_id)
|
|
items = q.all()
|
|
|
|
waiters_db = {u.id: u for u in db.query(User).all()}
|
|
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
|
|
summary: dict[int, dict] = {}
|
|
for item in items:
|
|
wid = item.added_by
|
|
if wid not in summary:
|
|
w = waiters_db.get(wid)
|
|
wname = (w.full_name or w.username) if w else f"#{wid}"
|
|
summary[wid] = {
|
|
"waiter_id": wid,
|
|
"waiter_name": wname,
|
|
"items": 0,
|
|
"total": 0.0,
|
|
"order_data": {},
|
|
}
|
|
summary[wid]["items"] += item.quantity
|
|
val = item.unit_price * item.quantity
|
|
summary[wid]["total"] += val
|
|
|
|
oid = item.order_id
|
|
if oid not in summary[wid]["order_data"]:
|
|
order = db.query(Order).filter(Order.id == oid).first()
|
|
summary[wid]["order_data"][oid] = {
|
|
"id": oid,
|
|
"time_open": order.opened_at.strftime("%H:%M") if order else "",
|
|
"time_close": order.closed_at.strftime("%H:%M") if order and order.closed_at else "",
|
|
"table": tables_db.get(order.table_id, f"#{oid}") if order else f"#{oid}",
|
|
"total": 0.0,
|
|
"items": [],
|
|
}
|
|
summary[wid]["order_data"][oid]["total"] += val
|
|
product_name = item.product.name if item.product else f"#{item.product_id}"
|
|
summary[wid]["order_data"][oid]["items"].append(
|
|
{"name": product_name, "quantity": item.quantity}
|
|
)
|
|
|
|
result = []
|
|
for entry in summary.values():
|
|
entry["orders"] = len(entry["order_data"])
|
|
entry["order_data"] = list(entry["order_data"].values())
|
|
result.append(entry)
|
|
|
|
return {"from": start.isoformat(), "to": end.isoformat(), "waiters": result}
|
|
|
|
|
|
@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"),
|
|
waiter_id: Optional[int] = None,
|
|
order_status: Optional[str] = Query(default=None, alias="status"),
|
|
table_id: Optional[int] = None,
|
|
business_day_id: Optional[int] = None,
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
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:
|
|
if from_date:
|
|
q = q.filter(Order.opened_at >= datetime.fromisoformat(from_date))
|
|
if to_date:
|
|
q = q.filter(Order.opened_at <= datetime.fromisoformat(to_date))
|
|
if waiter_id:
|
|
q = q.join(OrderWaiter).filter(OrderWaiter.waiter_id == waiter_id)
|
|
if order_status:
|
|
q = q.filter(Order.status == order_status)
|
|
if table_id:
|
|
q = q.filter(Order.table_id == table_id)
|
|
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")
|
|
def tables_summary(db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
|
tables = db.query(Table).filter(Table.is_active == True).all()
|
|
result = []
|
|
for table in tables:
|
|
active_order = db.query(Order).filter(
|
|
Order.table_id == table.id,
|
|
Order.status.in_(["open", "partially_paid"]),
|
|
).first()
|
|
result.append({
|
|
"table": TableOut.model_validate(table),
|
|
"status": active_order.status if active_order else "free",
|
|
"order_id": active_order.id if active_order else None,
|
|
})
|
|
return result
|
|
|
|
|
|
@router.get("/printers")
|
|
def printer_totals(
|
|
from_date: Optional[str] = Query(default=None, alias="from"),
|
|
to_date: Optional[str] = Query(default=None, alias="to"),
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
"""Returns totals per printer based on print_log entries in the date range."""
|
|
q = db.query(PrintLog).filter(PrintLog.success == True)
|
|
if from_date:
|
|
q = q.filter(PrintLog.printed_at >= datetime.fromisoformat(from_date))
|
|
if to_date:
|
|
q = q.filter(PrintLog.printed_at <= datetime.fromisoformat(to_date))
|
|
logs = q.all()
|
|
|
|
printers_db = {p.id: p for p in db.query(Printer).all()}
|
|
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
|
|
# summary[pid] — aggregated totals
|
|
summary: dict[int, dict] = {}
|
|
# order_map[pid][order_id] — per-order detail with items
|
|
order_map: dict[int, dict] = {}
|
|
|
|
for log in logs:
|
|
pid = log.printer_id
|
|
if pid not in summary:
|
|
printer = printers_db.get(pid)
|
|
summary[pid] = {
|
|
"printer_id": pid,
|
|
"printer_name": printer.name if printer else f"Printer #{pid}",
|
|
"print_jobs": 0,
|
|
"orders": set(),
|
|
"items": 0,
|
|
"total": 0.0,
|
|
}
|
|
order_map[pid] = {}
|
|
|
|
summary[pid]["print_jobs"] += 1
|
|
summary[pid]["orders"].add(log.order_id)
|
|
|
|
oid = log.order_id
|
|
if oid not in order_map[pid]:
|
|
order = db.query(Order).filter(Order.id == oid).first()
|
|
order_map[pid][oid] = {
|
|
"order_id": oid,
|
|
"time": log.printed_at.strftime("%H:%M"),
|
|
"table": tables_db.get(order.table_id, f"#{oid}") if order else f"#{oid}",
|
|
"total": 0.0,
|
|
"items": [],
|
|
}
|
|
|
|
try:
|
|
item_ids = json.loads(log.item_ids)
|
|
except Exception:
|
|
item_ids = []
|
|
for item_id in item_ids:
|
|
item = db.query(OrderItem).filter(OrderItem.id == item_id).first()
|
|
if item and item.status in ("active", "paid"):
|
|
summary[pid]["items"] += item.quantity
|
|
val = item.unit_price * item.quantity
|
|
summary[pid]["total"] += val
|
|
order_map[pid][oid]["total"] += val
|
|
product_name = item.product.name if item.product else f"#{item.product_id}"
|
|
order_map[pid][oid]["items"].append({"name": product_name, "quantity": item.quantity})
|
|
|
|
result = []
|
|
for pid, entry in summary.items():
|
|
entry["orders"] = len(entry["orders"])
|
|
entry["order_data"] = list(order_map.get(pid, {}).values())
|
|
result.append(entry)
|
|
return {"printers": result}
|
|
|
|
|
|
class PrintWaiterReportBody(BaseModel):
|
|
waiter_name: str
|
|
printer_id: int
|
|
mode: str # "simple" | "extensive"
|
|
from_dt: str
|
|
to_dt: str
|
|
|
|
|
|
class PrintPrinterReportBody(BaseModel):
|
|
printer_target_id: int
|
|
printer_id: int
|
|
mode: str # "simple" | "extensive"
|
|
from_dt: str
|
|
to_dt: str
|
|
|
|
|
|
class PrintOrderBody(BaseModel):
|
|
printer_id: int
|
|
|
|
|
|
@router.post("/print/waiter")
|
|
def print_waiter(
|
|
body: PrintWaiterReportBody,
|
|
background_tasks: BackgroundTasks,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
|
|
if not printer:
|
|
raise HTTPException(status_code=404, detail="Printer not found or inactive")
|
|
|
|
from_dt = datetime.fromisoformat(body.from_dt)
|
|
to_dt = datetime.fromisoformat(body.to_dt)
|
|
|
|
# Gather orders for this waiter in time range
|
|
waiter = db.query(User).filter(User.username == body.waiter_name).first()
|
|
q = db.query(Order).filter(
|
|
Order.opened_at >= from_dt,
|
|
Order.opened_at <= to_dt,
|
|
)
|
|
if waiter:
|
|
q = q.filter(Order.opened_by == waiter.id)
|
|
else:
|
|
q = q.filter(False)
|
|
orders = q.all()
|
|
|
|
# Enrich with table names
|
|
tables = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
order_data = []
|
|
for o in orders:
|
|
active_items = [i for i in o.items if i.status in ("active", "paid")]
|
|
total = sum(i.unit_price * i.quantity for i in active_items)
|
|
order_data.append({
|
|
"id": o.id,
|
|
"time_open": o.opened_at.strftime("%H:%M"),
|
|
"time_close": o.closed_at.strftime("%H:%M") if o.closed_at else "",
|
|
"table": tables.get(o.table_id, f"#{o.table_id}"),
|
|
"total": total,
|
|
"items": [
|
|
{"name": (i.product.name if i.product else f"#{i.product_id}"), "quantity": i.quantity}
|
|
for i in active_items
|
|
],
|
|
})
|
|
|
|
items_count = sum(
|
|
i.quantity for o in orders for i in o.items if i.status in ("active", "paid")
|
|
)
|
|
grand_total = sum(d["total"] for d in order_data)
|
|
|
|
report = {
|
|
"waiter_name": body.waiter_name,
|
|
"orders": len(orders),
|
|
"items": items_count,
|
|
"total": grand_total,
|
|
"order_data": order_data if body.mode == "extensive" else [],
|
|
"from_dt": from_dt.strftime("%d/%m/%Y %H:%M"),
|
|
"to_dt": to_dt.strftime("%d/%m/%Y %H:%M"),
|
|
}
|
|
|
|
background_tasks.add_task(print_waiter_report, printer.ip_address, printer.port, report, body.mode)
|
|
return {"status": "printing"}
|
|
|
|
|
|
@router.post("/print/printer")
|
|
def print_printer_totals(
|
|
body: PrintPrinterReportBody,
|
|
background_tasks: BackgroundTasks,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
|
|
if not printer:
|
|
raise HTTPException(status_code=404, detail="Printer not found or inactive")
|
|
|
|
target_printer = db.query(Printer).filter(Printer.id == body.printer_target_id).first()
|
|
target_name = target_printer.name if target_printer else f"Printer #{body.printer_target_id}"
|
|
|
|
from_dt = datetime.fromisoformat(body.from_dt)
|
|
to_dt = datetime.fromisoformat(body.to_dt)
|
|
|
|
logs = db.query(PrintLog).filter(
|
|
PrintLog.printer_id == body.printer_target_id,
|
|
PrintLog.success == True,
|
|
PrintLog.printed_at >= from_dt,
|
|
PrintLog.printed_at <= to_dt,
|
|
).all()
|
|
|
|
tables = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
|
|
# Build per-order entries keyed by order_id; each log may add more items
|
|
order_map: dict = {}
|
|
items_count = 0
|
|
grand_total = 0.0
|
|
for log in logs:
|
|
oid = log.order_id
|
|
if oid not in order_map:
|
|
order = db.query(Order).filter(Order.id == oid).first()
|
|
if order:
|
|
order_map[oid] = {
|
|
"id": oid,
|
|
"time": log.printed_at.strftime("%H:%M"),
|
|
"table": tables.get(order.table_id, f"#{order.table_id}"),
|
|
"total": 0.0,
|
|
"items": [],
|
|
}
|
|
try:
|
|
item_ids = json.loads(log.item_ids)
|
|
except Exception:
|
|
item_ids = []
|
|
for item_id in item_ids:
|
|
item = db.query(OrderItem).filter(OrderItem.id == item_id).first()
|
|
if item and item.status in ("active", "paid"):
|
|
items_count += item.quantity
|
|
val = item.unit_price * item.quantity
|
|
grand_total += val
|
|
if oid in order_map:
|
|
order_map[oid]["total"] += val
|
|
product_name = item.product.name if item.product else f"#{item.product_id}"
|
|
order_map[oid]["items"].append({"name": product_name, "quantity": item.quantity})
|
|
|
|
order_data = list(order_map.values())
|
|
|
|
report = {
|
|
"printer_name": target_name,
|
|
"print_jobs": len(logs),
|
|
"orders": len(order_map),
|
|
"items": items_count,
|
|
"total": grand_total,
|
|
"order_data": order_data if body.mode == "extensive" else [],
|
|
"from_dt": from_dt.strftime("%d/%m/%Y %H:%M"),
|
|
"to_dt": to_dt.strftime("%d/%m/%Y %H:%M"),
|
|
}
|
|
|
|
background_tasks.add_task(print_printer_report, printer.ip_address, printer.port, report, body.mode)
|
|
return {"status": "printing"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shift history report
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/shifts")
|
|
def shifts_report(
|
|
waiter_id: Optional[int] = None,
|
|
business_day_id: Optional[int] = None,
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
active_only: bool = False,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
from routers.shifts import compute_shift_total
|
|
|
|
q = db.query(WaiterShift)
|
|
if waiter_id:
|
|
q = q.filter(WaiterShift.waiter_id == waiter_id)
|
|
if business_day_id:
|
|
q = q.filter(WaiterShift.business_day_id == business_day_id)
|
|
if from_dt:
|
|
q = q.filter(WaiterShift.started_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(WaiterShift.started_at <= datetime.fromisoformat(to_dt))
|
|
if active_only:
|
|
q = q.filter(WaiterShift.ended_at == None)
|
|
|
|
shifts = q.order_by(WaiterShift.started_at.desc()).all()
|
|
waiters_db = {u.id: u for u in db.query(User).all()}
|
|
|
|
result = []
|
|
for shift in shifts:
|
|
w = waiters_db.get(shift.waiter_id)
|
|
wname = (w.full_name or w.username) if w else f"#{shift.waiter_id}"
|
|
total = compute_shift_total(shift.id, db) if shift.ended_at is None else (shift.total_collected or 0.0)
|
|
result.append({
|
|
"id": shift.id,
|
|
"waiter_id": shift.waiter_id,
|
|
"waiter_name": wname,
|
|
"business_day_id": shift.business_day_id,
|
|
"started_at": _dt(shift.started_at),
|
|
"ended_at": _dt(shift.ended_at),
|
|
"starting_cash": shift.starting_cash,
|
|
"total_collected": total,
|
|
"net_to_deliver": round(total + (shift.starting_cash or 0.0), 2),
|
|
"is_active": shift.ended_at is None,
|
|
"notes": shift.notes,
|
|
})
|
|
|
|
return {"shifts": result}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Product performance analytics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/products/performance")
|
|
def product_performance(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
category_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
from models.product import Product
|
|
|
|
q = db.query(OrderItem).filter(OrderItem.status.in_(["active", "paid"]))
|
|
if from_dt:
|
|
q = q.filter(OrderItem.added_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(OrderItem.added_at <= datetime.fromisoformat(to_dt))
|
|
if business_day_id:
|
|
q = q.join(Order).filter(Order.business_day_id == business_day_id)
|
|
|
|
items = q.all()
|
|
products_db = {p.id: p for p in db.query(Product).all()}
|
|
|
|
summary: dict = {}
|
|
for item in items:
|
|
pid = item.product_id
|
|
product = products_db.get(pid)
|
|
if category_id and (not product or product.category_id != category_id):
|
|
continue
|
|
if pid not in summary:
|
|
summary[pid] = {
|
|
"product_id": pid,
|
|
"product_name": product.name if product else f"#{pid}",
|
|
"category_id": product.category_id if product else None,
|
|
"qty_sold": 0,
|
|
"revenue": 0.0,
|
|
"order_ids": set(),
|
|
}
|
|
summary[pid]["qty_sold"] += item.quantity
|
|
summary[pid]["revenue"] += item.unit_price * item.quantity
|
|
summary[pid]["order_ids"].add(item.order_id)
|
|
|
|
result = []
|
|
for entry in summary.values():
|
|
entry["order_count"] = len(entry.pop("order_ids"))
|
|
entry["revenue"] = round(entry["revenue"], 2)
|
|
result.append(entry)
|
|
|
|
result.sort(key=lambda x: x["qty_sold"], reverse=True)
|
|
return {"products": result}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Table performance analytics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/tables/performance")
|
|
def table_performance(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
q = db.query(Order).filter(Order.status.in_(["closed", "paid"]))
|
|
if from_dt:
|
|
q = q.filter(Order.opened_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(Order.opened_at <= datetime.fromisoformat(to_dt))
|
|
if business_day_id:
|
|
q = q.filter(Order.business_day_id == business_day_id)
|
|
orders = q.all()
|
|
|
|
tables_db = {t.id: t for t in db.query(Table).all()}
|
|
|
|
summary: dict = {}
|
|
for order in orders:
|
|
tid = order.table_id
|
|
if tid not in summary:
|
|
t = tables_db.get(tid)
|
|
summary[tid] = {
|
|
"table_id": tid,
|
|
"table_name": (t.label or f"T{t.number}") if t else f"#{tid}",
|
|
"order_count": 0,
|
|
"revenue": 0.0,
|
|
"durations": [],
|
|
}
|
|
summary[tid]["order_count"] += 1
|
|
summary[tid]["revenue"] += sum(
|
|
i.unit_price * i.quantity for i in order.items if i.status in ("active", "paid")
|
|
)
|
|
if order.closed_at and order.opened_at:
|
|
summary[tid]["durations"].append(
|
|
(order.closed_at - order.opened_at).total_seconds() / 60
|
|
)
|
|
|
|
result = []
|
|
for entry in summary.values():
|
|
durations = entry.pop("durations")
|
|
entry["avg_duration_minutes"] = round(sum(durations) / len(durations), 1) if durations else None
|
|
entry["revenue"] = round(entry["revenue"], 2)
|
|
result.append(entry)
|
|
|
|
result.sort(key=lambda x: x["revenue"], reverse=True)
|
|
return {"tables": result}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Traffic analysis (hour-of-day / day-of-week)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/traffic")
|
|
def traffic_analysis(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
q = db.query(Order)
|
|
if from_dt:
|
|
q = q.filter(Order.opened_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(Order.opened_at <= datetime.fromisoformat(to_dt))
|
|
if business_day_id:
|
|
q = q.filter(Order.business_day_id == business_day_id)
|
|
orders = q.all()
|
|
|
|
by_hour = {h: {"hour": h, "orders": 0, "revenue": 0.0} for h in range(24)}
|
|
day_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
by_weekday = {d: {"day": d, "label": day_labels[d], "orders": 0, "revenue": 0.0} for d in range(7)}
|
|
|
|
for order in orders:
|
|
revenue = sum(
|
|
i.unit_price * i.quantity for i in order.items if i.status in ("active", "paid")
|
|
)
|
|
h = order.opened_at.hour
|
|
d = order.opened_at.weekday()
|
|
by_hour[h]["orders"] += 1
|
|
by_hour[h]["revenue"] += revenue
|
|
by_weekday[d]["orders"] += 1
|
|
by_weekday[d]["revenue"] += revenue
|
|
|
|
for h in by_hour:
|
|
by_hour[h]["revenue"] = round(by_hour[h]["revenue"], 2)
|
|
for d in by_weekday:
|
|
by_weekday[d]["revenue"] = round(by_weekday[d]["revenue"], 2)
|
|
|
|
return {
|
|
"by_hour": list(by_hour.values()),
|
|
"by_weekday": list(by_weekday.values()),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Business days list
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/business-days")
|
|
def business_days_list(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
q = db.query(BusinessDay)
|
|
if from_dt:
|
|
q = q.filter(BusinessDay.opened_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(BusinessDay.opened_at <= datetime.fromisoformat(to_dt))
|
|
days = q.order_by(BusinessDay.opened_at.desc()).all()
|
|
|
|
waiters_db = {u.id: u for u in db.query(User).all()}
|
|
result = []
|
|
for d in days:
|
|
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"]
|
|
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
|
|
)
|
|
opener = waiters_db.get(d.opened_by_id)
|
|
closer = waiters_db.get(d.closed_by_id) if d.closed_by_id else None
|
|
result.append({
|
|
"id": d.id,
|
|
"status": d.status,
|
|
"opened_at": _dt(d.opened_at),
|
|
"closed_at": _dt(d.closed_at),
|
|
"opened_by": (opener.full_name or opener.username) if opener else None,
|
|
"closed_by": (closer.full_name or closer.username) if closer else None,
|
|
"notes": d.notes,
|
|
"order_count": len(orders),
|
|
"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}
|
|
|
|
|
|
@router.get("/business-days/current")
|
|
def current_business_day(
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
day = db.query(BusinessDay).filter(BusinessDay.status == "open").order_by(BusinessDay.opened_at.desc()).first()
|
|
if not day:
|
|
return {"business_day": None}
|
|
|
|
orders = db.query(Order).filter(Order.business_day_id == day.id).all()
|
|
closed_orders = [o for o in orders if o.status in ("closed", "paid")]
|
|
open_orders = [o for o in orders if o.status in ("open", "partially_paid")]
|
|
cancelled_orders = [o for o in orders if o.status == "cancelled"]
|
|
active_shifts = db.query(WaiterShift).filter(
|
|
WaiterShift.business_day_id == day.id,
|
|
WaiterShift.ended_at == None,
|
|
).all()
|
|
revenue = sum(
|
|
sum(i.unit_price * i.quantity for i in o.items if i.status in ("active", "paid"))
|
|
for o in closed_orders
|
|
)
|
|
|
|
item_counts: dict = {}
|
|
for o in orders:
|
|
if o.status == "cancelled":
|
|
continue
|
|
for item in o.items:
|
|
if item.status in ("active", "paid"):
|
|
pid = item.product_id
|
|
item_counts[pid] = item_counts.get(pid, 0) + item.quantity
|
|
top_product_id = max(item_counts, key=item_counts.get) if item_counts else None
|
|
top_product = None
|
|
if top_product_id:
|
|
from models.product import Product
|
|
p = db.query(Product).filter(Product.id == top_product_id).first()
|
|
top_product = {"id": top_product_id, "name": p.name if p else f"#{top_product_id}", "qty": item_counts[top_product_id]}
|
|
|
|
return {
|
|
"business_day": {
|
|
"id": day.id,
|
|
"opened_at": _dt(day.opened_at),
|
|
"revenue": round(revenue, 2),
|
|
"orders_closed": len(closed_orders),
|
|
"orders_open": len(open_orders),
|
|
"active_waiters": len(active_shifts),
|
|
"cancellations": len(cancelled_orders),
|
|
"top_product": top_product,
|
|
}
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Revenue trends
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/revenue/trends")
|
|
def revenue_trends(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
granularity: str = Query(default="daily"), # daily | weekly | monthly
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
q = db.query(Order).filter(Order.status.in_(["closed", "paid"]))
|
|
if from_dt:
|
|
q = q.filter(Order.opened_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(Order.opened_at <= datetime.fromisoformat(to_dt))
|
|
orders = q.all()
|
|
|
|
buckets: dict = {}
|
|
for order in orders:
|
|
dt = order.opened_at
|
|
if granularity == "monthly":
|
|
key = dt.strftime("%Y-%m")
|
|
elif granularity == "weekly":
|
|
monday = dt - timedelta(days=dt.weekday())
|
|
key = monday.strftime("%Y-%m-%d")
|
|
else:
|
|
key = dt.strftime("%Y-%m-%d")
|
|
|
|
if key not in buckets:
|
|
buckets[key] = {"date": key, "revenue": 0.0, "orders": 0}
|
|
rev = sum(i.unit_price * i.quantity for i in order.items if i.status in ("active", "paid"))
|
|
buckets[key]["revenue"] += rev
|
|
buckets[key]["orders"] += 1
|
|
|
|
arr = sorted(buckets.values(), key=lambda x: x["date"])
|
|
for d in arr:
|
|
d["revenue"] = round(d["revenue"], 2)
|
|
|
|
if granularity == "daily":
|
|
for i, d in enumerate(arr):
|
|
window = arr[max(0, i - 6): i + 1]
|
|
d["rolling7"] = round(sum(x["revenue"] for x in window) / len(window), 2)
|
|
|
|
return {"trends": arr, "granularity": granularity}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Category performance
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/categories/performance")
|
|
def category_performance(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
from models.product import Product, Category
|
|
|
|
q = db.query(OrderItem).filter(OrderItem.status.in_(["active", "paid"]))
|
|
if from_dt:
|
|
q = q.filter(OrderItem.added_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(OrderItem.added_at <= datetime.fromisoformat(to_dt))
|
|
if business_day_id:
|
|
q = q.join(Order).filter(Order.business_day_id == business_day_id)
|
|
items = q.all()
|
|
|
|
products_db = {p.id: p for p in db.query(Product).all()}
|
|
categories_db = {c.id: c for c in db.query(Category).all()}
|
|
|
|
summary: dict = {}
|
|
for item in items:
|
|
product = products_db.get(item.product_id)
|
|
if not product:
|
|
continue
|
|
cid = product.category_id
|
|
if cid not in summary:
|
|
cat = categories_db.get(cid)
|
|
summary[cid] = {
|
|
"category_id": cid,
|
|
"category_name": cat.name if cat else f"#{cid}",
|
|
"color": cat.color if cat and hasattr(cat, "color") else None,
|
|
"units_sold": 0,
|
|
"revenue": 0.0,
|
|
"product_ids": set(),
|
|
}
|
|
summary[cid]["units_sold"] += item.quantity
|
|
summary[cid]["revenue"] += item.unit_price * item.quantity
|
|
summary[cid]["product_ids"].add(item.product_id)
|
|
|
|
total_rev = sum(v["revenue"] for v in summary.values())
|
|
result = []
|
|
for entry in summary.values():
|
|
entry["product_count"] = len(entry.pop("product_ids"))
|
|
entry["revenue"] = round(entry["revenue"], 2)
|
|
entry["pct"] = round((entry["revenue"] / total_rev * 100) if total_rev else 0, 1)
|
|
result.append(entry)
|
|
|
|
result.sort(key=lambda x: x["revenue"], reverse=True)
|
|
return {"categories": result}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cancellations log
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/cancellations")
|
|
def cancellations_log(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
waiter_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
q = db.query(OrderItem).filter(OrderItem.status == "cancelled")
|
|
if from_dt:
|
|
q = q.filter(OrderItem.added_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(OrderItem.added_at <= datetime.fromisoformat(to_dt))
|
|
if business_day_id:
|
|
q = q.join(Order).filter(Order.business_day_id == business_day_id)
|
|
if waiter_id:
|
|
q = q.filter(OrderItem.cancelled_by == waiter_id)
|
|
|
|
items = q.order_by(OrderItem.added_at.desc()).all()
|
|
waiters_db = {u.id: u for u in db.query(User).all()}
|
|
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
|
|
result = []
|
|
for item in items:
|
|
order = db.query(Order).filter(Order.id == item.order_id).first()
|
|
table_name = tables_db.get(order.table_id, f"#{order.table_id}") if order else "—"
|
|
waiter = waiters_db.get(item.added_by)
|
|
waiter_name = (waiter.full_name or waiter.username) if waiter else f"#{item.added_by}"
|
|
cancelled_by_user = waiters_db.get(item.cancelled_by) if item.cancelled_by else None
|
|
cancelled_by_name = (cancelled_by_user.full_name or cancelled_by_user.username) if cancelled_by_user else None
|
|
product_name = item.product.name if item.product else f"#{item.product_id}"
|
|
cancelled_at = getattr(item, "cancelled_at", None)
|
|
result.append({
|
|
"id": item.id,
|
|
"order_id": item.order_id,
|
|
"table": table_name,
|
|
"waiter_name": waiter_name,
|
|
"product_name": product_name,
|
|
"quantity": item.quantity,
|
|
"unit_price": item.unit_price,
|
|
"value": round(item.unit_price * item.quantity, 2),
|
|
"cancelled_by": cancelled_by_name,
|
|
"cancel_reason": getattr(item, "cancel_reason", None),
|
|
"cancelled_at": _dt(cancelled_at) if cancelled_at else _dt(item.added_at),
|
|
})
|
|
|
|
total_value = sum(r["value"] for r in result)
|
|
return {"cancellations": result, "total_value": round(total_value, 2)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Printer history (detailed log)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/printers/history")
|
|
def printer_history(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
printer_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
q = db.query(PrintLog)
|
|
if from_dt:
|
|
q = q.filter(PrintLog.printed_at >= datetime.fromisoformat(from_dt))
|
|
if to_dt:
|
|
q = q.filter(PrintLog.printed_at <= datetime.fromisoformat(to_dt))
|
|
if printer_id:
|
|
q = q.filter(PrintLog.printer_id == printer_id)
|
|
if business_day_id:
|
|
q = q.join(Order).filter(Order.business_day_id == business_day_id)
|
|
|
|
logs = q.order_by(PrintLog.printed_at.desc()).all()
|
|
printers_db = {p.id: p for p in db.query(Printer).all()}
|
|
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
|
|
result = []
|
|
for log in logs:
|
|
order = db.query(Order).filter(Order.id == log.order_id).first()
|
|
printer = printers_db.get(log.printer_id)
|
|
try:
|
|
item_ids = json.loads(log.item_ids)
|
|
except Exception:
|
|
item_ids = []
|
|
items = []
|
|
for iid in item_ids:
|
|
oi = db.query(OrderItem).filter(OrderItem.id == iid).first()
|
|
if oi:
|
|
pname = oi.product.name if oi.product else f"#{oi.product_id}"
|
|
items.append({"name": pname, "quantity": oi.quantity})
|
|
result.append({
|
|
"id": log.id,
|
|
"printed_at": _dt(log.printed_at),
|
|
"printer_name": printer.name if printer else f"#{log.printer_id}",
|
|
"order_id": log.order_id,
|
|
"table": tables_db.get(order.table_id, "—") if order else "—",
|
|
"items": items,
|
|
"success": log.success,
|
|
"error_message": log.error_message,
|
|
})
|
|
|
|
total = len(result)
|
|
failed = sum(1 for r in result if not r["success"])
|
|
return {"logs": result, "total": total, "failed": failed}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Waiters list (for filter dropdowns)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/meta/waiters")
|
|
def meta_waiters(
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
waiters = db.query(User).filter(User.role == "waiter", User.is_active == True).all()
|
|
return {"waiters": [{"id": w.id, "name": w.full_name or w.username} for w in waiters]}
|
|
|
|
|
|
@router.get("/meta/tables")
|
|
def meta_tables(
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
tables = db.query(Table).filter(Table.is_active == True).all()
|
|
return {"tables": [{"id": t.id, "name": t.label or f"T{t.number}", "group": t.group.name if t.group else None} for t in tables]}
|
|
|
|
|
|
@router.get("/meta/printers")
|
|
def meta_printers(
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
printers = db.query(Printer).filter(Printer.is_active == True).all()
|
|
return {"printers": [{"id": p.id, "name": p.name} for p in printers]}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CSV export endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _csv_response(rows: list, filename: str) -> StreamingResponse:
|
|
if not rows:
|
|
rows = [{}]
|
|
output = io.StringIO()
|
|
writer = csv.DictWriter(output, fieldnames=list(rows[0].keys()))
|
|
writer.writeheader()
|
|
writer.writerows(rows)
|
|
output.seek(0)
|
|
return StreamingResponse(
|
|
iter([output.getvalue()]),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
|
|
|
|
@router.get("/shifts/export")
|
|
def shifts_export(
|
|
waiter_id: Optional[int] = None,
|
|
business_day_id: Optional[int] = None,
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
data = shifts_report(waiter_id=waiter_id, business_day_id=business_day_id, from_dt=from_dt, to_dt=to_dt, active_only=False, db=db, user=user)
|
|
rows = []
|
|
for s in data["shifts"]:
|
|
rows.append({
|
|
"id": s["id"],
|
|
"waiter": s["waiter_name"],
|
|
"started_at": s["started_at"],
|
|
"ended_at": s["ended_at"] or "",
|
|
"starting_cash": s["starting_cash"],
|
|
"total_collected": s["total_collected"],
|
|
"net_to_deliver": s["net_to_deliver"],
|
|
"status": "active" if s["is_active"] else "ended",
|
|
})
|
|
date_str = (from_dt or "")[:10]
|
|
return _csv_response(rows, f"shifts-{date_str}.csv")
|
|
|
|
|
|
@router.get("/orders/export")
|
|
def orders_export(
|
|
from_date: Optional[str] = Query(default=None, alias="from"),
|
|
to_date: Optional[str] = Query(default=None, alias="to"),
|
|
waiter_id: Optional[int] = None,
|
|
order_status: Optional[str] = Query(default=None, alias="status"),
|
|
table_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
orders = order_history(from_date=from_date, to_date=to_date, waiter_id=waiter_id, order_status=order_status, table_id=table_id, page=1, page_size=10000, db=db, user=user)
|
|
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
waiters_db = {u.id: u for u in db.query(User).all()}
|
|
rows = []
|
|
for o in orders:
|
|
waiter = waiters_db.get(o.opened_by)
|
|
rows.append({
|
|
"id": o.id,
|
|
"table": tables_db.get(o.table_id, ""),
|
|
"waiter": (waiter.full_name or waiter.username) if waiter else "",
|
|
"opened_at": _dt(o.opened_at),
|
|
"closed_at": _dt(o.closed_at) if o.closed_at else "",
|
|
"status": o.status,
|
|
"total": round(sum(i.unit_price * i.quantity for i in o.items if i.status in ("active", "paid")), 2),
|
|
})
|
|
date_str = (from_date or "")[:10]
|
|
return _csv_response(rows, f"orders-{date_str}.csv")
|
|
|
|
|
|
@router.get("/products/export")
|
|
def products_export(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
category_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
data = product_performance(from_dt=from_dt, to_dt=to_dt, business_day_id=business_day_id, category_id=category_id, db=db, user=user)
|
|
rows = [{
|
|
"product_id": p["product_id"],
|
|
"product_name": p["product_name"],
|
|
"category_id": p.get("category_id", ""),
|
|
"qty_sold": p["qty_sold"],
|
|
"revenue": p["revenue"],
|
|
"order_count": p["order_count"],
|
|
} for p in data["products"]]
|
|
date_str = (from_dt or "")[:10]
|
|
return _csv_response(rows, f"products-{date_str}.csv")
|
|
|
|
|
|
@router.get("/printers/export")
|
|
def printers_export(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
printer_id: Optional[int] = None,
|
|
business_day_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
data = printer_history(from_dt=from_dt, to_dt=to_dt, business_day_id=business_day_id, printer_id=printer_id, db=db, user=user)
|
|
rows = [{
|
|
"id": r["id"],
|
|
"printed_at": r["printed_at"],
|
|
"printer": r["printer_name"],
|
|
"order_id": r["order_id"],
|
|
"table": r["table"],
|
|
"success": r["success"],
|
|
"error_message": r.get("error_message") or "",
|
|
} for r in data["logs"]]
|
|
date_str = (from_dt or "")[:10]
|
|
return _csv_response(rows, f"printer-history-{date_str}.csv")
|
|
|
|
|
|
@router.get("/cancellations/export")
|
|
def cancellations_export(
|
|
from_dt: Optional[str] = Query(default=None, alias="from"),
|
|
to_dt: Optional[str] = Query(default=None, alias="to"),
|
|
business_day_id: Optional[int] = None,
|
|
waiter_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
data = cancellations_log(from_dt=from_dt, to_dt=to_dt, business_day_id=business_day_id, waiter_id=waiter_id, db=db, user=user)
|
|
rows = [{
|
|
"id": c["id"],
|
|
"order_id": c["order_id"],
|
|
"table": c["table"],
|
|
"waiter": c["waiter_name"],
|
|
"product": c["product_name"],
|
|
"quantity": c["quantity"],
|
|
"value": c["value"],
|
|
"cancelled_by": c.get("cancelled_by") or "",
|
|
"reason": c.get("cancel_reason") or "",
|
|
"cancelled_at": c["cancelled_at"],
|
|
} for c in data["cancellations"]]
|
|
date_str = (from_dt or "")[:10]
|
|
return _csv_response(rows, f"cancellations-{date_str}.csv")
|