Task #69: Delete past orders from My Orders
- 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.
This commit is contained in:
@@ -376,4 +376,59 @@ router.patch(
|
||||
},
|
||||
);
|
||||
|
||||
// DELETE /api/orders/:id — owner can delete terminal orders; admin can delete any
|
||||
router.delete(
|
||||
"/orders/:id",
|
||||
requireAuth,
|
||||
async (req, res): Promise<void> => {
|
||||
const params = ServiceOrderIdParams.safeParse(req.params);
|
||||
if (!params.success) {
|
||||
res.status(400).json({ error: params.error.message });
|
||||
return;
|
||||
}
|
||||
const userId = req.session.userId!;
|
||||
const orderId = params.data.id;
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(serviceOrdersTable)
|
||||
.where(eq(serviceOrdersTable.id, orderId));
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Order not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const admin = await isAdmin(userId);
|
||||
const isOwner = existing.userId === userId;
|
||||
const isTerminal =
|
||||
existing.status === "completed" || existing.status === "cancelled";
|
||||
|
||||
if (!admin && !(isOwner && isTerminal)) {
|
||||
res.status(403).json({ error: "Forbidden" });
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(serviceOrdersTable)
|
||||
.where(eq(serviceOrdersTable.id, orderId));
|
||||
|
||||
await emitToUser(existing.userId, "order_deleted", { id: orderId });
|
||||
if (existing.assignedTo && existing.assignedTo !== existing.userId) {
|
||||
await emitToUser(existing.assignedTo, "order_deleted", { id: orderId });
|
||||
}
|
||||
// If the deleted order was visible to receivers (pending or active),
|
||||
// refresh their incoming queues.
|
||||
if (
|
||||
existing.status === "pending" ||
|
||||
existing.status === "received" ||
|
||||
existing.status === "preparing"
|
||||
) {
|
||||
const receivers = await getReceiverUserIds();
|
||||
await broadcastIncomingChanged(receivers);
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -297,6 +297,70 @@ test("requester gets a distinct notification when the receiver cancels a claimed
|
||||
assert.match(cancelNotif.body_ar, /استلم طلبك ولكن تم إلغاؤه لاحقاً/);
|
||||
});
|
||||
|
||||
test("DELETE /orders/:id — owner can delete completed/cancelled, admin can delete any, others 403", async () => {
|
||||
const owner = await createUser("delOwn", "user");
|
||||
const recv = await createUser("delRecv", "order_receiver");
|
||||
const stranger = await createUser("delStr", "user");
|
||||
const adminUser = await createUser("delAdm", "admin");
|
||||
const co = await login(owner.username);
|
||||
const cr = await login(recv.username);
|
||||
const cs = await login(stranger.username);
|
||||
const ca = await login(adminUser.username);
|
||||
|
||||
// Helper to create an order owned by `co`
|
||||
const placeOrder = async () => {
|
||||
const r = await call(co, "POST", "/orders", { serviceId });
|
||||
assert.equal(r.status, 201);
|
||||
const o = await r.json();
|
||||
createdOrderIds.push(o.id);
|
||||
return o;
|
||||
};
|
||||
|
||||
// 1) Pending order: owner cannot delete (not terminal)
|
||||
const o1 = await placeOrder();
|
||||
let res = await call(co, "DELETE", `/orders/${o1.id}`);
|
||||
assert.equal(res.status, 403);
|
||||
|
||||
// 2) Stranger cannot delete a terminal order they don't own
|
||||
const o2 = await placeOrder();
|
||||
res = await call(co, "PATCH", `/orders/${o2.id}/status`, { status: "cancelled" });
|
||||
assert.equal(res.status, 200);
|
||||
res = await call(cs, "DELETE", `/orders/${o2.id}`);
|
||||
assert.equal(res.status, 403);
|
||||
|
||||
// 3) Owner CAN delete a cancelled order they own
|
||||
res = await call(co, "DELETE", `/orders/${o2.id}`);
|
||||
assert.equal(res.status, 204);
|
||||
// Confirm it's gone from /orders/my
|
||||
res = await call(co, "GET", "/orders/my");
|
||||
const list1 = await res.json();
|
||||
assert.ok(!list1.find((o) => o.id === o2.id));
|
||||
|
||||
// 4) Owner CAN delete a completed order
|
||||
const o3 = await placeOrder();
|
||||
res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`);
|
||||
assert.equal(res.status, 200);
|
||||
res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" });
|
||||
assert.equal(res.status, 200);
|
||||
res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "completed" });
|
||||
assert.equal(res.status, 200);
|
||||
res = await call(co, "DELETE", `/orders/${o3.id}`);
|
||||
assert.equal(res.status, 204);
|
||||
|
||||
// 5) Admin CAN delete a non-terminal (pending) order
|
||||
const o4 = await placeOrder();
|
||||
res = await call(ca, "DELETE", `/orders/${o4.id}`);
|
||||
assert.equal(res.status, 204);
|
||||
|
||||
// 6) 404 for unknown id
|
||||
res = await call(co, "DELETE", `/orders/9999999`);
|
||||
assert.equal(res.status, 404);
|
||||
|
||||
// 7) Unauthenticated → 401
|
||||
res = await fetch(`${API_BASE}/api/orders/1`, { method: "DELETE" });
|
||||
assert.equal(res.status, 401);
|
||||
});
|
||||
|
||||
test("unauthenticated cannot place order", async () => {
|
||||
const res = await fetch(`${API_BASE}/api/orders`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -44,6 +44,15 @@ export function useNotificationsSocket() {
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("order_deleted", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListMyServiceOrdersQueryKey(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListIncomingServiceOrdersQueryKey(),
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("order_incoming_changed", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListIncomingServiceOrdersQueryKey(),
|
||||
|
||||
@@ -131,7 +131,22 @@
|
||||
"cancelConfirm": "نعم، إلغاء",
|
||||
"keep": "احتفظ به",
|
||||
"cancelled": "تم إلغاء الطلب",
|
||||
"cancelFailed": "تعذّر إلغاء الطلب"
|
||||
"cancelFailed": "تعذّر إلغاء الطلب",
|
||||
"delete": "حذف",
|
||||
"deleteConfirmTitle": "حذف هذا الطلب؟",
|
||||
"deleteConfirmBody": "سيتم حذف الطلب نهائياً من سجلّك.",
|
||||
"deleteConfirm": "نعم، احذف",
|
||||
"deleted": "تم حذف الطلب",
|
||||
"deleteFailed": "تعذّر حذف الطلب",
|
||||
"clearFinished_one": "مسح طلب منتهٍ",
|
||||
"clearFinished_other": "مسح {{count}} طلبات منتهية",
|
||||
"clearConfirmTitle": "مسح الطلبات المنتهية؟",
|
||||
"clearConfirmBody_one": "سيتم حذف طلب واحد منتهٍ نهائياً من سجلّك.",
|
||||
"clearConfirmBody_other": "سيتم حذف {{count}} طلبات منتهية نهائياً من سجلّك.",
|
||||
"clearConfirm": "نعم، امسح",
|
||||
"clearedCount_one": "تم حذف طلب واحد",
|
||||
"clearedCount_other": "تم حذف {{count}} طلبات",
|
||||
"clearedPartial": "تم حذف {{ok}} وفشل {{fail}}"
|
||||
},
|
||||
"orderStatus": {
|
||||
"pending": "بانتظار المستلِم",
|
||||
|
||||
@@ -131,7 +131,22 @@
|
||||
"cancelConfirm": "Yes, cancel",
|
||||
"keep": "Keep it",
|
||||
"cancelled": "Order cancelled",
|
||||
"cancelFailed": "Could not cancel the order"
|
||||
"cancelFailed": "Could not cancel the order",
|
||||
"delete": "Delete",
|
||||
"deleteConfirmTitle": "Delete this order?",
|
||||
"deleteConfirmBody": "This will permanently remove the order from your history.",
|
||||
"deleteConfirm": "Yes, delete",
|
||||
"deleted": "Order deleted",
|
||||
"deleteFailed": "Could not delete the order",
|
||||
"clearFinished_one": "Clear 1 finished",
|
||||
"clearFinished_other": "Clear {{count}} finished",
|
||||
"clearConfirmTitle": "Clear finished orders?",
|
||||
"clearConfirmBody_one": "1 finished order will be permanently removed from your history.",
|
||||
"clearConfirmBody_other": "{{count}} finished orders will be permanently removed from your history.",
|
||||
"clearConfirm": "Yes, clear",
|
||||
"clearedCount_one": "1 order deleted",
|
||||
"clearedCount_other": "{{count}} orders deleted",
|
||||
"clearedPartial": "{{ok}} deleted, {{fail}} failed"
|
||||
},
|
||||
"orderStatus": {
|
||||
"pending": "Awaiting receiver",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useListMyServiceOrders,
|
||||
getListMyServiceOrdersQueryKey,
|
||||
useUpdateServiceOrderStatus,
|
||||
useDeleteServiceOrder,
|
||||
type ServiceOrder,
|
||||
type ServiceOrderStatus,
|
||||
} from "@workspace/api-client-react";
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
Flame,
|
||||
PackageCheck,
|
||||
XCircle,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -51,6 +53,10 @@ const STATUS_PILL: Record<ServiceOrderStatus, string> = {
|
||||
cancelled: "bg-rose-100 text-rose-700",
|
||||
};
|
||||
|
||||
function isFinished(status: ServiceOrderStatus) {
|
||||
return status === "completed" || status === "cancelled";
|
||||
}
|
||||
|
||||
function StatusPill({ status }: { status: ServiceOrderStatus }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
@@ -149,13 +155,16 @@ function OrderCard({ order }: { order: ServiceOrder }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const lang = i18n.language;
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [confirmCancelOpen, setConfirmCancelOpen] = useState(false);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const updateStatus = useUpdateServiceOrderStatus();
|
||||
const deleteOrder = useDeleteServiceOrder();
|
||||
|
||||
const name = lang === "ar" ? order.service.nameAr : order.service.nameEn;
|
||||
const img = resolveServiceImageUrl(order.service.imageUrl);
|
||||
const cancellable =
|
||||
order.status === "pending" || order.status === "received";
|
||||
const deletable = isFinished(order.status);
|
||||
|
||||
const handleCancel = () => {
|
||||
updateStatus.mutate(
|
||||
@@ -166,7 +175,7 @@ function OrderCard({ order }: { order: ServiceOrder }) {
|
||||
queryKey: getListMyServiceOrdersQueryKey(),
|
||||
});
|
||||
toast({ title: t("myOrders.cancelled") });
|
||||
setConfirmOpen(false);
|
||||
setConfirmCancelOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
@@ -178,6 +187,27 @@ function OrderCard({ order }: { order: ServiceOrder }) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteOrder.mutate(
|
||||
{ id: order.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListMyServiceOrdersQueryKey(),
|
||||
});
|
||||
toast({ title: t("myOrders.deleted") });
|
||||
setConfirmDeleteOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: t("myOrders.deleteFailed"),
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass-panel rounded-xl p-4 flex flex-col gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -205,21 +235,36 @@ function OrderCard({ order }: { order: ServiceOrder }) {
|
||||
|
||||
<StatusTimeline status={order.status} />
|
||||
|
||||
{cancellable && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-rose-700 hover:text-rose-800 hover:bg-rose-50"
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
disabled={updateStatus.isPending}
|
||||
>
|
||||
{t("myOrders.cancel")}
|
||||
</Button>
|
||||
{(cancellable || deletable) && (
|
||||
<div className="flex justify-end gap-1">
|
||||
{cancellable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-rose-700 hover:text-rose-800 hover:bg-rose-50"
|
||||
onClick={() => setConfirmCancelOpen(true)}
|
||||
disabled={updateStatus.isPending}
|
||||
>
|
||||
{t("myOrders.cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{deletable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-500 hover:text-rose-700 hover:bg-rose-50"
|
||||
onClick={() => setConfirmDeleteOpen(true)}
|
||||
disabled={deleteOrder.isPending}
|
||||
aria-label={t("myOrders.delete")}
|
||||
title={t("myOrders.delete")}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<AlertDialog open={confirmCancelOpen} onOpenChange={setConfirmCancelOpen}>
|
||||
<AlertDialogContent dir={lang === "ar" ? "rtl" : "ltr"}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
@@ -246,6 +291,34 @@ function OrderCard({ order }: { order: ServiceOrder }) {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={confirmDeleteOpen} onOpenChange={setConfirmDeleteOpen}>
|
||||
<AlertDialogContent dir={lang === "ar" ? "rtl" : "ltr"}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("myOrders.deleteConfirmTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("myOrders.deleteConfirmBody")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteOrder.isPending}>
|
||||
{t("myOrders.keep")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDelete();
|
||||
}}
|
||||
disabled={deleteOrder.isPending}
|
||||
className="bg-rose-600 hover:bg-rose-700 text-white"
|
||||
>
|
||||
{t("myOrders.deleteConfirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -253,9 +326,14 @@ function OrderCard({ order }: { order: ServiceOrder }) {
|
||||
export default function MyOrdersPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [, setLocation] = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const lang = i18n.language;
|
||||
const isRtl = lang === "ar";
|
||||
const BackIcon = isRtl ? ArrowRight : ArrowLeft;
|
||||
const [confirmClearOpen, setConfirmClearOpen] = useState(false);
|
||||
const deleteOrder = useDeleteServiceOrder();
|
||||
const [bulkPending, setBulkPending] = useState(false);
|
||||
|
||||
const { data: orders, isLoading, isError, refetch } = useListMyServiceOrders({
|
||||
query: { queryKey: getListMyServiceOrdersQueryKey() },
|
||||
@@ -268,6 +346,36 @@ export default function MyOrdersPage() {
|
||||
)
|
||||
: [];
|
||||
|
||||
const finishedOrders = sortedOrders.filter((o) => isFinished(o.status));
|
||||
const finishedCount = finishedOrders.length;
|
||||
|
||||
const handleClearFinished = async () => {
|
||||
setBulkPending(true);
|
||||
let okCount = 0;
|
||||
let failCount = 0;
|
||||
for (const o of finishedOrders) {
|
||||
try {
|
||||
await deleteOrder.mutateAsync({ id: o.id });
|
||||
okCount += 1;
|
||||
} catch {
|
||||
failCount += 1;
|
||||
}
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListMyServiceOrdersQueryKey(),
|
||||
});
|
||||
setBulkPending(false);
|
||||
setConfirmClearOpen(false);
|
||||
if (failCount === 0) {
|
||||
toast({ title: t("myOrders.clearedCount", { count: okCount }) });
|
||||
} else {
|
||||
toast({
|
||||
title: t("myOrders.clearedPartial", { ok: okCount, fail: failCount }),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen os-bg flex flex-col">
|
||||
<div className="glass-panel border-b border-slate-200/70 px-4 py-4 flex items-center gap-3 sticky top-0 z-10">
|
||||
@@ -319,12 +427,54 @@ export default function MyOrdersPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{finishedCount > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-600 hover:text-rose-700 hover:bg-rose-50 gap-1.5"
|
||||
onClick={() => setConfirmClearOpen(true)}
|
||||
disabled={bulkPending}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
{t("myOrders.clearFinished", { count: finishedCount })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{sortedOrders.map((o) => (
|
||||
<OrderCard key={o.id} order={o} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog open={confirmClearOpen} onOpenChange={setConfirmClearOpen}>
|
||||
<AlertDialogContent dir={lang === "ar" ? "rtl" : "ltr"}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("myOrders.clearConfirmTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("myOrders.clearConfirmBody", { count: finishedCount })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={bulkPending}>
|
||||
{t("myOrders.keep")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleClearFinished();
|
||||
}}
|
||||
disabled={bulkPending}
|
||||
className="bg-rose-600 hover:bg-rose-700 text-white"
|
||||
>
|
||||
{t("myOrders.clearConfirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4379,6 +4379,94 @@ export const useDeleteUser = <
|
||||
return useMutation(getDeleteUserMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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).
|
||||
|
||||
* @summary Delete a service order
|
||||
*/
|
||||
export const getDeleteServiceOrderUrl = (id: number) => {
|
||||
return `/api/orders/${id}`;
|
||||
};
|
||||
|
||||
export const deleteServiceOrder = async (
|
||||
id: number,
|
||||
options?: RequestInit,
|
||||
): Promise<void> => {
|
||||
return customFetch<void>(getDeleteServiceOrderUrl(id), {
|
||||
...options,
|
||||
method: "DELETE",
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteServiceOrderMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceOrder>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceOrder>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["deleteServiceOrder"];
|
||||
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 deleteServiceOrder>>,
|
||||
{ id: number }
|
||||
> = (props) => {
|
||||
const { id } = props ?? {};
|
||||
|
||||
return deleteServiceOrder(id, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteServiceOrderMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteServiceOrder>>
|
||||
>;
|
||||
|
||||
export type DeleteServiceOrderMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Delete a service order
|
||||
*/
|
||||
export const useDeleteServiceOrder = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceOrder>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteServiceOrder>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteServiceOrderMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Idempotently add a role to a user (admin)
|
||||
*/
|
||||
|
||||
@@ -1125,6 +1125,37 @@ paths:
|
||||
"204":
|
||||
description: Deleted
|
||||
|
||||
/orders/{id}:
|
||||
delete:
|
||||
operationId: deleteServiceOrder
|
||||
tags: [orders]
|
||||
summary: Delete a service order
|
||||
description: |
|
||||
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).
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"204":
|
||||
description: Deleted
|
||||
"403":
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Order not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/users/{id}/roles:
|
||||
post:
|
||||
operationId: addUserRole
|
||||
|
||||
@@ -1405,6 +1405,17 @@ export const DeleteUserParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* 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).
|
||||
|
||||
* @summary Delete a service order
|
||||
*/
|
||||
export const DeleteServiceOrderParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Idempotently add a role to a user (admin)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user