- OrderDetail/InvoiceDetail/LeaveRequests mutations now invalidate the same domain sets as their list-page twins (orders+offers+projects+invoices / +projects / +users+dashboard) — no more stale project/order labels after a detail-page action. - USER_INVALIDATE hoisted to lib/queries/users.ts; DashProfile self-edit now refreshes trips/attendance/leave/projects like an admin edit. - Global QueryCache.onError toast: failed first-loads no longer masquerade as empty lists (data-present refetches stay silent; Unauthorized skipped; 4s rate limit; meta.suppressGlobalErrorToast opt-out used by offer/issued-order detail + the local TOTP QR query). - Users/Vehicles active-toggle mutations toast on error (was silent revert); clickable status chips got affordance tooltips (incl. warehouse parity). - AuditLog: refetchOnMount always + staleTime 0 (mutations write audit rows but nothing invalidates the key); search debounced; dead auditLogOptions module removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
import { queryOptions } from "@tanstack/react-query";
|
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
|
// Single shared definition (type-only import, erased at compile time) — the
|
|
// canonical CurrencyAmount lives in offers.ts; re-exported here so existing
|
|
// importers keep working.
|
|
import type { CurrencyAmount } from "./offers";
|
|
|
|
export type { CurrencyAmount };
|
|
|
|
export interface IssuedOrder {
|
|
id: number;
|
|
po_number: string | null;
|
|
supplier_id: number | null;
|
|
supplier_name: string | null;
|
|
status: string;
|
|
currency: string | null;
|
|
order_date: string | null;
|
|
// NET total — issued orders carry no VAT (not tax documents).
|
|
total: number;
|
|
}
|
|
|
|
/** Active supplier row from the lightweight PO-picker lookup endpoint. */
|
|
export interface Supplier {
|
|
id: number;
|
|
name: string;
|
|
ico: string | null;
|
|
dic: string | null;
|
|
street: string | null;
|
|
city: string | null;
|
|
postal_code: string | null;
|
|
country: string | null;
|
|
}
|
|
|
|
export interface IssuedOrderItem {
|
|
id?: number;
|
|
description: string | null;
|
|
item_description: string | null;
|
|
quantity: number | string | null;
|
|
unit: string | null;
|
|
unit_price: number | string | null;
|
|
position?: number;
|
|
}
|
|
|
|
/** Rich-text CZ/EN PDF section (mirrors offers' scope sections). */
|
|
export interface IssuedOrderSection {
|
|
id?: number;
|
|
title: string | null;
|
|
title_cz: string | null;
|
|
content: string | null;
|
|
position?: number;
|
|
}
|
|
|
|
export interface IssuedOrderDetail extends IssuedOrder {
|
|
exchange_rate: number | string | null;
|
|
delivery_date: string | null;
|
|
language: string | null;
|
|
order_text: string | null;
|
|
internal_notes: string | null;
|
|
items: IssuedOrderItem[];
|
|
sections: IssuedOrderSection[];
|
|
supplier: Record<string, unknown> | null;
|
|
valid_transitions: string[];
|
|
selected_custom_fields: number[];
|
|
/** Fresh edit lock held by ANOTHER user (same shape as offers), else null. */
|
|
locked_by: {
|
|
user_id: number;
|
|
username: string;
|
|
full_name: string;
|
|
} | null;
|
|
}
|
|
|
|
// Suppliers lookup for the PO supplier picker — hits the issued-orders-scoped
|
|
// endpoint (orders permissions), NOT the warehouse.manage-guarded CRUD list.
|
|
export const issuedOrderSuppliersOptions = () =>
|
|
queryOptions({
|
|
queryKey: ["issued-orders", "suppliers"],
|
|
queryFn: () => jsonQuery<Supplier[]>("/api/admin/issued-orders/suppliers"),
|
|
staleTime: 2 * 60_000,
|
|
});
|
|
|
|
export const issuedOrderListOptions = (filters: {
|
|
search?: string;
|
|
sort?: string;
|
|
order?: string;
|
|
page?: number;
|
|
perPage?: number;
|
|
status?: string;
|
|
supplier_id?: number;
|
|
month?: number;
|
|
year?: number;
|
|
}) =>
|
|
queryOptions({
|
|
queryKey: ["issued-orders", "list", filters],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams();
|
|
if (filters.search) params.set("search", filters.search);
|
|
if (filters.sort) params.set("sort", filters.sort);
|
|
if (filters.order) params.set("order", filters.order);
|
|
if (filters.page) params.set("page", String(filters.page));
|
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
|
if (filters.status) params.set("status", filters.status);
|
|
if (filters.supplier_id)
|
|
params.set("supplier_id", String(filters.supplier_id));
|
|
if (filters.month) params.set("month", String(filters.month));
|
|
if (filters.year) params.set("year", String(filters.year));
|
|
const qs = params.toString();
|
|
return paginatedJsonQuery<IssuedOrder>(
|
|
`/api/admin/issued-orders${qs ? `?${qs}` : ""}`,
|
|
);
|
|
},
|
|
});
|
|
|
|
export const issuedOrderStatsOptions = (filters: {
|
|
search?: string;
|
|
status?: string;
|
|
supplier_id?: number;
|
|
month?: number;
|
|
year?: number;
|
|
}) =>
|
|
queryOptions({
|
|
queryKey: ["issued-orders", "stats", filters],
|
|
queryFn: async () => {
|
|
const params = new URLSearchParams();
|
|
if (filters.search) params.set("search", filters.search);
|
|
if (filters.status) params.set("status", filters.status);
|
|
if (filters.supplier_id)
|
|
params.set("supplier_id", String(filters.supplier_id));
|
|
if (filters.month) params.set("month", String(filters.month));
|
|
if (filters.year) params.set("year", String(filters.year));
|
|
const qs = params.toString();
|
|
const data = await jsonQuery<{ totals?: CurrencyAmount[] }>(
|
|
`/api/admin/issued-orders/stats${qs ? `?${qs}` : ""}`,
|
|
);
|
|
return data.totals ?? [];
|
|
},
|
|
});
|
|
|
|
export const issuedOrderDetailOptions = (id: string | undefined) =>
|
|
queryOptions({
|
|
queryKey: ["issued-orders", id],
|
|
queryFn: () =>
|
|
jsonQuery<IssuedOrderDetail>(`/api/admin/issued-orders/${id}`),
|
|
enabled: !!id,
|
|
// 404s (deleted/missing orders) are not transient. Retrying just spams
|
|
// GETs and keeps the detail page on an infinite spinner.
|
|
retry: false,
|
|
// IssuedOrderDetail toasts its own Czech error + navigates away (and
|
|
// suppresses it during delete) — the global QueryCache toast would double up.
|
|
meta: { suppressGlobalErrorToast: true },
|
|
});
|
|
|
|
export const issuedOrderNextNumberOptions = () =>
|
|
queryOptions({
|
|
queryKey: ["issued-orders", "next-number"],
|
|
queryFn: () =>
|
|
jsonQuery<{ next_number?: string; number?: string }>(
|
|
"/api/admin/issued-orders/next-number",
|
|
).then((d) => d?.next_number || d?.number || ""),
|
|
});
|