feat: bump client-services (accumulated feature work + deploy fixes)
Snapshot of in-progress work across local_backend, manager_dashboard, and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM, inventory, permissions), plus the nginx/docker-compose deploy fixes for the Unraid + NPM reverse-proxy setup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,24 @@ def compute_shift_total(shift_id: int, db: Session) -> float:
|
||||
return round(sum(i.unit_price * i.quantity for i in items), 2)
|
||||
|
||||
|
||||
def compute_shift_payment_split(shift_id: int, db: Session) -> dict:
|
||||
"""Return cash_sales and card_sales for a shift (items paid in this shift)."""
|
||||
items = db.query(OrderItem).filter(
|
||||
OrderItem.paid_in_shift_id == shift_id,
|
||||
OrderItem.status == "paid",
|
||||
).all()
|
||||
cash = 0.0
|
||||
card = 0.0
|
||||
for i in items:
|
||||
amount = float(i.unit_price) * i.quantity
|
||||
method = (i.payment_method or "cash").lower()
|
||||
if method == "card":
|
||||
card += amount
|
||||
else:
|
||||
cash += amount
|
||||
return {"cash_sales": round(cash, 2), "card_sales": round(card, 2)}
|
||||
|
||||
|
||||
def compute_shift_pay(shift: WaiterShift) -> dict:
|
||||
"""Return duration_hours and shift_pay. shift_pay is None if no rate snapshot."""
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -74,6 +92,7 @@ def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
|
||||
w = shift.waiter
|
||||
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)
|
||||
split = compute_shift_payment_split(shift.id, db)
|
||||
pay_data = compute_shift_pay(shift)
|
||||
# Count cancelled items and their value attributed to this waiter during shift window
|
||||
cancelled_q = db.query(OrderItem).filter(
|
||||
@@ -96,7 +115,9 @@ def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
|
||||
"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),
|
||||
"cash_sales": split["cash_sales"],
|
||||
"card_sales": split["card_sales"],
|
||||
"net_to_deliver": round(split["cash_sales"] + (shift.starting_cash or 0.0), 2),
|
||||
"is_active": shift.ended_at is None,
|
||||
"notes": shift.notes,
|
||||
"hourly_rate_snapshot": shift.hourly_rate_snapshot,
|
||||
@@ -105,9 +126,11 @@ def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
|
||||
"cancellation_events": cancellation_events,
|
||||
"cancellations": cancellations,
|
||||
"cancellation_value": cancellation_value,
|
||||
# Phase 2E
|
||||
# Phase 2E — discrepancy computed live against cash-only expected (starting_cash + cash_sales)
|
||||
"counted_cash_end": shift.counted_cash_end,
|
||||
"cash_discrepancy": shift.cash_discrepancy,
|
||||
"cash_discrepancy": round(
|
||||
shift.counted_cash_end - (split["cash_sales"] + (shift.starting_cash or 0.0)), 2
|
||||
) if shift.counted_cash_end is not None else None,
|
||||
"breaks": [
|
||||
{"id": b.id, "shift_id": b.shift_id, "started_at": _dt(b.started_at), "ended_at": _dt(b.ended_at)}
|
||||
for b in shift.breaks
|
||||
@@ -133,14 +156,14 @@ def start_shift(
|
||||
target_id = body.waiter_id
|
||||
|
||||
if target_id and target_id != user.id:
|
||||
if user.role not in ("manager", "sysadmin"):
|
||||
raise HTTPException(status_code=403, detail="Only managers can start shifts for other waiters")
|
||||
if not user.perm_access_dashboard and user.role != "superadmin":
|
||||
raise HTTPException(status_code=403, detail="Only managers can start shifts for other staff")
|
||||
target = db.query(User).filter(User.id == target_id, User.is_active == True).first()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="Waiter not found")
|
||||
raise HTTPException(status_code=404, detail="Staff member not found")
|
||||
else:
|
||||
target_id = user.id
|
||||
if user.role == "waiter" and _get_setting(db, "shifts.waiter_self_start") != "true":
|
||||
if not user.perm_access_dashboard and user.role != "superadmin" and _get_setting(db, "shifts.waiter_self_start") != "true":
|
||||
raise HTTPException(status_code=403, detail="Shift start requires manager confirmation")
|
||||
|
||||
active_day = db.query(BusinessDay).filter(BusinessDay.status == "open").first()
|
||||
@@ -173,7 +196,7 @@ def end_shift(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
if user.role == "waiter" and _get_setting(db, "shifts.waiter_self_end") != "true":
|
||||
if not user.perm_access_dashboard and user.role != "superadmin" and _get_setting(db, "shifts.waiter_self_end") != "true":
|
||||
raise HTTPException(status_code=403, detail="Shift end requires manager confirmation")
|
||||
|
||||
shift = db.query(WaiterShift).filter(
|
||||
@@ -189,11 +212,9 @@ def end_shift(
|
||||
shift.ended_at = now
|
||||
if body.notes:
|
||||
shift.notes = body.notes
|
||||
# Phase 2E: cash reconciliation
|
||||
# Phase 2E: cash reconciliation — store counted amount; discrepancy computed live in _enrich_shift
|
||||
if body.counted_cash_end is not None:
|
||||
shift.counted_cash_end = body.counted_cash_end
|
||||
expected = (shift.starting_cash or 0.0) + total
|
||||
shift.cash_discrepancy = round(body.counted_cash_end - expected, 2)
|
||||
|
||||
open_break = db.query(ShiftBreak).filter(
|
||||
ShiftBreak.shift_id == shift.id, ShiftBreak.ended_at == None
|
||||
@@ -262,11 +283,9 @@ def manager_end_shift(
|
||||
shift.ended_at = now
|
||||
if body.notes:
|
||||
shift.notes = body.notes
|
||||
# Phase 2E: cash reconciliation
|
||||
# Phase 2E: cash reconciliation — store counted amount; discrepancy computed live in _enrich_shift
|
||||
if body.counted_cash_end is not None:
|
||||
shift.counted_cash_end = body.counted_cash_end
|
||||
expected = (shift.starting_cash or 0.0) + total
|
||||
shift.cash_discrepancy = round(body.counted_cash_end - expected, 2)
|
||||
|
||||
open_break = db.query(ShiftBreak).filter(
|
||||
ShiftBreak.shift_id == shift.id, ShiftBreak.ended_at == None
|
||||
@@ -288,7 +307,7 @@ def start_break(
|
||||
shift = db.query(WaiterShift).filter(WaiterShift.id == shift_id).first()
|
||||
if not shift:
|
||||
raise HTTPException(status_code=404, detail="Shift not found")
|
||||
if shift.waiter_id != user.id and user.role not in ("manager", "sysadmin"):
|
||||
if shift.waiter_id != user.id and not user.perm_access_dashboard and user.role != "superadmin":
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
if shift.ended_at:
|
||||
raise HTTPException(status_code=400, detail="Shift already ended")
|
||||
@@ -315,7 +334,7 @@ def end_break(
|
||||
shift = db.query(WaiterShift).filter(WaiterShift.id == shift_id).first()
|
||||
if not shift:
|
||||
raise HTTPException(status_code=404, detail="Shift not found")
|
||||
if shift.waiter_id != user.id and user.role not in ("manager", "sysadmin"):
|
||||
if shift.waiter_id != user.id and not user.perm_access_dashboard and user.role != "superadmin":
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
open_break = db.query(ShiftBreak).filter(
|
||||
|
||||
Reference in New Issue
Block a user