mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user