Task #64: Service Orders — receiver page + admin role toggle

Backend (artifacts/api-server)
- Add admin-only role-toggle endpoints:
  - POST /users/:id/roles { roleName } — idempotent, returns UserProfile
  - DELETE /users/:id/roles/:roleName — idempotent, returns UserProfile
- Allow assigned receiver to cancel their own claimed order
  (received/preparing) in PATCH /orders/:id/status. Owner & admin
  rules unchanged.
- New backend test cases: receiver can cancel own assigned order;
  another receiver gets 403.

API spec / codegen
- Add AddUserRoleBody schema and the two new role endpoints
  under a new "roles" tag in lib/api-spec/openapi.yaml.
- Regenerated api-zod and api-client-react.

Frontend (artifacts/teaboy-os)
- New page src/pages/orders-incoming.tsx at /orders/incoming:
  RBAC-gated (admin || order_receiver), shows "My active orders"
  and "Awaiting receiver" sections, with claim, mark preparing,
  mark completed and cancel buttons. Handles 409 already_claimed.
- Add Inbox button to home top bar, conditional on the same roles.
- Admin users table now has an "Order Receiver" Switch wired to
  the new role-toggle hooks.
- Extend use-notifications-socket to invalidate the incoming-orders
  query on order_incoming_changed and on notification_created with
  type === "order".
- Bilingual locale keys (ar/en) for the new page and admin label.

Tests
- All 25 api-server tests pass (24 existing + 1 new receiver-cancel
  case). All 3 teaboy-os e2e tests pass.

Follow-up filed: #67 (notify requester when receiver cancels).
This commit is contained in:
Riyadh
2026-04-21 18:58:19 +00:00
parent 1bf1c0b9a8
commit 7c1e8bc85f
14 changed files with 899 additions and 3 deletions
@@ -309,9 +309,16 @@ router.patch(
}
} else if (next === "cancelled") {
const isOwner = existing.userId === userId;
const isAssignee = existing.assignedTo === userId;
const cancellableByOwner =
existing.status === "pending" || existing.status === "received";
if (!admin && !(isOwner && cancellableByOwner)) {
const cancellableByAssignee =
existing.status === "received" || existing.status === "preparing";
if (
!admin &&
!(isOwner && cancellableByOwner) &&
!(isAssignee && cancellableByAssignee)
) {
res.status(403).json({ error: "Forbidden" });
return;
}