feat(plan): react query options and page state hook
This commit is contained in:
174
src/admin/hooks/usePlanWork.ts
Normal file
174
src/admin/hooks/usePlanWork.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { useState, useCallback, useMemo } from "react";
|
||||||
|
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../utils/api";
|
||||||
|
import { planKeys, GridData, ResolvedCell } from "../lib/queries/plan";
|
||||||
|
|
||||||
|
export type ViewMode = "week" | "month";
|
||||||
|
export type ModalMode =
|
||||||
|
| { kind: "closed" }
|
||||||
|
| { kind: "create"; userId: number; date: string }
|
||||||
|
| { kind: "edit-range"; entryId: number; userId: number; date: string }
|
||||||
|
| { kind: "edit-override"; overrideId: number; userId: number; date: string }
|
||||||
|
| { kind: "day-in-range"; userId: number; date: string; entryId: number }
|
||||||
|
| { kind: "view"; userId: number; date: string };
|
||||||
|
|
||||||
|
function isoDate(d: Date): string {
|
||||||
|
return d.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfWeek(d: Date): Date {
|
||||||
|
const day = d.getUTCDay(); // 0 = Sun
|
||||||
|
const diff = day === 0 ? -6 : 1 - day; // Monday-start
|
||||||
|
const r = new Date(d);
|
||||||
|
r.setUTCDate(d.getUTCDate() + diff);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(d: Date, n: number): Date {
|
||||||
|
const r = new Date(d);
|
||||||
|
r.setUTCDate(d.getUTCDate() + n);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsePlanWorkArgs {
|
||||||
|
initialDate?: Date;
|
||||||
|
canEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const [view, setView] = useState<ViewMode>("week");
|
||||||
|
const [anchor, setAnchor] = useState<Date>(initialDate ?? new Date());
|
||||||
|
const [filterActive, setFilterActive] = useState(true);
|
||||||
|
const [modal, setModal] = useState<ModalMode>({ kind: "closed" });
|
||||||
|
|
||||||
|
const range = useMemo(() => {
|
||||||
|
if (view === "week") {
|
||||||
|
const from = startOfWeek(anchor);
|
||||||
|
const to = addDays(from, 6);
|
||||||
|
return { from, to };
|
||||||
|
}
|
||||||
|
const from = new Date(
|
||||||
|
Date.UTC(anchor.getUTCFullYear(), anchor.getUTCMonth(), 1),
|
||||||
|
);
|
||||||
|
const to = new Date(
|
||||||
|
Date.UTC(anchor.getUTCFullYear(), anchor.getUTCMonth() + 1, 0),
|
||||||
|
);
|
||||||
|
return { from, to };
|
||||||
|
}, [view, anchor]);
|
||||||
|
|
||||||
|
const gridQuery = useQuery({
|
||||||
|
queryKey: planKeys.grid(isoDate(range.from), isoDate(range.to), view),
|
||||||
|
queryFn: () =>
|
||||||
|
apiFetch(
|
||||||
|
`/api/admin/plan/grid?date_from=${isoDate(range.from)}&date_to=${isoDate(range.to)}&view=${view}`,
|
||||||
|
).then((r) => r.json().then((b: any) => b.data as GridData)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidate = useCallback(() => {
|
||||||
|
qc.invalidateQueries({ queryKey: planKeys.all });
|
||||||
|
}, [qc]);
|
||||||
|
|
||||||
|
// --- Mutations ---
|
||||||
|
const createEntry = useMutation({
|
||||||
|
mutationFn: (body: any) =>
|
||||||
|
apiFetch("/api/admin/plan/entries", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateEntry = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
body,
|
||||||
|
force,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: any;
|
||||||
|
force?: boolean;
|
||||||
|
}) =>
|
||||||
|
apiFetch(`/api/admin/plan/entries/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteEntry = useMutation({
|
||||||
|
mutationFn: ({ id, force }: { id: number; force?: boolean }) =>
|
||||||
|
apiFetch(`/api/admin/plan/entries/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createOverride = useMutation({
|
||||||
|
mutationFn: (body: any) =>
|
||||||
|
apiFetch("/api/admin/plan/overrides", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateOverride = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
body,
|
||||||
|
force,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: any;
|
||||||
|
force?: boolean;
|
||||||
|
}) =>
|
||||||
|
apiFetch(`/api/admin/plan/overrides/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteOverride = useMutation({
|
||||||
|
mutationFn: ({ id, force }: { id: number; force?: boolean }) =>
|
||||||
|
apiFetch(`/api/admin/plan/overrides/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
view,
|
||||||
|
setView,
|
||||||
|
anchor,
|
||||||
|
setAnchor,
|
||||||
|
filterActive,
|
||||||
|
setFilterActive,
|
||||||
|
range,
|
||||||
|
grid: gridQuery.data,
|
||||||
|
gridLoading: gridQuery.isLoading,
|
||||||
|
modal,
|
||||||
|
setModal,
|
||||||
|
canEdit,
|
||||||
|
createEntry,
|
||||||
|
updateEntry,
|
||||||
|
deleteEntry,
|
||||||
|
createOverride,
|
||||||
|
updateOverride,
|
||||||
|
deleteOverride,
|
||||||
|
invalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCell(
|
||||||
|
grid: GridData | undefined,
|
||||||
|
userId: number,
|
||||||
|
date: string,
|
||||||
|
): ResolvedCell | null {
|
||||||
|
return grid?.cells?.[userId]?.[date] ?? null;
|
||||||
|
}
|
||||||
63
src/admin/lib/queries/plan.ts
Normal file
63
src/admin/lib/queries/plan.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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;
|
||||||
|
category: string;
|
||||||
|
note: string;
|
||||||
|
rangeFrom: string | null;
|
||||||
|
rangeTo: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user