feat: enhance admin page functionality with new components and hooks

- Added `AdminDutyList` and `ReassignSheet` components for improved duty management in the admin panel.
- Introduced `useAdminPage` hook to encapsulate admin-related logic, including user and duty loading, and reassign functionality.
- Updated `frontend.mdc` documentation to reflect new admin components and their usage.
- Improved error handling for API responses, particularly for access denied scenarios.
- Refactored admin page to utilize new components, streamlining the UI and enhancing maintainability.
This commit is contained in:
2026-03-06 10:17:28 +03:00
parent c390a4dd6e
commit a3152a4545
7 changed files with 565 additions and 420 deletions

View File

@@ -128,6 +128,27 @@ function buildFetchOptions(
return { headers, signal: controller.signal, cleanup };
}
/**
* Parse 403 response body for user-facing detail. Returns default i18n message if body is invalid.
*/
async function handle403Response(
res: Response,
acceptLang: ApiLang,
defaultI18nKey: string
): Promise<string> {
let detail = translate(acceptLang, defaultI18nKey);
try {
const body = await res.json();
if (body && (body as { detail?: string }).detail !== undefined) {
const d = (body as { detail: string | { msg?: string } }).detail;
detail = typeof d === "string" ? d : (d.msg ?? JSON.stringify(d));
}
} catch {
/* ignore */
}
return detail;
}
/**
* Fetch duties for date range. Throws AccessDeniedError on 403.
* Rethrows AbortError when the request is cancelled (e.g. stale load).
@@ -148,17 +169,7 @@ export async function fetchDuties(
const res = await fetch(url, { headers: opts.headers, signal: opts.signal });
if (res.status === 403) {
logger.warn("Access denied", from, to);
let detail = translate(acceptLang, "access_denied");
try {
const body = await res.json();
if (body && (body as { detail?: string }).detail !== undefined) {
const d = (body as { detail: string | { msg?: string } }).detail;
detail =
typeof d === "string" ? d : (d.msg ?? JSON.stringify(d));
}
} catch {
/* ignore */
}
const detail = await handle403Response(res, acceptLang, "access_denied");
throw new AccessDeniedError(API_ACCESS_DENIED, detail);
}
if (!res.ok) {
@@ -197,17 +208,7 @@ export async function fetchCalendarEvents(
const res = await fetch(url, { headers: opts.headers, signal: opts.signal });
if (res.status === 403) {
logger.warn("Access denied", from, to, "calendar-events");
let detail = translate(acceptLang, "access_denied");
try {
const body = await res.json();
if (body && (body as { detail?: string }).detail !== undefined) {
const d = (body as { detail: string | { msg?: string } }).detail;
detail =
typeof d === "string" ? d : (d.msg ?? JSON.stringify(d));
}
} catch {
/* ignore */
}
const detail = await handle403Response(res, acceptLang, "access_denied");
throw new AccessDeniedError(API_ACCESS_DENIED, detail);
}
if (!res.ok) return [];
@@ -273,17 +274,7 @@ export async function fetchAdminUsers(
logger.debug("API request", "/api/admin/users");
const res = await fetch(url, { headers: opts.headers, signal: opts.signal });
if (res.status === 403) {
let detail = translate(acceptLang, "admin.access_denied");
try {
const body = await res.json();
if (body && (body as { detail?: string }).detail !== undefined) {
const d = (body as { detail: string | { msg?: string } }).detail;
detail =
typeof d === "string" ? d : (d.msg ?? JSON.stringify(d));
}
} catch {
/* ignore */
}
const detail = await handle403Response(res, acceptLang, "admin.access_denied");
throw new AccessDeniedError(API_ACCESS_DENIED, detail);
}
if (!res.ok) {
@@ -338,17 +329,7 @@ export async function patchAdminDuty(
signal: opts.signal,
});
if (res.status === 403) {
let detail = translate(acceptLang, "admin.access_denied");
try {
const body = await res.json();
if (body && (body as { detail?: string }).detail !== undefined) {
const d = (body as { detail: string | { msg?: string } }).detail;
detail =
typeof d === "string" ? d : (d.msg ?? JSON.stringify(d));
}
} catch {
/* ignore */
}
const detail = await handle403Response(res, acceptLang, "admin.access_denied");
throw new AccessDeniedError(API_ACCESS_DENIED, detail);
}
const data = await res.json().catch(() => ({}));