fix: tell receivers when an order is no longer available + fix disabled apps disappearing from admin
## Task #80 — Tell receivers when an order is no longer available to claim ### Problem When a receiver tried to act on an order that had already been cancelled, completed, or claimed by someone else, the UI showed a generic "Could not complete the action" toast. The stale card also stayed in the list, requiring a manual refresh. ### Changes **artifacts/api-server/src/routes/service-orders.ts** - PATCH /orders/:id/confirm-receipt: after a failed atomic claim, query the current order status; return 409 `order_unavailable` if it's cancelled/completed (vs the existing 409 `already_claimed` for when it was grabbed by someone else first) - PATCH /orders/:id/status (preparing/completed branch): terminal-state check runs BEFORE permission gating — returns 409 `order_unavailable` when order is already cancelled/completed instead of ever hitting 403 Forbidden - PATCH /orders/:id/status (cancel branch): same — terminal-state check moved to the top of the branch so non-admin receivers trying to cancel an already-terminal order get 409 `order_unavailable` (not 403 Forbidden which would never let the UI remove the stale card) **artifacts/tx-os/src/pages/orders-incoming.tsx** - handleConfirmReceipt onError: distinguish 409 `order_unavailable` from `already_claimed`, show specific toast, remove stale card via invalidate() - handleSetStatus onError: same pattern — specific toast + invalidate() on `order_unavailable` - handleCancel onError: same pattern **artifacts/tx-os/src/locales/en.json + ar.json** - Added `incomingOrders.orderUnavailable` ("This order is no longer available" / "هذا الطلب لم يعد متاحاً") ## Bonus fix — Disabled app disappears from admin panel (user-reported) ### Problem When an admin disabled an app via the toggle, the app disappeared from the admin panel too (isActive filter applied to everyone), making it impossible to re-enable without direct DB access. ### Changes **artifacts/api-server/src/routes/apps.ts** - getVisibleAppsForUser: admins now receive ALL apps including inactive ones via conditional $dynamic() Drizzle query; non-admins still only see active apps **artifacts/tx-os/src/pages/home.tsx** - Filter orderedApps to only active apps before rendering so inactive apps don't appear on the home screen even for admins **artifacts/tx-os/src/pages/admin.tsx** - Inactive app cards show reduced opacity + a "Disabled/معطّل" badge **artifacts/tx-os/src/locales/en.json + ar.json** - Added `admin.appDisabled` ("Disabled" / "معطّل")
This commit is contained in:
@@ -37,7 +37,9 @@ async function getVisibleAppsForUser(userId: number): Promise<typeof appsTable.$
|
||||
const isAdmin = adminRoleRows.length > 0;
|
||||
|
||||
// Pull the user's preferred order, then order by COALESCE(user_order, app.sort_order, name).
|
||||
const orderedRows = await db
|
||||
// Admins receive ALL apps (including inactive ones) so they can re-enable them.
|
||||
// Non-admins only receive active apps.
|
||||
const baseQuery = db
|
||||
.select({
|
||||
app: appsTable,
|
||||
userSort: userAppOrdersTable.sortOrder,
|
||||
@@ -47,11 +49,15 @@ async function getVisibleAppsForUser(userId: number): Promise<typeof appsTable.$
|
||||
userAppOrdersTable,
|
||||
sql`${userAppOrdersTable.appId} = ${appsTable.id} and ${userAppOrdersTable.userId} = ${userId}`,
|
||||
)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(
|
||||
sql`COALESCE(${userAppOrdersTable.sortOrder}, ${appsTable.sortOrder})`,
|
||||
asc(appsTable.nameEn),
|
||||
);
|
||||
.$dynamic();
|
||||
|
||||
const orderedRows = await (isAdmin
|
||||
? baseQuery
|
||||
: baseQuery.where(eq(appsTable.isActive, true))
|
||||
).orderBy(
|
||||
sql`COALESCE(${userAppOrdersTable.sortOrder}, ${appsTable.sortOrder})`,
|
||||
asc(appsTable.nameEn),
|
||||
);
|
||||
|
||||
const apps = orderedRows.map((r) => r.app);
|
||||
|
||||
|
||||
@@ -248,7 +248,18 @@ router.patch(
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
res.status(409).json({ error: "already_claimed" });
|
||||
// Distinguish: was it claimed by someone else, or is it no longer claimable
|
||||
// (cancelled / completed / already in a terminal state)?
|
||||
const current = await db
|
||||
.select({ status: serviceOrdersTable.status })
|
||||
.from(serviceOrdersTable)
|
||||
.where(eq(serviceOrdersTable.id, orderId));
|
||||
const currentStatus = current[0]?.status;
|
||||
if (!currentStatus || currentStatus === "cancelled" || currentStatus === "completed") {
|
||||
res.status(409).json({ error: "order_unavailable" });
|
||||
} else {
|
||||
res.status(409).json({ error: "already_claimed" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,6 +356,13 @@ router.patch(
|
||||
return;
|
||||
}
|
||||
} else if (next === "preparing" || next === "completed") {
|
||||
// If the order is already terminal (cancelled or completed by someone
|
||||
// else), surface a specific error so the receiver UI can show the right
|
||||
// message and remove the stale card.
|
||||
if (existing.status === "cancelled" || existing.status === "completed") {
|
||||
res.status(409).json({ error: "order_unavailable" });
|
||||
return;
|
||||
}
|
||||
// Only the assigned receiver or admin
|
||||
const isAssignee = existing.assignedTo === userId;
|
||||
if (!isAssignee && !admin) {
|
||||
@@ -365,6 +383,12 @@ router.patch(
|
||||
res.status(400).json({ error: "invalid_transition" });
|
||||
return;
|
||||
} else if (next === "cancelled") {
|
||||
// Check terminal state first so the receiver sees the right message
|
||||
// regardless of permission gating order.
|
||||
if (existing.status === "cancelled" || existing.status === "completed") {
|
||||
res.status(409).json({ error: "order_unavailable" });
|
||||
return;
|
||||
}
|
||||
const isOwner = existing.userId === userId;
|
||||
const isAssignee = existing.assignedTo === userId;
|
||||
const cancellableByOwner =
|
||||
@@ -379,11 +403,6 @@ router.patch(
|
||||
res.status(403).json({ error: "Forbidden" });
|
||||
return;
|
||||
}
|
||||
// Admin can cancel any status. Already-cancelled is a no-op invalid transition.
|
||||
if (existing.status === "cancelled") {
|
||||
res.status(400).json({ error: "invalid_transition" });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(400).json({ error: "invalid_transition" });
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user