Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/dashboard/OverviewPage.tsx

194 lines
5.0 KiB
TypeScript

import { useState } from "react";
import {
AlertCircle,
Banknote,
FileText,
Train,
UserCheck,
Users,
} from "lucide-react";
import {
Alert,
Badge,
Button,
Container,
Paper,
Skeleton,
Stack,
Tabs,
} from "@mantine/core";
import { useQueryClient } from "@tanstack/react-query";
import { OverviewPageHeader } from "@/components/overview/OverviewPageHeader";
import { OverviewQuickLinks } from "@/components/overview/OverviewQuickLinks";
import { OverviewTabContent } from "@/components/overview/OverviewTabContent";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { useOverview } from "@/hooks/useOverview";
import type { OverviewRange, OverviewTabKey } from "@/types/overview";
const TAB_ITEMS: Array<{
value: OverviewTabKey;
label: string;
icon: typeof FileText;
kpiKey: "bookings" | "billing" | "operations" | "customers" | "staff";
metricKey: string;
}> = [
{
value: "bookings",
label: "Bookings",
icon: FileText,
kpiKey: "bookings",
metricKey: "totalActive",
},
{
value: "billing",
label: "Billing",
icon: Banknote,
kpiKey: "billing",
metricKey: "successfulPaymentsMtd",
},
{
value: "operations",
label: "Operations",
icon: Train,
kpiKey: "operations",
metricKey: "trainsActive",
},
{
value: "customers",
label: "Customers",
icon: Users,
kpiKey: "customers",
metricKey: "totalCustomers",
},
{
value: "staff",
label: "Staff",
icon: UserCheck,
kpiKey: "staff",
metricKey: "activeEmployees",
},
];
function HeaderSkeleton() {
return (
<Stack gap="md">
<Skeleton height={48} radius="md" />
<Skeleton height={52} radius="lg" />
</Stack>
);
}
const OverviewPage = () => {
const [range, setRange] = useState<OverviewRange>("30d");
const [activeTab, setActiveTab] = useState<OverviewTabKey>("bookings");
const queryClient = useQueryClient();
const { data: summary, isLoading, isError, refetch, isFetching } = useOverview(range);
const handleRefresh = () => {
void refetch();
void queryClient.invalidateQueries({ queryKey: QUERY_KEYS.OVERVIEW.ROOT });
};
const getTabBadge = (tab: (typeof TAB_ITEMS)[number]) => {
if (!summary?.kpis) return 0;
const group = summary.kpis[tab.kpiKey] as Record<string, number>;
return group[tab.metricKey] ?? 0;
};
return (
<Container fluid px="md" py="md">
<Stack gap="lg">
{isLoading && !summary ? (
<HeaderSkeleton />
) : (
<OverviewPageHeader
range={range}
onRangeChange={setRange}
generatedAt={summary?.generatedAt}
onRefresh={handleRefresh}
isRefreshing={isFetching && !isLoading}
/>
)}
{isError && (
<Alert
icon={<AlertCircle size={16} />}
color="red"
title="Unable to load dashboard summary"
variant="light"
>
<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>
)}
<Paper
radius="lg"
withBorder
p="md"
style={{
background: "white",
border: "1px solid var(--mantine-color-gray-2)",
}}
>
<Tabs
value={activeTab}
onChange={(value) => setActiveTab((value as OverviewTabKey) ?? "bookings")}
variant="pills"
color="green"
keepMounted={false}
>
<Tabs.List
style={{
flexWrap: "wrap",
gap: 8,
background: "var(--freight-brand-muted, #f0fdf4)",
padding: 8,
borderRadius: 12,
}}
>
{TAB_ITEMS.map((tab) => {
const Icon = tab.icon;
return (
<Tabs.Tab
key={tab.value}
value={tab.value}
leftSection={<Icon size={16} />}
rightSection={
summary ? (
<Badge size="sm" variant="light" color="green">
{getTabBadge(tab)}
</Badge>
) : undefined
}
style={{ fontWeight: 600 }}
>
{tab.label}
</Tabs.Tab>
);
})}
</Tabs.List>
{TAB_ITEMS.map((tab) => (
<Tabs.Panel key={tab.value} value={tab.value} pt="lg">
<OverviewTabContent tab={tab.value} range={range} />
</Tabs.Panel>
))}
</Tabs>
</Paper>
<Paper p="lg" radius="lg" withBorder>
<OverviewQuickLinks />
</Paper>
</Stack>
</Container>
);
};
export default OverviewPage;