feat: dashbaord overview

This commit is contained in:
Nathnael
2026-08-13 18:13:42 +00:00
parent 89cdc0ad06
commit 147887b7c5
23 changed files with 1795 additions and 94 deletions

View File

@@ -1,44 +1,40 @@
import { useState } from "react";
import { useState, type ReactElement } from "react";
import { AlertCircle } from "lucide-react";
import { Alert, Button, Grid, Skeleton, Stack, Text } from "@mantine/core";
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 { OverviewActivityHeatmap } from "@/components/overview/summary/OverviewActivityHeatmap";
import { OverviewAttentionCard } from "@/components/overview/summary/OverviewAttentionCard";
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 { OverviewNetworkCard } from "@/components/overview/summary/OverviewNetworkCard";
import { OverviewPipelineFunnel } from "@/components/overview/summary/OverviewPipelineFunnel";
import { OverviewRevenueMix } from "@/components/overview/summary/OverviewRevenueMix";
import { OverviewRevenueVolumeChart } from "@/components/overview/summary/OverviewRevenueVolumeChart";
import { OverviewSankeyFlow } from "@/components/overview/summary/OverviewSankeyFlow";
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" };
const RANGE_DAYS: Record<OverviewRange, number> = { "7d": 7, "30d": 30, "90d": 90 };
/** Uppercase section eyebrow — matches the WarehouseDashboardPage convention. */
function SectionTitle({ children }: { children: string }) {
return (
<Text fw={700} fz="sm" tt="uppercase" c="edr-muted" style={{ letterSpacing: 0.4 }}>
{children}
</Text>
);
}
/** One page band: eyebrow + content, with a staggered entrance by index. */
function Band({ index, title, children }: { index: number; title: string; children: React.ReactNode }) {
return (
<Stack gap="sm" className="ov-band" style={{ animationDelay: `${index * 70}ms` }}>
<SectionTitle>{title}</SectionTitle>
{children}
</Stack>
);
}
/** 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 (
@@ -54,8 +50,14 @@ function OverviewSkeleton() {
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;
@@ -74,6 +76,7 @@ const OverviewPage = () => {
generatedAt={data?.generatedAt}
onRefresh={handleRefresh}
isRefreshing={isFetching && !isLoading}
label={OVERVIEW_LAYOUT_LABEL[layoutKey]}
/>
{data ? (
<div style={{ marginTop: -52, paddingInline: 20, position: "relative" }}>
@@ -115,60 +118,7 @@ const OverviewPage = () => {
<OverviewSkeleton />
</Stack>
) : data ? (
<Stack gap="xl" mt="xl">
{/* Band 1 — revenue & volume: growing, making money, pacing vs last period. */}
<Band index={1} title="Revenue & volume">
<Grid gap="md">
<Grid.Col span={{ base: 12, lg: 8 }}>
<OverviewRevenueVolumeChart
bookingTrend={data.bookingTrend}
paymentTrend={data.paymentTrend}
previousPaymentTrend={data.previousPaymentTrend}
rangeDays={RANGE_DAYS[range]}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 4 }}>
<OverviewRevenueMix
byDirection={data.revenueByDirection}
byFreightType={data.revenueByFreightType}
/>
</Grid.Col>
</Grid>
</Band>
{/* Band 2 — where the money runs, and what's waiting on someone. */}
<Band index={2} title="Money flow & attention">
<Grid gap="md">
<Grid.Col span={{ base: 12, lg: 7 }}>
<OverviewSankeyFlow flows={data.revenueFlows} />
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 5 }}>
<OverviewAttentionCard
bookings={data.kpis.bookings}
contracts={data.kpis.contracts}
billing={data.kpis.billing}
/>
</Grid.Col>
</Grid>
</Band>
{/* Band 3 — the network now, and when demand arrives. */}
<Band index={3} title="Network & rhythm">
<Grid gap="md">
<Grid.Col span={{ base: 12, lg: 5 }}>
<OverviewNetworkCard kpis={data.kpis.operations} />
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 7 }}>
<OverviewActivityHeatmap cells={data.bookingHeatmap} />
</Grid.Col>
</Grid>
</Band>
{/* Band 4 — the booking pipeline, full width so every stage bar has room. */}
<Band index={4} title="Pipeline">
<OverviewPipelineFunnel data={data.bookingsByPipeline} />
</Band>
</Stack>
<RoleLayout data={data} range={range} />
) : null}
</PageContainer>
);