feat(plan): react query options and page state hook

This commit is contained in:
BOHA
2026-06-05 12:54:50 +02:00
parent 9c10ff9f22
commit ca911bf96c
2 changed files with 237 additions and 0 deletions

View 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;
}