116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
import { queryOptions } from "@tanstack/react-query";
|
|
import apiFetch from "../../utils/api";
|
|
import { jsonQuery } from "../apiAdapter";
|
|
|
|
export interface PlanUser {
|
|
id: number;
|
|
full_name: string;
|
|
username: string;
|
|
role_name: string | null;
|
|
is_active: boolean;
|
|
has_attendance_record: boolean;
|
|
}
|
|
|
|
export interface ResolvedCell {
|
|
source: "entry" | "override";
|
|
entryId: number | null;
|
|
overrideId: number | null;
|
|
user_id: number;
|
|
shift_date: string;
|
|
project_id: number | null;
|
|
/** Project label resolved server-side (see resolveGrid). Lets the UI show
|
|
* the project without depending on the capped/permission-gated projects
|
|
* list. Null when the cell has no project. */
|
|
project_number: string | null;
|
|
project_name: string | null;
|
|
category: string;
|
|
note: string;
|
|
rangeFrom: string | null;
|
|
rangeTo: string | null;
|
|
}
|
|
|
|
export interface PlanCategory {
|
|
id: number;
|
|
key: string;
|
|
label: string;
|
|
color: string;
|
|
sort_order: number;
|
|
is_active: boolean;
|
|
}
|
|
|
|
export const planCategoriesOptions = () =>
|
|
queryOptions({
|
|
queryKey: ["plan", "categories"],
|
|
queryFn: () => jsonQuery<PlanCategory[]>("/api/admin/plan/categories"),
|
|
});
|
|
|
|
/** Build a key → category lookup from the categories list. */
|
|
export function categoryMap(
|
|
categories: PlanCategory[] | undefined,
|
|
): Record<string, PlanCategory> {
|
|
const map: Record<string, PlanCategory> = {};
|
|
for (const c of categories ?? []) map[c.key] = c;
|
|
return map;
|
|
}
|
|
|
|
/** Czech label for a plan category key, resolved from the loaded categories
|
|
* map, falling back to the raw key. The `map` arg is optional so callers not
|
|
* yet threading categories still compile (they show the raw key until wired). */
|
|
export function planCategoryLabel(
|
|
key: string,
|
|
map?: Record<string, PlanCategory>,
|
|
): string {
|
|
return map?.[key]?.label ?? key;
|
|
}
|
|
|
|
/** Build the display label for a cell's project, preferring the server-
|
|
* embedded fields. Returns null when the cell has no project. */
|
|
export function cellProjectLabel(cell: {
|
|
project_number: string | null;
|
|
project_name: string | null;
|
|
}): string | null {
|
|
if (!cell.project_number && !cell.project_name) return null;
|
|
if (cell.project_number && cell.project_name) {
|
|
return `${cell.project_number} — ${cell.project_name}`;
|
|
}
|
|
return cell.project_number ?? cell.project_name;
|
|
}
|
|
|
|
export interface GridData {
|
|
view: "week" | "month";
|
|
date_from: string;
|
|
date_to: string;
|
|
users: PlanUser[];
|
|
cells: Record<number, Record<string, ResolvedCell | null>>;
|
|
}
|
|
|
|
export const planKeys = {
|
|
all: ["plan"] as const,
|
|
grid: (dateFrom: string, dateTo: string, view: "week" | "month") =>
|
|
["plan", "grid", { dateFrom, dateTo, view }] as const,
|
|
entries: (userId: number | null, dateFrom: string, dateTo: string) =>
|
|
["plan", "entries", { userId, dateFrom, dateTo }] as const,
|
|
overrides: (userId: number | null, dateFrom: string, dateTo: string) =>
|
|
["plan", "overrides", { userId, dateFrom, dateTo }] as const,
|
|
users: () => ["plan", "users"] as const,
|
|
};
|
|
|
|
export const gridQuery = (
|
|
dateFrom: string,
|
|
dateTo: string,
|
|
view: "week" | "month",
|
|
) =>
|
|
queryOptions({
|
|
queryKey: planKeys.grid(dateFrom, dateTo, view),
|
|
queryFn: () =>
|
|
jsonQuery<GridData>(
|
|
`/api/admin/plan/grid?date_from=${dateFrom}&date_to=${dateTo}&view=${view}`,
|
|
),
|
|
});
|
|
|
|
export const usersQuery = () =>
|
|
queryOptions({
|
|
queryKey: planKeys.users(),
|
|
queryFn: () => jsonQuery<PlanUser[]>("/api/admin/plan/users"),
|
|
});
|