v1.8.0: warehouse module (16 commits), docházka mzda model, leave_type=holiday removal, api.ts race fix
Highlights: - Warehouse module: receipts, issues, reservations, inventory, reports, dashboard, master data (categories, suppliers, locations), FIFO service, integration tests - Docházka: mzda PDF counting model (Odpracováno / Vč. svátků / Přesčas / Svátek / So/Ne / Noc) with Czech weekday names and decimal hours - AttendanceAdmin/AttendanceHistory KPI cards unified to mzda formula with fund bar colored by delta, badges for Práce/Dov/Nem/Sv/Nep - Remove leave_type=holiday entirely (auto-computed from Czech public holidays) - Allow multiple work shifts per day (overlap detection only) - Pre-flight refresh in api.ts eliminates spurious 401s on fresh page loads - Prisma: company_settings gets 6 nullable columns for warehouse numbering (PRI/VYD/INV prefixes, default patterns); migration seeds defaults
This commit is contained in:
113
src/admin/lib/queries/mutations.ts
Normal file
113
src/admin/lib/queries/mutations.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
type UseMutationOptions,
|
||||
} from "@tanstack/react-query";
|
||||
import apiFetch from "../../utils/api";
|
||||
|
||||
export interface ApiError extends Error {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export type HttpMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
interface ApiResponseBody<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
async function performMutation<TIn, TOut>(opts: {
|
||||
url: string | ((input: TIn) => string);
|
||||
method: HttpMethod | ((input: TIn) => HttpMethod);
|
||||
body?: TIn;
|
||||
}): Promise<TOut> {
|
||||
const url =
|
||||
typeof opts.url === "function" ? opts.url(opts.body as TIn) : opts.url;
|
||||
const method =
|
||||
typeof opts.method === "function"
|
||||
? opts.method(opts.body as TIn)
|
||||
: opts.method;
|
||||
const response = await apiFetch(url, {
|
||||
method,
|
||||
headers:
|
||||
opts.body !== undefined
|
||||
? { "Content-Type": "application/json" }
|
||||
: undefined,
|
||||
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
const err: ApiError = new Error("Unauthorized");
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
|
||||
let result: ApiResponseBody<TOut>;
|
||||
try {
|
||||
result = (await response.json()) as ApiResponseBody<TOut>;
|
||||
} catch {
|
||||
throw new Error("Invalid JSON response");
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const err: ApiError = new Error(
|
||||
result.error || `Request failed (${response.status})`,
|
||||
);
|
||||
err.status = response.status;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return result.data as TOut;
|
||||
}
|
||||
|
||||
export interface ApiMutationOptions<TIn, TOut> {
|
||||
url: string | ((input: TIn) => string);
|
||||
method: HttpMethod | ((input: TIn) => HttpMethod);
|
||||
/** Query-key prefixes to invalidate on success. Broad per CLAUDE.md. */
|
||||
invalidate?: readonly string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that wraps `useMutation` with `apiFetch` and broad-invalidation.
|
||||
*
|
||||
* Usage:
|
||||
* const createCategory = useApiMutation<CategoryForm, WarehouseCategory>({
|
||||
* url: "/api/admin/warehouse/categories",
|
||||
* method: "POST",
|
||||
* invalidate: ["warehouse"],
|
||||
* });
|
||||
* createCategory.mutate(form, {
|
||||
* onSuccess: () => { ... },
|
||||
* });
|
||||
*
|
||||
* The returned `data` from a successful mutation is `TOut`. Errors are thrown
|
||||
* as `ApiError` (with `.status` attached) so callers can branch on HTTP code.
|
||||
*/
|
||||
export function useApiMutation<TIn, TOut>(
|
||||
opts: ApiMutationOptions<TIn, TOut> &
|
||||
Omit<UseMutationOptions<TOut, ApiError, TIn>, "mutationFn">,
|
||||
) {
|
||||
const { url, method, invalidate, onSuccess, ...rest } = opts;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TOut, ApiError, TIn, unknown>({
|
||||
mutationFn: (input: TIn) =>
|
||||
performMutation<TIn, TOut>({ url, method, body: input }),
|
||||
...rest,
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
if (invalidate) {
|
||||
for (const key of invalidate) {
|
||||
queryClient.invalidateQueries({ queryKey: [key] });
|
||||
}
|
||||
}
|
||||
// Forward to user-provided onSuccess with the 4-arg signature expected by TanStack.
|
||||
(
|
||||
onSuccess as
|
||||
| ((d: TOut, v: TIn, r: unknown, c: unknown) => void)
|
||||
| undefined
|
||||
)?.(data, variables, onMutateResult, context);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user