Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/dashboard/OverviewPage.tsx
2026-08-13 18:13:42 +00:00

128 lines
4.4 KiB
TypeScript

import { useState, type ReactElement } from "react";
import { AlertCircle } from "lucide-react";
import { Alert, Button, Skeleton, Stack } from "@mantine/core";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/auth/useAuth";
import { PageContainer } from "@/components/page";
import { ClearanceOverview } from "@/components/overview/layouts/ClearanceOverview";
import { ExecutiveOverview } from "@/components/overview/layouts/ExecutiveOverview";
import { FinanceOverview } from "@/components/overview/layouts/FinanceOverview";
import { MarketingOverview } from "@/components/overview/layouts/MarketingOverview";
import { OccOverview } from "@/components/overview/layouts/OccOverview";
import { OperationsOverview } from "@/components/overview/layouts/OperationsOverview";
import type { RoleOverviewProps } from "@/components/overview/layouts/layout-kit";
import {
OVERVIEW_LAYOUT_LABEL,
resolveOverviewLayout,
type OverviewLayoutKey,
} from "@/components/overview/role-dashboards.config";
import { OverviewHero } from "@/components/overview/summary/OverviewHero";
import { OverviewHeroKpis } from "@/components/overview/summary/OverviewHeroKpis";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { useOverview } from "@/hooks/useOverview";
import type { OverviewRange } from "@/types/overview";
import "@/components/overview/summary/overview-summary.css";
const RANGE_LABEL: Record<OverviewRange, string> = { "7d": "7d", "30d": "30d", "90d": "90d" };
/** Which composition each role sees below the hero. */
const LAYOUTS: Record<OverviewLayoutKey, (props: RoleOverviewProps) => ReactElement> = {
executive: ExecutiveOverview,
operations: OperationsOverview,
occ: OccOverview,
marketing: MarketingOverview,
finance: FinanceOverview,
clearance: ClearanceOverview,
};
function OverviewSkeleton() {
return (
<Stack gap="lg">
<Skeleton height={92} radius="lg" />
<Skeleton height={340} radius="lg" />
<Skeleton height={340} radius="lg" />
<Skeleton height={340} radius="lg" />
</Stack>
);
}
const OverviewPage = () => {
const [range, setRange] = useState<OverviewRange>("30d");
const queryClient = useQueryClient();
const { user } = useAuth();
const { data, isLoading, isError, error, refetch, isFetching } = useOverview(range);
// Hero, range control and headline KPIs are role-neutral; everything below
// them is chosen by role key.
const layoutKey = resolveOverviewLayout(user);
const RoleLayout = LAYOUTS[layoutKey];
const accessDenied =
(error as { response?: { status?: number } } | null)?.response?.status === 403;
const handleRefresh = () => {
void refetch();
void queryClient.invalidateQueries({ queryKey: QUERY_KEYS.OVERVIEW.ROOT });
};
return (
<PageContainer>
{/* Gradient greeting hero; the KPI strip overlaps its bottom edge. */}
<div className="ov-band">
<OverviewHero
range={range}
onRangeChange={setRange}
generatedAt={data?.generatedAt}
onRefresh={handleRefresh}
isRefreshing={isFetching && !isLoading}
label={OVERVIEW_LAYOUT_LABEL[layoutKey]}
/>
{data ? (
<div style={{ marginTop: -52, paddingInline: 20, position: "relative" }}>
<OverviewHeroKpis
kpis={data.kpis}
current={data.current}
previous={data.previous}
rangeLabel={RANGE_LABEL[range]}
/>
</div>
) : null}
</div>
{isError && !accessDenied && (
<Alert
icon={<AlertCircle size={16} />}
color="red"
title="Unable to load dashboard summary"
variant="light"
mt="lg"
>
<Stack gap="sm" align="flex-start">
<span>Check your connection and try again.</span>
<Button size="xs" variant="light" color="red" onClick={() => void refetch()}>
Retry
</Button>
</Stack>
</Alert>
)}
{accessDenied && (
<Alert color="gray" variant="light" title="No dashboard access" mt="lg">
Your role has no access to the overview.
</Alert>
)}
{isLoading && !data ? (
<Stack mt="lg">
<OverviewSkeleton />
</Stack>
) : data ? (
<RoleLayout data={data} range={range} />
) : null}
</PageContainer>
);
};
export default OverviewPage;