feat(platform): harden access scoping and delivery baseline
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
type DemandRequirement,
|
||||
} from "@capakraken/shared";
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTimelineSSE } from "~/hooks/useTimelineSSE.js";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
@@ -217,7 +218,13 @@ export function TimelineProvider({
|
||||
isDragging,
|
||||
children,
|
||||
}: TimelineProviderProps) {
|
||||
const { data: session, status: sessionStatus } = useSession();
|
||||
const searchParams = useSearchParams();
|
||||
const role = sessionStatus === "authenticated"
|
||||
? ((session.user as { role?: string } | undefined)?.role ?? "USER")
|
||||
: null;
|
||||
const isSelfServiceTimeline = role === "USER" || role === "VIEWER";
|
||||
const isRoleLoading = sessionStatus !== "authenticated";
|
||||
|
||||
const today = useMemo(() => {
|
||||
const d = new Date();
|
||||
@@ -283,19 +290,25 @@ export function TimelineProvider({
|
||||
|
||||
// ─── Data queries ──────────────────────────────────────────────────────────
|
||||
const mountedRef = useRef(false);
|
||||
const timelineQueryInput = {
|
||||
startDate: viewStart,
|
||||
endDate: viewEnd,
|
||||
...(filters.clientIds.length > 0 ? { clientIds: filters.clientIds } : {}),
|
||||
...(filters.projectIds.length > 0 ? { projectIds: filters.projectIds } : {}),
|
||||
...(filters.chapters.length > 0 ? { chapters: filters.chapters } : {}),
|
||||
...(filters.eids.length > 0 ? { eids: filters.eids } : {}),
|
||||
...(filters.countryCodes.length > 0 ? { countryCodes: filters.countryCodes } : {}),
|
||||
};
|
||||
|
||||
const entriesViewQuery = trpc.timeline.getEntriesView.useQuery(
|
||||
{
|
||||
startDate: viewStart,
|
||||
endDate: viewEnd,
|
||||
...(filters.clientIds.length > 0 ? { clientIds: filters.clientIds } : {}),
|
||||
...(filters.projectIds.length > 0 ? { projectIds: filters.projectIds } : {}),
|
||||
...(filters.chapters.length > 0 ? { chapters: filters.chapters } : {}),
|
||||
...(filters.eids.length > 0 ? { eids: filters.eids } : {}),
|
||||
...(filters.countryCodes.length > 0 ? { countryCodes: filters.countryCodes } : {}),
|
||||
},
|
||||
const staffEntriesViewQuery = trpc.timeline.getEntriesView.useQuery(
|
||||
timelineQueryInput,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ placeholderData: (prev: any) => prev, refetchOnWindowFocus: false, staleTime: 90_000 },
|
||||
{
|
||||
enabled: !isRoleLoading && !isSelfServiceTimeline,
|
||||
placeholderData: (prev: any) => prev,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 90_000,
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as {
|
||||
data: TimelineEntriesView | undefined;
|
||||
@@ -303,6 +316,23 @@ export function TimelineProvider({
|
||||
refetch: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
const selfEntriesViewQuery = trpc.timeline.getMyEntriesView.useQuery(
|
||||
timelineQueryInput,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{
|
||||
enabled: !isRoleLoading && isSelfServiceTimeline,
|
||||
placeholderData: (prev: any) => prev,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 90_000,
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as {
|
||||
data: TimelineEntriesView | undefined;
|
||||
isLoading: boolean;
|
||||
refetch: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
const entriesViewQuery = isSelfServiceTimeline ? selfEntriesViewQuery : staffEntriesViewQuery;
|
||||
const { data: entriesView, isLoading, refetch: refetchEntriesView } = entriesViewQuery;
|
||||
|
||||
const assignments = entriesView?.assignments ?? [];
|
||||
@@ -316,24 +346,33 @@ export function TimelineProvider({
|
||||
{ placeholderData: (prev) => prev, refetchOnWindowFocus: false, staleTime: 90_000 },
|
||||
);
|
||||
|
||||
const staffHolidayOverlayQuery = trpc.timeline.getHolidayOverlays.useQuery(
|
||||
timelineQueryInput,
|
||||
{
|
||||
enabled: !isRoleLoading && !isSelfServiceTimeline,
|
||||
placeholderData: (prev) => prev,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 90_000,
|
||||
},
|
||||
);
|
||||
const selfHolidayOverlayQuery = trpc.timeline.getMyHolidayOverlays.useQuery(
|
||||
timelineQueryInput,
|
||||
{
|
||||
enabled: !isRoleLoading && isSelfServiceTimeline,
|
||||
placeholderData: (prev) => prev,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 90_000,
|
||||
},
|
||||
);
|
||||
const activeHolidayOverlayQuery = isSelfServiceTimeline ? selfHolidayOverlayQuery : staffHolidayOverlayQuery;
|
||||
const {
|
||||
data: holidayOverlayEntries = [],
|
||||
refetch: refetchHolidayOverlays,
|
||||
} = trpc.timeline.getHolidayOverlays.useQuery(
|
||||
{
|
||||
startDate: viewStart,
|
||||
endDate: viewEnd,
|
||||
...(filters.clientIds.length > 0 ? { clientIds: filters.clientIds } : {}),
|
||||
...(filters.projectIds.length > 0 ? { projectIds: filters.projectIds } : {}),
|
||||
...(filters.chapters.length > 0 ? { chapters: filters.chapters } : {}),
|
||||
...(filters.eids.length > 0 ? { eids: filters.eids } : {}),
|
||||
...(filters.countryCodes.length > 0 ? { countryCodes: filters.countryCodes } : {}),
|
||||
},
|
||||
{ placeholderData: (prev) => prev, refetchOnWindowFocus: false, staleTime: 90_000 },
|
||||
);
|
||||
} = activeHolidayOverlayQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (mountedRef.current) return;
|
||||
if (isRoleLoading) return;
|
||||
mountedRef.current = true;
|
||||
|
||||
// Harden client-side route transitions: the timeline must actively refresh
|
||||
@@ -341,7 +380,7 @@ export function TimelineProvider({
|
||||
void refetchEntriesView();
|
||||
void refetchVacations();
|
||||
void refetchHolidayOverlays();
|
||||
}, [refetchEntriesView, refetchHolidayOverlays, refetchVacations]);
|
||||
}, [isRoleLoading, refetchEntriesView, refetchHolidayOverlays, refetchVacations]);
|
||||
|
||||
const vacationsByResource = useMemo(() => {
|
||||
const map = new Map<string, VacationEntry[]>();
|
||||
@@ -378,9 +417,9 @@ export function TimelineProvider({
|
||||
}, [holidayOverlayEntries, vacationEntries]);
|
||||
|
||||
// When EID filter is active, explicitly fetch those resources.
|
||||
const { data: eidFilterData } = trpc.resource.list.useQuery(
|
||||
const { data: eidFilterData } = trpc.resource.directory.useQuery(
|
||||
{ eids: filters.eids, limit: 100 },
|
||||
{ enabled: filters.eids.length > 0, staleTime: 30_000 },
|
||||
{ enabled: !isSelfServiceTimeline && filters.eids.length > 0, staleTime: 30_000 },
|
||||
);
|
||||
|
||||
// ─── Filtered entries ──────────────────────────────────────────────────────
|
||||
@@ -633,7 +672,7 @@ export function TimelineProvider({
|
||||
]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// ─── Derived counts ───────────────────────────────────────────────────────
|
||||
const isInitialLoading = isLoading && !entriesView;
|
||||
const isInitialLoading = (isRoleLoading || isLoading) && !entriesView;
|
||||
const totalAllocCount = entriesView?.allocations.length ?? 0;
|
||||
const activeFilterCount =
|
||||
filters.clientIds.length +
|
||||
|
||||
Reference in New Issue
Block a user