add contracts overview tab with KPIs, recent contracts, and charts

- Introduced OverviewContractKpisDto and OverviewRecentContractDto for contract metrics.
- Implemented OverviewContractsTabDto to structure the contracts tab response.
- Added contract-related constants for status groupings and pipeline stages.
- Created OverviewContractsTabPanel and OverviewRecentContractsTable components for UI representation.
- Updated OverviewService and OverviewRepository to fetch contract data.
- Integrated contracts tab into OverviewController and OverviewTabContent.
- Added hooks for fetching contracts data in useOverview.
- Updated types in the overview module to include contracts.
- Enhanced the contract detail page with a "View contract" button for generated PDFs.
This commit is contained in:
Marshal
2026-06-28 17:54:03 +00:00
parent 11c7f1bb74
commit a34e846d90
18 changed files with 688 additions and 11 deletions

View File

@@ -0,0 +1,81 @@
import { useNavigate } from "react-router-dom";
import { Badge, Paper, Stack, Table, Text } from "@mantine/core";
import { ContractStatusBadge } from "@/components/contracts/ContractStatusBadge";
import type { IOverviewRecentContract } from "@/types/overview";
function kindLabel(kind: string) {
return kind === "GENERAL" ? "General" : "One-time";
}
export function OverviewRecentContractsTable({
contracts,
}: {
contracts: IOverviewRecentContract[];
}) {
const navigate = useNavigate();
return (
<Paper p="lg" radius="lg" withBorder>
<Stack gap="md">
<Text fw={600}>Recent contracts</Text>
{contracts.length === 0 ? (
<Text size="sm" c="dimmed" ta="center" py="lg">
No recent contracts
</Text>
) : (
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>Reference</Table.Th>
<Table.Th>Customer</Table.Th>
<Table.Th>Kind</Table.Th>
<Table.Th>Cargo</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Valid until</Table.Th>
<Table.Th>Created</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{contracts.map((contract) => (
<Table.Tr
key={contract.id}
style={{ cursor: "pointer" }}
onClick={() =>
navigate(`/dashboard/contract-requests/${contract.id}`)
}
>
<Table.Td>
<Text fw={600} size="sm">
{contract.reference}
</Text>
</Table.Td>
<Table.Td>{contract.customerLabel}</Table.Td>
<Table.Td>
<Badge variant="light" color="gray" radius="sm">
{kindLabel(contract.contractKind)}
</Badge>
</Table.Td>
<Table.Td>
{contract.freightType === "CONTAINER" ? "Container" : "Bulk"}
</Table.Td>
<Table.Td>
<ContractStatusBadge status={contract.status} />
</Table.Td>
<Table.Td>
{contract.validUntil
? new Date(contract.validUntil).toLocaleDateString()
: "—"}
</Table.Td>
<Table.Td>
{new Date(contract.createdAt).toLocaleDateString()}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
</Stack>
</Paper>
);
}

View File

@@ -4,6 +4,7 @@ import { Alert, Button, Center, Loader, Paper, Skeleton, Stack } from "@mantine/
import {
useOverviewBillingTab,
useOverviewBookingsTab,
useOverviewContractsTab,
useOverviewCustomersTab,
useOverviewOperationsTab,
useOverviewStaffTab,
@@ -11,6 +12,7 @@ import {
import type { OverviewRange, OverviewTabKey } from "@/types/overview";
import { OverviewBillingTabPanel } from "./tabs/OverviewBillingTabPanel";
import { OverviewBookingsTabPanel } from "./tabs/OverviewBookingsTabPanel";
import { OverviewContractsTabPanel } from "./tabs/OverviewContractsTabPanel";
import { OverviewCustomersTabPanel } from "./tabs/OverviewCustomersTabPanel";
import { OverviewOperationsTabPanel } from "./tabs/OverviewOperationsTabPanel";
import { OverviewStaffTabPanel } from "./tabs/OverviewStaffTabPanel";
@@ -32,6 +34,7 @@ interface OverviewTabContentProps {
export function OverviewTabContent({ tab, range }: OverviewTabContentProps) {
const bookings = useOverviewBookingsTab(range, tab === "bookings");
const contracts = useOverviewContractsTab(range, tab === "contracts");
const billing = useOverviewBillingTab(range, tab === "billing");
const operations = useOverviewOperationsTab(tab === "operations");
const customers = useOverviewCustomersTab(range, tab === "customers");
@@ -40,13 +43,15 @@ export function OverviewTabContent({ tab, range }: OverviewTabContentProps) {
const query =
tab === "bookings"
? bookings
: tab === "billing"
? billing
: tab === "operations"
? operations
: tab === "customers"
? customers
: staff;
: tab === "contracts"
? contracts
: tab === "billing"
? billing
: tab === "operations"
? operations
: tab === "customers"
? customers
: staff;
const { isLoading, isError, refetch, isFetching } = query;
@@ -85,6 +90,9 @@ export function OverviewTabContent({ tab, range }: OverviewTabContentProps) {
{tab === "bookings" && bookings.data && (
<OverviewBookingsTabPanel data={bookings.data} />
)}
{tab === "contracts" && contracts.data && (
<OverviewContractsTabPanel data={contracts.data} />
)}
{tab === "billing" && billing.data && (
<OverviewBillingTabPanel data={billing.data} />
)}

View File

@@ -0,0 +1,129 @@
import {
AlertCircle,
FileSignature,
ShieldCheck,
UserCheck,
} from "lucide-react";
import { Grid, Stack } from "@mantine/core";
import { CONTRACT_STATUS_META } from "@/features/contracts/contract-status.config";
import type { IOverviewContractsTab } from "@/types/overview";
import { OverviewBookingTrendChart } from "../OverviewBookingTrendChart";
import { OverviewDonutChart } from "../OverviewDonutChart";
import { OverviewHorizontalBarChart } from "../OverviewHorizontalBarChart";
import { OverviewKpiStrip } from "../OverviewKpiStrip";
import { OverviewRecentContractsTable } from "../OverviewRecentContractsTable";
/** Human labels for the contract pipeline stages (see OVERVIEW_CONTRACT_PIPELINE on the API). */
const PIPELINE_STAGE_LABELS: Record<string, string> = {
draft: "Draft",
intake: "Intake",
in_approval: "In approval",
signing: "Signing",
clearance: "Clearance",
active: "Active",
closed: "Closed",
cancelled: "Cancelled",
};
function kindLabel(kind: string) {
return kind === "GENERAL" ? "General" : kind === "ONE_TIME" ? "One-time" : kind;
}
interface OverviewContractsTabPanelProps {
data: IOverviewContractsTab;
}
export function OverviewContractsTabPanel({
data,
}: OverviewContractsTabPanelProps) {
return (
<Stack gap="lg">
<OverviewKpiStrip
items={[
{
label: "Active contracts",
value: data.kpis.totalActive,
icon: FileSignature,
accent: "emerald",
hint: "Currently in workflow",
},
{
label: "Needs action",
value: data.kpis.needsAction,
icon: AlertCircle,
accent: "amber",
hint: "Awaiting your review",
},
{
label: "In approval",
value: data.kpis.inApproval,
icon: UserCheck,
accent: "sky",
hint: "Pending sign-off",
},
{
label: "In clearance",
value: data.kpis.inClearance,
icon: ShieldCheck,
accent: "rose",
hint: "Customs / documents",
},
{
label: "Created today",
value: data.kpis.createdToday,
icon: FileSignature,
hint: "New since midnight",
},
]}
/>
<Grid gap="md">
<Grid.Col span={{ base: 12, lg: 7 }}>
<OverviewBookingTrendChart data={data.contractTrend} />
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 5 }}>
<OverviewHorizontalBarChart
title="Pipeline by stage"
data={data.contractsByPipeline.map((item) => ({
label: PIPELINE_STAGE_LABELS[item.stage] ?? item.stage,
value: item.count,
}))}
/>
</Grid.Col>
</Grid>
<Grid gap="md">
<Grid.Col span={{ base: 12, md: 6 }}>
<OverviewDonutChart
title="By status"
data={data.contractsByStatus.map((item) => ({
name: CONTRACT_STATUS_META[item.status]?.title ?? item.status,
value: item.count,
}))}
emptyMessage="No contracts yet"
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<OverviewDonutChart
title="By kind"
data={data.contractsByKind.map((item) => ({
name: kindLabel(item.label),
value: item.count,
}))}
/>
</Grid.Col>
</Grid>
<OverviewHorizontalBarChart
title="By freight type"
data={data.contractsByFreightType.map((item) => ({
label: item.label === "CONTAINER" ? "Container" : "Bulk",
value: item.count,
}))}
/>
<OverviewRecentContractsTable contracts={data.recentContracts} />
</Stack>
);
}