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:
2026-07-19 10:00:14 +03:00
parent 02ec1aa28f
commit 34ae328b0d
182 changed files with 34874 additions and 3556 deletions

View File

@@ -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(