mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 03:38:17 +00:00
feat: dashbaord overview
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user