Auth middleware:
- Add getEffectiveRoleIds() that UNIONs user_roles with group_roles via
group memberships, used by requireAdmin / requirePermission /
userHasPermission / getUserRoles.
- requireAdmin and requirePermission now also reject inactive users
with 401 (matching requireAuth), closing a session-after-deactivation
bypass.
Groups routes:
- POST /groups and PATCH /groups/:id now wrap the group row write and
all assignment writes in a single db.transaction via
applyGroupAssignmentsTx(tx, ...), so partial state cannot leak.
- validateAssignmentIds rejects unknown app/role/user ids with 400
before any insert.
- Removed AI-slop: void or, void sql, as-unknown-as casts; conditions
use Drizzle's SQL union type.
Users route:
- /api/users supports q, groupId, status filters (server-side).
Admin UI (teaboy-os/admin.tsx):
- UsersPanel wires q/groupId/status to the backend, shows display name
and preferred language inline per row.
- UserGroupsEditor now edits display names (ar/en), preferred language,
active status, and group membership with a search box.
- GroupsPanel adds a top-level group search box.
- GroupDetailEditor Users tab adds a user search box.
Infra:
- scripts/post-merge.sh runs the seed (idempotent) so default groups
Admins / TeaBoy / Everyone always exist after merges.
Tests (artifacts/api-server/tests/groups-crud.test.mjs, all passing):
- Admin-only access (regular user gets 403).
- Default seed groups exist.
- Create group + member assignment.
- Bad userIds yields 400 with no leaked group row.
- Admin role inherited via group_roles grants admin access.
- Deactivated admin session is rejected with 401.
- Group create rolls back atomically when assignment fails.
- /api/users q + groupId + status filters return correct rows.
Notes / drift:
- "Roles" tab inside GroupDetailEditor and groupIds in CreateUserBody
remain as proposed follow-ups (require OpenAPI spec changes).
- Pre-existing pagination-test flake unrelated to this work.
Auth middleware:
- Add getEffectiveRoleIds() that UNIONs user_roles with group_roles via
group memberships, used by requireAdmin / requirePermission /
userHasPermission / getUserRoles.
- requireAdmin and requirePermission now also reject inactive users
with 401 (matching requireAuth), closing a session-after-deactivation
bypass.
Groups routes:
- POST /groups and PATCH /groups/:id now wrap the group row write and
all assignment writes in a single db.transaction via
applyGroupAssignmentsTx(tx, ...), so partial state cannot leak.
- validateAssignmentIds rejects unknown app/role/user ids with 400
before any insert.
- Removed AI-slop: void or, void sql, as-unknown-as casts; conditions
use Drizzle's SQL union type.
Users route:
- /api/users supports q, groupId, status filters (server-side).
Admin UI (teaboy-os/admin.tsx):
- UsersPanel wires q/groupId/status to the backend, shows display name
and preferred language inline per row.
- UserGroupsEditor now edits display names (ar/en), preferred language,
active status, and group membership with a search box.
- GroupsPanel adds a top-level group search box.
- GroupDetailEditor Users tab adds a user search box.
Infra:
- scripts/post-merge.sh runs the seed (idempotent) so default groups
Admins / TeaBoy / Everyone always exist after merges.
Tests (artifacts/api-server/tests/groups-crud.test.mjs, all passing):
- Admin-only access (regular user gets 403).
- Default seed groups exist.
- Create group + member assignment.
- Bad userIds yields 400 with no leaked group row.
- Admin role inherited via group_roles grants admin access.
- Deactivated admin session is rejected with 401.
- Group create rolls back atomically when assignment fails.
- /api/users q + groupId + status filters return correct rows.
Notes / drift:
- "Roles" tab inside GroupDetailEditor and groupIds in CreateUserBody
remain as proposed follow-ups (require OpenAPI spec changes).
- Pre-existing pagination-test flake unrelated to this work.
Backend:
- New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts)
- Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users
- /api/groups CRUD with admin guard, batch counts, system-group delete protection
- Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in
a single DB transaction (no partial state on failure)
- /api/users gains q/groupId/status filters, batch role+group loading, groupIds
replacement on PATCH, auto-assigns Everyone on admin-create
- /auth/register also auto-assigns Everyone for consistent default linkage
- buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema)
- App visibility (getVisibleAppsForUser) unions group-granted apps via
group_apps + user_groups in addition to existing permission gating
Frontend (admin.tsx):
- Nav restructured: User Management section with Users + Groups children
- Section deep-linked via #section=… URL hash
- Users page rebuilt: search, group filter, status filter, sortable table,
groups column, edit-groups dialog, mobile cards
- New Groups page: cards with member/app/role counts, create dialog,
detail editor with Info/Apps/Users tabs and system-group guard
- ar/en translations added for all new keys
Testing:
- pnpm typecheck clean (api + web)
- 25/26 api tests pass; the only failure is pre-existing flaky pagination test
(admin-app-opens-pagination) — left as-is per scratchpad note
- Code review feedback addressed (validation, transactions, register auto-assign)
Backend:
- New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts)
- Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users
- /api/groups CRUD with admin guard, batch counts, system-group delete protection
- Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in
a single DB transaction (no partial state on failure)
- /api/users gains q/groupId/status filters, batch role+group loading, groupIds
replacement on PATCH, auto-assigns Everyone on admin-create
- /auth/register also auto-assigns Everyone for consistent default linkage
- buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema)
- App visibility (getVisibleAppsForUser) unions group-granted apps via
group_apps + user_groups in addition to existing permission gating
Frontend (admin.tsx):
- Nav restructured: User Management section with Users + Groups children
- Section deep-linked via #section=… URL hash
- Users page rebuilt: search, group filter, status filter, sortable table,
groups column, edit-groups dialog, mobile cards
- New Groups page: cards with member/app/role counts, create dialog,
detail editor with Info/Apps/Users tabs and system-group guard
- ar/en translations added for all new keys
Testing:
- pnpm typecheck clean (api + web)
- 25/26 api tests pass; the only failure is pre-existing flaky pagination test
(admin-app-opens-pagination) — left as-is per scratchpad note
- Code review feedback addressed (validation, transactions, register auto-assign)
Task: Audit notification rules in service-orders.ts so users never
receive notifications about actions they themselves initiated. The
specific scenario: a user with the `orders.receive` permission who
cancels their own assigned order would notify themselves under the
old logic.
Approach:
- Added an optional `actorId` parameter to `notifyUser`. When the
notification recipient equals the actor, the function returns
early (no DB insert, no socket emit).
- Threaded `actorId` through the three notification call sites:
1. POST /orders — placing an order no longer notifies the placer
even if they also hold the receiver role.
2. PATCH /orders/:id/confirm-receipt — a receiver claiming their
own order no longer notifies themselves.
3. PATCH /orders/:id/status — replaced the narrow
`!initiatedByOwner` guard with the generic `actorId === userId`
check inside `notifyUser`. This now covers owner-cancel as
before AND owner-as-assignee transitions (preparing/completed/
cancelled) where the owner happens to also be the assignee.
Notes / deviations:
- Kept the existing distinguishing wording for receiver-initiated
cancels ("claimed but later cancelled by the receiver"); it's
simply suppressed when the receiver is also the owner.
- No schema or API contract changes; purely server-side notification
filtering. Typecheck passes.
- No follow-ups proposed: existing project tasks already cover
automated tests for order/cancel flow, re-order from history, and
an undo window for cancellations.
Task: Audit notification rules in service-orders.ts so users never
receive notifications about actions they themselves initiated. The
specific scenario: a user with the `orders.receive` permission who
cancels their own assigned order would notify themselves under the
old logic.
Approach:
- Added an optional `actorId` parameter to `notifyUser`. When the
notification recipient equals the actor, the function returns
early (no DB insert, no socket emit).
- Threaded `actorId` through the three notification call sites:
1. POST /orders — placing an order no longer notifies the placer
even if they also hold the receiver role.
2. PATCH /orders/:id/confirm-receipt — a receiver claiming their
own order no longer notifies themselves.
3. PATCH /orders/:id/status — replaced the narrow
`!initiatedByOwner` guard with the generic `actorId === userId`
check inside `notifyUser`. This now covers owner-cancel as
before AND owner-as-assignee transitions (preparing/completed/
cancelled) where the owner happens to also be the assignee.
Notes / deviations:
- Kept the existing distinguishing wording for receiver-initiated
cancels ("claimed but later cancelled by the receiver"); it's
simply suppressed when the receiver is also the owner.
- No schema or API contract changes; purely server-side notification
filtering. Typecheck passes.
- No follow-ups proposed: existing project tasks already cover
automated tests for order/cancel flow, re-order from history, and
an undo window for cancellations.
Replit-Task-Id: 65b6d89a-6882-42f8-adb7-6ac1a2560748
Task #70: After deleting a finished order from My Orders, users had no way
to recover it. This change introduces a short undo window so accidental
deletes can be reversed while keeping the cleanup workflow snappy.
Approach:
- Implemented as a client-side deferred-delete in
artifacts/teaboy-os/src/pages/my-orders.tsx. No backend changes.
- On delete (single trash icon or bulk "Clear N finished"):
* The order(s) are added to a local pendingDeleteIds set so they
immediately disappear from the list.
* A toast is shown with a localised "Undo" action and a 7s duration.
* A setTimeout is scheduled to call DELETE /api/orders/:id for the
affected ids and then invalidate the my-orders query.
- Clicking "Undo" cancels the timer, removes the ids from
pendingDeleteIds (so the cards reappear), dismisses the toast, and
shows a "Restored" confirmation toast.
- On unmount, any still-pending deletions are flushed so they aren't
silently lost if the user navigates away mid-window.
- If the deferred DELETE later fails, the ids are restored to the list
and a "Could not delete the order" destructive toast is shown.
OrderCard now delegates the actual delete to a new onDelete callback
from the page (the confirmation dialog stays in the card). Bulk clear
no longer fires N sequential mutations from inside a confirm dialog
spinner — it just schedules them.
i18n: added myOrders.undo / myOrders.undone strings to en.json and
ar.json.
Verification:
- pnpm tsc --noEmit passes for teaboy-os.
- e2e (runTest) covered: single delete + undo restores; single delete
with no undo becomes permanent after 7s; bulk clear + undo restores
all. DB state was asserted in each case.
Follow-up proposed: #72 — apply the same undo pattern to cancelled
orders.
Task #70: After deleting a finished order from My Orders, users had no way
to recover it. This change introduces a short undo window so accidental
deletes can be reversed while keeping the cleanup workflow snappy.
Approach:
- Implemented as a client-side deferred-delete in
artifacts/teaboy-os/src/pages/my-orders.tsx. No backend changes.
- On delete (single trash icon or bulk "Clear N finished"):
* The order(s) are added to a local pendingDeleteIds set so they
immediately disappear from the list.
* A toast is shown with a localised "Undo" action and a 7s duration.
* A setTimeout is scheduled to call DELETE /api/orders/:id for the
affected ids and then invalidate the my-orders query.
- Clicking "Undo" cancels the timer, removes the ids from
pendingDeleteIds (so the cards reappear), dismisses the toast, and
shows a "Restored" confirmation toast.
- On unmount, any still-pending deletions are flushed so they aren't
silently lost if the user navigates away mid-window.
- If the deferred DELETE later fails, the ids are restored to the list
and a "Could not delete the order" destructive toast is shown.
OrderCard now delegates the actual delete to a new onDelete callback
from the page (the confirmation dialog stays in the card). Bulk clear
no longer fires N sequential mutations from inside a confirm dialog
spinner — it just schedules them.
i18n: added myOrders.undo / myOrders.undone strings to en.json and
ar.json.
Verification:
- pnpm tsc --noEmit passes for teaboy-os.
- e2e (runTest) covered: single delete + undo restores; single delete
with no undo becomes permanent after 7s; bulk clear + undo restores
all. DB state was asserted in each case.
Follow-up proposed: #72 — apply the same undo pattern to cancelled
orders.
Replit-Task-Id: 1854a0b1-f6d3-4148-b2c2-b0940202d0df
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
Context
- After Task #64 receivers can cancel an order they have already claimed.
- Previously the requester's cancellation notification body was the same regardless of who cancelled (admin vs receiver), giving them no signal that their order had been claimed and then dropped.
Changes
- artifacts/api-server/src/routes/service-orders.ts (PATCH /orders/:id/status, cancel branch):
- When the cancellation is initiated by the order's currently assigned receiver
(existing.assignedTo === userId && next === "cancelled"), the requester's
notification body now includes the bilingual phrase
"استلم طلبك ولكن تم إلغاؤه لاحقاً" / "Your order was claimed but later
cancelled by the receiver", appended to the service name for context.
- Admin / non-assignee cancellations keep the existing service-name body so
the two cases remain distinguishable in the requester's notifications list.
- The notification is created via the existing notifyUser() helper, which
persists to notifications and emits notification_created over Socket.IO,
so the bell badge updates in real time.
- artifacts/api-server/tests/service-orders.test.mjs:
- Added "requester gets a distinct notification when the receiver cancels a
claimed order" covering the full flow: place → confirm-receipt → cancel,
then asserts the owner has a cancellation notification whose body matches
the new bilingual receiver-cancel copy.
Verification
- All 7 tests in service-orders.test.mjs pass locally against the running API.
Notes / deviations
- Locale JSON files (ar.json/en.json) were inspected but not modified: notification
copy is stored server-side as titleAr/titleEn/bodyAr/bodyEn on the notifications
row; the client just renders those strings directly.
Context
- After Task #64 receivers can cancel an order they have already claimed.
- Previously the requester's cancellation notification body was the same regardless of who cancelled (admin vs receiver), giving them no signal that their order had been claimed and then dropped.
Changes
- artifacts/api-server/src/routes/service-orders.ts (PATCH /orders/:id/status, cancel branch):
- When the cancellation is initiated by the order's currently assigned receiver
(existing.assignedTo === userId && next === "cancelled"), the requester's
notification body now includes the bilingual phrase
"استلم طلبك ولكن تم إلغاؤه لاحقاً" / "Your order was claimed but later
cancelled by the receiver", appended to the service name for context.
- Admin / non-assignee cancellations keep the existing service-name body so
the two cases remain distinguishable in the requester's notifications list.
- The notification is created via the existing notifyUser() helper, which
persists to notifications and emits notification_created over Socket.IO,
so the bell badge updates in real time.
- artifacts/api-server/tests/service-orders.test.mjs:
- Added "requester gets a distinct notification when the receiver cancels a
claimed order" covering the full flow: place → confirm-receipt → cancel,
then asserts the owner has a cancellation notification whose body matches
the new bilingual receiver-cancel copy.
Verification
- All 7 tests in service-orders.test.mjs pass locally against the running API.
Notes / deviations
- Locale JSON files (ar.json/en.json) were inspected but not modified: notification
copy is stored server-side as titleAr/titleEn/bodyAr/bodyEn on the notifications
row; the client just renders those strings directly.
Replit-Task-Id: 5aa480a9-c6e0-4a06-8919-b2b6784d8a98
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).
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).
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).
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).
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.
What's new:
- Order button on each available service card (hidden when service is
unavailable) opens a focused OrderServiceModal showing only the
service name + image with a multi-line notes textarea (500-char cap
with live counter). Submitting calls POST /orders, shows a toast, and
invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
status pill, a horizontal 4-step timeline (pending → received →
preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
pending or received; it calls PATCH /orders/:id/status with
cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
my-orders query on notification_created (when type === 'order') and
on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
createdAt desc.
Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.
What's new:
- Order button on each available service card (hidden when service is
unavailable) opens a focused OrderServiceModal showing only the
service name + image with a multi-line notes textarea (500-char cap
with live counter). Submitting calls POST /orders, shows a toast, and
invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
status pill, a horizontal 4-step timeline (pending → received →
preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
pending or received; it calls PATCH /orders/:id/status with
cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
my-orders query on notification_created (when type === 'order') and
on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
createdAt desc.
Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.
What's new:
- Order button on each available service card (hidden when service is
unavailable) opens a focused OrderServiceModal showing only the
service name + image with a multi-line notes textarea (500-char cap
with live counter). Submitting calls POST /orders, shows a toast, and
invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
status pill, a horizontal 4-step timeline (pending → received →
preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
pending or received; it calls PATCH /orders/:id/status with
cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
my-orders query on notification_created (when type === 'order') and
on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
createdAt desc.
Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.
What's new:
- Order button on each available service card (hidden when service is
unavailable) opens a focused OrderServiceModal showing only the
service name + image with a multi-line notes textarea (500-char cap
with live counter). Submitting calls POST /orders, shows a toast, and
invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
status pill, a horizontal 4-step timeline (pending → received →
preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
pending or received; it calls PATCH /orders/:id/status with
cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
my-orders query on notification_created (when type === 'order') and
on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
createdAt desc.
Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
- New `service_orders` table (status pending|claimed|delivered|received|cancelled)
- New `orders:receive` permission + `order_receiver` role; admins implicitly allowed
- Added `requirePermission(name)` and `userHasPermission(userId, name)` middleware helpers
- New routes:
- POST /api/orders place order (authenticated)
- GET /api/orders/my list current user's orders
- GET /api/orders/incoming receivers see pending+active visible orders
- PATCH /api/orders/:id/status claim (atomic), deliver, cancel
- PATCH /api/orders/:id/confirm-receipt requester confirms a delivered order
- Atomic claim via UPDATE ... WHERE status='pending' AND assigned_to IS NULL
(returns 409 already_claimed on race)
- Realtime: emits `notification_created` to receivers/requester,
`order_incoming_changed` to all receivers, `order_updated` to requester
- Service shape in order responses limited to id/nameAr/nameEn/imageUrl
(no description fields), per spec
- OpenAPI updated with new paths and schemas; codegen run
- Seed updated idempotently (permission, role, role_permissions)
- New tests in artifacts/api-server/tests/service-orders.test.mjs
(full lifecycle, atomic claim race, unauth rejection) — all 21 api tests pass
No deviations from the planned scope. Tasks #63 (client UI) and #64
(receiver page + admin role toggle) remain blocked-by #62 and are next.
- New `service_orders` table (status pending|claimed|delivered|received|cancelled)
- New `orders:receive` permission + `order_receiver` role; admins implicitly allowed
- Added `requirePermission(name)` and `userHasPermission(userId, name)` middleware helpers
- New routes:
- POST /api/orders place order (authenticated)
- GET /api/orders/my list current user's orders
- GET /api/orders/incoming receivers see pending+active visible orders
- PATCH /api/orders/:id/status claim (atomic), deliver, cancel
- PATCH /api/orders/:id/confirm-receipt requester confirms a delivered order
- Atomic claim via UPDATE ... WHERE status='pending' AND assigned_to IS NULL
(returns 409 already_claimed on race)
- Realtime: emits `notification_created` to receivers/requester,
`order_incoming_changed` to all receivers, `order_updated` to requester
- Service shape in order responses limited to id/nameAr/nameEn/imageUrl
(no description fields), per spec
- OpenAPI updated with new paths and schemas; codegen run
- Seed updated idempotently (permission, role, role_permissions)
- New tests in artifacts/api-server/tests/service-orders.test.mjs
(full lifecycle, atomic claim race, unauth rejection) — all 21 api tests pass
No deviations from the planned scope. Tasks #63 (client UI) and #64
(receiver page + admin role toggle) remain blocked-by #62 and are next.
Integrates the top bar clock's visibility toggle into the ClockStylePicker component and removes the standalone eye button. Updates locale files for Arabic and English to reflect the new string for showing the top bar clock.
Integrates the top bar clock's visibility toggle into the ClockStylePicker component and removes the standalone eye button. Updates locale files for Arabic and English to reflect the new string for showing the top bar clock.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 89fb8094-c538-4564-8b9b-29c0f4236486
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
Introduced a new hook `useTopbarClockVisibility` in `artifacts/teaboy-os/src/components/clock.tsx` to manage the visibility of the top bar clock, separate from the home clock widget. Updated `artifacts/teaboy-os/src/pages/home.tsx` to utilize this new hook, allowing the eye icon to toggle the top bar clock's visibility.
Introduced a new hook `useTopbarClockVisibility` in `artifacts/teaboy-os/src/components/clock.tsx` to manage the visibility of the top bar clock, separate from the home clock widget. Updated `artifacts/teaboy-os/src/pages/home.tsx` to utilize this new hook, allowing the eye icon to toggle the top bar clock's visibility.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 08cff793-79ae-45ad-9958-d029baf1737d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.
- Wired to the existing useHomeClockVisibility hook so it stays in
sync with the toggle inside the clock-style popover (same storage
key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
home.clockStyle.showWidget for aria-label and title (flips with
state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
the popover toggle is left intact as requested.
Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
and the new icon button between ClockStylePicker and the language
toggle).
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.
- Wired to the existing useHomeClockVisibility hook so it stays in
sync with the toggle inside the clock-style popover (same storage
key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
home.clockStyle.showWidget for aria-label and title (flips with
state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
the popover toggle is left intact as requested.
Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
and the new icon button between ClockStylePicker and the language
toggle).
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.
- Wired to the existing useHomeClockVisibility hook so it stays in
sync with the toggle inside the clock-style popover (same storage
key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
home.clockStyle.showWidget for aria-label and title (flips with
state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
the popover toggle is left intact as requested.
Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
and the new icon button between ClockStylePicker and the language
toggle).
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.
- Wired to the existing useHomeClockVisibility hook so it stays in
sync with the toggle inside the clock-style popover (same storage
key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
home.clockStyle.showWidget for aria-label and title (flips with
state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
the popover toggle is left intact as requested.
Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
and the new icon button between ClockStylePicker and the language
toggle).
Update home component logic to correctly pin the clock to the far-left position in RTL layouts when it's unset or dragged to the end of the app list, ensuring it occupies the first two columns.
Update home component logic to correctly pin the clock to the far-left position in RTL layouts when it's unset or dragged to the end of the app list, ensuring it occupies the first two columns.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a6f7b52f-d48d-41b0-9435-36a5f4524394
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true