Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() helper and refactored DELETE /orders/:id
into shared authorizeAndDeleteOrder() so single and bulk paths use the
same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization (per code review): receivers may only
delete pending/received/preparing orders; terminal (completed/cancelled)
orders remain the owner's cleanup responsibility. OpenAPI description
and the implementation now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas; updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — useBulkDelete
ServiceOrders is now available.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Selection mode toggle in the page header (RTL-safe).
- Per-card Checkbox plus a Select-all control.
- Sticky bottom action bar with destructive Delete and selected count.
- AlertDialog confirmation using pluralized i18n keys; toast surfaces
partial-failure results when some ids couldn't be deleted.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization.
Tests: Added two new tests in service-orders.test.mjs covering receiver
delete on incoming queue, receiver forbidden on terminal orders, and
bulk-delete with mixed authorized / unknown / dedup / unauthenticated
cases. Both pass. Pre-existing executive-meetings PDF/font test failures
are unrelated.
This commit is contained in:
@@ -622,6 +622,21 @@ export interface UpdateServiceOrderStatusBody {
|
||||
status: UpdateServiceOrderStatusBodyStatus;
|
||||
}
|
||||
|
||||
export interface BulkDeleteServiceOrdersBody {
|
||||
/**
|
||||
* @minItems 1
|
||||
* @maxItems 200
|
||||
*/
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export interface BulkDeleteServiceOrdersResponse {
|
||||
/** Ids that were successfully deleted. */
|
||||
deletedIds: number[];
|
||||
/** Ids that were skipped (not found, or caller is not authorized). */
|
||||
failedIds: number[];
|
||||
}
|
||||
|
||||
export interface ParticipantInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
|
||||
@@ -35,6 +35,8 @@ import type {
|
||||
AppSettings,
|
||||
AuditLogList,
|
||||
AuthUser,
|
||||
BulkDeleteServiceOrdersBody,
|
||||
BulkDeleteServiceOrdersResponse,
|
||||
ConversationWithDetails,
|
||||
CreateAppBody,
|
||||
CreateConversationBody,
|
||||
@@ -4962,9 +4964,105 @@ export const useDeleteUser = <
|
||||
};
|
||||
|
||||
/**
|
||||
* Permanently deletes the order. Allowed when the caller owns the order
|
||||
AND it is in a terminal status (completed/cancelled), OR the caller is
|
||||
an admin (any status).
|
||||
* Deletes several orders in one round-trip. Authorization is still
|
||||
enforced per id using the same rules as DELETE /orders/{id}, so the
|
||||
client cannot bypass the permission check by batching. Returns a
|
||||
summary of which ids were deleted and which were rejected.
|
||||
|
||||
* @summary Delete multiple service orders in one request
|
||||
*/
|
||||
export const getBulkDeleteServiceOrdersUrl = () => {
|
||||
return `/api/orders/bulk-delete`;
|
||||
};
|
||||
|
||||
export const bulkDeleteServiceOrders = async (
|
||||
bulkDeleteServiceOrdersBody: BulkDeleteServiceOrdersBody,
|
||||
options?: RequestInit,
|
||||
): Promise<BulkDeleteServiceOrdersResponse> => {
|
||||
return customFetch<BulkDeleteServiceOrdersResponse>(
|
||||
getBulkDeleteServiceOrdersUrl(),
|
||||
{
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(bulkDeleteServiceOrdersBody),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getBulkDeleteServiceOrdersMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof bulkDeleteServiceOrders>>,
|
||||
TError,
|
||||
{ data: BodyType<BulkDeleteServiceOrdersBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof bulkDeleteServiceOrders>>,
|
||||
TError,
|
||||
{ data: BodyType<BulkDeleteServiceOrdersBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["bulkDeleteServiceOrders"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof bulkDeleteServiceOrders>>,
|
||||
{ data: BodyType<BulkDeleteServiceOrdersBody> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return bulkDeleteServiceOrders(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type BulkDeleteServiceOrdersMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof bulkDeleteServiceOrders>>
|
||||
>;
|
||||
export type BulkDeleteServiceOrdersMutationBody =
|
||||
BodyType<BulkDeleteServiceOrdersBody>;
|
||||
export type BulkDeleteServiceOrdersMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Delete multiple service orders in one request
|
||||
*/
|
||||
export const useBulkDeleteServiceOrders = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof bulkDeleteServiceOrders>>,
|
||||
TError,
|
||||
{ data: BodyType<BulkDeleteServiceOrdersBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof bulkDeleteServiceOrders>>,
|
||||
TError,
|
||||
{ data: BodyType<BulkDeleteServiceOrdersBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getBulkDeleteServiceOrdersMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* Permanently deletes the order. Allowed when the caller is an admin,
|
||||
when the caller owns the order AND it is in a terminal status
|
||||
(completed/cancelled), OR when the caller holds the `orders.receive`
|
||||
permission (i.e. a receiver clearing items from their incoming queue).
|
||||
|
||||
* @summary Delete a service order
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user