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:
@@ -56,6 +56,12 @@ export const apiFetch = async (
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<Response> => {
|
||||
// Honour abort signals up-front — TanStack Query may cancel a stale
|
||||
// request before we've done any work.
|
||||
if (options.signal?.aborted) {
|
||||
return new Response(null, { status: 499 });
|
||||
}
|
||||
|
||||
let token: string | null = null;
|
||||
try {
|
||||
token = state.getTokenFn ? state.getTokenFn() : null;
|
||||
@@ -63,6 +69,36 @@ export const apiFetch = async (
|
||||
// token retrieval failed
|
||||
}
|
||||
|
||||
// No usable token yet (e.g. fresh page load where the silent refresh
|
||||
// kicked off in useEffect hasn't populated the in-memory access token).
|
||||
// Refresh up front, BEFORE firing the request, so the caller never sees
|
||||
// a 401 in the network log on the first attempt. Concurrent callers join
|
||||
// the same in-flight refresh via state.refreshPromise.
|
||||
if (!token && state.refreshFn) {
|
||||
try {
|
||||
if (!state.refreshPromise) {
|
||||
state.refreshPromise = state.refreshFn().finally(() => {
|
||||
state.refreshPromise = null;
|
||||
});
|
||||
}
|
||||
const refreshed = await state.refreshPromise;
|
||||
if (options.signal?.aborted) {
|
||||
return new Response(null, { status: 499 });
|
||||
}
|
||||
if (refreshed) {
|
||||
try {
|
||||
token = state.getTokenFn ? state.getTokenFn() : null;
|
||||
} catch {
|
||||
token = null;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Refresh itself failed — fall through and let the request fire
|
||||
// without a token. The 401 handler below will set a session-expired
|
||||
// flag so the UI can react.
|
||||
}
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
@@ -86,6 +122,12 @@ export const apiFetch = async (
|
||||
});
|
||||
|
||||
if (response.status === 401 && state.refreshFn) {
|
||||
// If the caller already unmounted (TanStack Query aborted the request),
|
||||
// do not refresh + retry. Doing so would fire a global "session expired"
|
||||
// toast on a page the user has already left and leak a fetch+refresh pair.
|
||||
if (options.signal?.aborted) {
|
||||
return response;
|
||||
}
|
||||
try {
|
||||
if (!state.refreshPromise) {
|
||||
state.refreshPromise = state.refreshFn().finally(() => {
|
||||
@@ -93,12 +135,17 @@ export const apiFetch = async (
|
||||
});
|
||||
}
|
||||
const refreshed = await state.refreshPromise;
|
||||
// Re-check after the await: the caller may have unmounted while the
|
||||
// refresh was in flight (refresh + abort race).
|
||||
if (options.signal?.aborted) {
|
||||
return response;
|
||||
}
|
||||
if (refreshed) {
|
||||
token = state.getTokenFn ? state.getTokenFn() : null;
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
const { signal, ...retryOptions } = options;
|
||||
const { signal: _signal, ...retryOptions } = options;
|
||||
response = await fetch(url, {
|
||||
...retryOptions,
|
||||
headers,
|
||||
|
||||
Reference in New Issue
Block a user