Files
app/src/admin/lib/queries/issued-orders.ts
BOHA 87e644eef8 fix(freshness): converge invalidation sets; global query-error toast; silent-toggle feedback
- 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>
2026-07-04 03:21:50 +02:00

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 || ""),
});