diff --git a/frontend/src/components/dashboard/CostPerRouteWidget/CostPerRouteWidget.test.tsx b/frontend/src/components/dashboard/CostPerRouteWidget/CostPerRouteWidget.test.tsx
new file mode 100644
index 0000000..92fdf49
--- /dev/null
+++ b/frontend/src/components/dashboard/CostPerRouteWidget/CostPerRouteWidget.test.tsx
@@ -0,0 +1,24 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import CostPerRouteWidget from './CostPerRouteWidget';
+import { MOCK_ROUTE_COST_DATA } from './mockCostPerRouteData';
+
+describe('CostPerRouteWidget', () => {
+ it('renders chart view by default', () => {
+ render();
+ expect(screen.getByText('Cost per Route')).toBeInTheDocument();
+ expect(screen.getByText('Base')).toBeInTheDocument();
+ expect(screen.getByText('Fuel')).toBeInTheDocument();
+ });
+
+ it('switches to table view and opens drill-down modal', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: /Table/i }));
+ expect(screen.getByRole('columnheader', { name: 'Margin' })).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText('New York, NY → Los Angeles, CA'));
+ expect(screen.getByText('Shipments on New York, NY → Los Angeles, CA')).toBeInTheDocument();
+ expect(screen.getByText('NV-001')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/dashboard/CostPerRouteWidget/CostPerRouteWidget.tsx b/frontend/src/components/dashboard/CostPerRouteWidget/CostPerRouteWidget.tsx
new file mode 100644
index 0000000..ee55885
--- /dev/null
+++ b/frontend/src/components/dashboard/CostPerRouteWidget/CostPerRouteWidget.tsx
@@ -0,0 +1,224 @@
+import React, { useMemo, useState } from 'react';
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+import { BarChart3, Table2 } from 'lucide-react';
+import RouteShipmentsModal from './RouteShipmentsModal';
+import {
+ MOCK_ROUTE_COST_DATA,
+ type RouteCostData,
+} from './mockCostPerRouteData';
+import {
+ COST_SEGMENTS,
+ formatCurrency,
+ getMarginColorClass,
+ getRouteMarginPercent,
+ getRouteTotalCost,
+ toChartRow,
+} from './costPerRouteUtils';
+
+type ViewMode = 'chart' | 'table';
+
+interface CostPerRouteWidgetProps {
+ data?: RouteCostData[];
+}
+
+interface TooltipPayloadItem {
+ name?: string;
+ value?: number;
+ color?: string;
+ dataKey?: string;
+}
+
+interface RouteTooltipProps {
+ active?: boolean;
+ payload?: TooltipPayloadItem[];
+ label?: string;
+}
+
+function RouteTooltip({ active, payload, label }: RouteTooltipProps) {
+ if (!active || !payload?.length) return null;
+
+ const total = payload.reduce((sum, item) => sum + (item.value ?? 0), 0);
+
+ return (
+
+
{label}
+
+ {payload.map((item) => (
+
+ {item.name}
+ {formatCurrency(item.value ?? 0)}
+
+ ))}
+
+
+ Total cost: {formatCurrency(total)}
+
+
+ );
+}
+
+const CostPerRouteWidget: React.FC = ({
+ data = MOCK_ROUTE_COST_DATA,
+}) => {
+ const [viewMode, setViewMode] = useState('chart');
+ const [selectedRoute, setSelectedRoute] = useState(null);
+
+ const sortedRoutes = useMemo(
+ () =>
+ [...data].sort((a, b) => getRouteTotalCost(b) - getRouteTotalCost(a)),
+ [data],
+ );
+
+ const topRoutes = useMemo(() => sortedRoutes.slice(0, 10), [sortedRoutes]);
+ const chartData = useMemo(() => topRoutes.map(toChartRow), [topRoutes]);
+
+ const openRoute = (routeName: string) => {
+ const route = data.find((entry) => entry.route === routeName) ?? null;
+ setSelectedRoute(route);
+ };
+
+ return (
+
+
+
+
+ Route Analytics
+
+
+
+ Cost per Route
+
+
+
+
+
+
+
+
+
+ {viewMode === 'chart' ? (
+ <>
+
+ {COST_SEGMENTS.map((segment) => (
+
+
+ {segment.label}
+
+ ))}
+
+
+
+
+
+
+
+
+ } cursor={{ fill: 'rgba(59, 130, 246, 0.08)' }} />
+ {COST_SEGMENTS.map((segment) => (
+ openRoute(String(barData.route))}
+ className="cursor-pointer"
+ />
+ ))}
+
+
+
+ >
+ ) : (
+
+
+
+
+ | Route |
+ Cost |
+ Revenue |
+ Margin |
+
+
+
+ {sortedRoutes.map((route) => {
+ const margin = getRouteMarginPercent(route);
+ return (
+ setSelectedRoute(route)}
+ >
+ | {route.route} |
+ {formatCurrency(getRouteTotalCost(route))} |
+ {formatCurrency(route.revenue)} |
+
+ {margin.toFixed(1)}%
+ |
+
+ );
+ })}
+
+
+
+ )}
+
+ setSelectedRoute(null)} />
+
+ );
+};
+
+export default CostPerRouteWidget;
diff --git a/frontend/src/components/dashboard/CostPerRouteWidget/RouteShipmentsModal.tsx b/frontend/src/components/dashboard/CostPerRouteWidget/RouteShipmentsModal.tsx
new file mode 100644
index 0000000..a38e4ba
--- /dev/null
+++ b/frontend/src/components/dashboard/CostPerRouteWidget/RouteShipmentsModal.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import Modal from '../../common/Modal/Modal';
+import type { RouteCostData } from './mockCostPerRouteData';
+import { formatCurrency, getRouteMarginPercent, getRouteTotalCost, getMarginColorClass } from './costPerRouteUtils';
+
+interface RouteShipmentsModalProps {
+ route: RouteCostData | null;
+ onClose: () => void;
+}
+
+const RouteShipmentsModal: React.FC = ({ route, onClose }) => {
+ if (!route) return null;
+
+ const totalCost = getRouteTotalCost(route);
+ const margin = getRouteMarginPercent(route);
+
+ return (
+
+
+
+
+
Total cost
+
{formatCurrency(totalCost)}
+
+
+
Revenue
+
{formatCurrency(route.revenue)}
+
+
+
Margin
+
+ {margin.toFixed(1)}%
+
+
+
+
+
+
+
+
+ | Tracking # |
+ Shipment ID |
+ Cost |
+ Revenue |
+
+
+
+ {route.shipments.map((shipment) => (
+
+ | {shipment.trackingNumber} |
+ {shipment.id} |
+ {formatCurrency(shipment.cost)} |
+ {formatCurrency(shipment.revenue)} |
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default RouteShipmentsModal;
diff --git a/frontend/src/components/dashboard/CostPerRouteWidget/costPerRouteUtils.test.ts b/frontend/src/components/dashboard/CostPerRouteWidget/costPerRouteUtils.test.ts
new file mode 100644
index 0000000..3d7fb3b
--- /dev/null
+++ b/frontend/src/components/dashboard/CostPerRouteWidget/costPerRouteUtils.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest';
+import { getMarginColorClass, getRouteMarginPercent, getRouteTotalCost } from './costPerRouteUtils';
+import { MOCK_ROUTE_COST_DATA } from './mockCostPerRouteData';
+
+describe('costPerRouteUtils', () => {
+ it('calculates total cost and margin', () => {
+ const route = MOCK_ROUTE_COST_DATA[0];
+ expect(getRouteTotalCost(route)).toBe(7070);
+ expect(getRouteMarginPercent(route)).toBeCloseTo(27.9, 1);
+ });
+
+ it('color-codes margin thresholds', () => {
+ expect(getMarginColorClass(25)).toBe('text-[#10b981]');
+ expect(getMarginColorClass(15)).toBe('text-[#f59e0b]');
+ expect(getMarginColorClass(5)).toBe('text-[#ef4444]');
+ });
+});
diff --git a/frontend/src/components/dashboard/CostPerRouteWidget/costPerRouteUtils.ts b/frontend/src/components/dashboard/CostPerRouteWidget/costPerRouteUtils.ts
new file mode 100644
index 0000000..4627429
--- /dev/null
+++ b/frontend/src/components/dashboard/CostPerRouteWidget/costPerRouteUtils.ts
@@ -0,0 +1,43 @@
+import type { RouteCostData } from './mockCostPerRouteData';
+
+export const COST_SEGMENTS = [
+ { key: 'base', label: 'Base', color: '#3b82f6' },
+ { key: 'fuel', label: 'Fuel', color: '#f59e0b' },
+ { key: 'customs', label: 'Customs', color: '#8b5cf6' },
+ { key: 'insurance', label: 'Insurance', color: '#10b981' },
+] as const;
+
+export type CostSegmentKey = (typeof COST_SEGMENTS)[number]['key'];
+
+export function getRouteTotalCost(route: RouteCostData): number {
+ return route.base + route.fuel + route.customs + route.insurance;
+}
+
+export function getRouteMarginPercent(route: RouteCostData): number {
+ if (route.revenue <= 0) return 0;
+ return ((route.revenue - getRouteTotalCost(route)) / route.revenue) * 100;
+}
+
+export function getMarginColorClass(margin: number): string {
+ if (margin > 20) return 'text-[#10b981]';
+ if (margin >= 10) return 'text-[#f59e0b]';
+ return 'text-[#ef4444]';
+}
+
+export function formatCurrency(value: number): string {
+ return `$${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
+}
+
+export function toChartRow(route: RouteCostData) {
+ return {
+ route: route.route,
+ routeKey: route.route,
+ base: route.base,
+ fuel: route.fuel,
+ customs: route.customs,
+ insurance: route.insurance,
+ totalCost: getRouteTotalCost(route),
+ revenue: route.revenue,
+ margin: getRouteMarginPercent(route),
+ };
+}
diff --git a/frontend/src/components/dashboard/CostPerRouteWidget/index.ts b/frontend/src/components/dashboard/CostPerRouteWidget/index.ts
new file mode 100644
index 0000000..e4f2821
--- /dev/null
+++ b/frontend/src/components/dashboard/CostPerRouteWidget/index.ts
@@ -0,0 +1,2 @@
+export { default } from './CostPerRouteWidget';
+export { default as CostPerRouteWidget } from './CostPerRouteWidget';
diff --git a/frontend/src/components/dashboard/CostPerRouteWidget/mockCostPerRouteData.ts b/frontend/src/components/dashboard/CostPerRouteWidget/mockCostPerRouteData.ts
new file mode 100644
index 0000000..6e8cd16
--- /dev/null
+++ b/frontend/src/components/dashboard/CostPerRouteWidget/mockCostPerRouteData.ts
@@ -0,0 +1,166 @@
+export interface RouteShipment {
+ id: string;
+ trackingNumber: string;
+ cost: number;
+ revenue: number;
+}
+
+export interface RouteCostData {
+ route: string;
+ origin: string;
+ destination: string;
+ base: number;
+ fuel: number;
+ customs: number;
+ insurance: number;
+ revenue: number;
+ shipments: RouteShipment[];
+}
+
+export const MOCK_ROUTE_COST_DATA: RouteCostData[] = [
+ {
+ route: 'New York, NY → Los Angeles, CA',
+ origin: 'New York, NY',
+ destination: 'Los Angeles, CA',
+ base: 4200,
+ fuel: 1800,
+ customs: 650,
+ insurance: 420,
+ revenue: 9800,
+ shipments: [
+ { id: 'ship-001', trackingNumber: 'NV-001', cost: 7070, revenue: 9800 },
+ { id: 'ship-010', trackingNumber: 'NV-010', cost: 6200, revenue: 8900 },
+ ],
+ },
+ {
+ route: 'Chicago, IL → Houston, TX',
+ origin: 'Chicago, IL',
+ destination: 'Houston, TX',
+ base: 3100,
+ fuel: 1200,
+ customs: 280,
+ insurance: 310,
+ revenue: 6200,
+ shipments: [
+ { id: 'ship-002', trackingNumber: 'NV-002', cost: 4890, revenue: 6200 },
+ ],
+ },
+ {
+ route: 'Seattle, WA → Miami, FL',
+ origin: 'Seattle, WA',
+ destination: 'Miami, FL',
+ base: 5100,
+ fuel: 2400,
+ customs: 920,
+ insurance: 540,
+ revenue: 11200,
+ shipments: [
+ { id: 'ship-003', trackingNumber: 'NV-003', cost: 8960, revenue: 11200 },
+ { id: 'ship-011', trackingNumber: 'NV-011', cost: 8100, revenue: 10400 },
+ ],
+ },
+ {
+ route: 'Boston, MA → Denver, CO',
+ origin: 'Boston, MA',
+ destination: 'Denver, CO',
+ base: 2800,
+ fuel: 1100,
+ customs: 210,
+ insurance: 260,
+ revenue: 5400,
+ shipments: [
+ { id: 'ship-004', trackingNumber: 'NV-004', cost: 4370, revenue: 5400 },
+ ],
+ },
+ {
+ route: 'San Francisco, CA → Dallas, TX',
+ origin: 'San Francisco, CA',
+ destination: 'Dallas, TX',
+ base: 3600,
+ fuel: 1500,
+ customs: 430,
+ insurance: 350,
+ revenue: 7100,
+ shipments: [
+ { id: 'ship-005', trackingNumber: 'NV-005', cost: 5880, revenue: 7100 },
+ ],
+ },
+ {
+ route: 'Atlanta, GA → Phoenix, AZ',
+ origin: 'Atlanta, GA',
+ destination: 'Phoenix, AZ',
+ base: 2500,
+ fuel: 980,
+ customs: 180,
+ insurance: 220,
+ revenue: 4800,
+ shipments: [
+ { id: 'ship-006', trackingNumber: 'NV-006', cost: 3880, revenue: 4800 },
+ ],
+ },
+ {
+ route: 'Singapore → Los Angeles, CA',
+ origin: 'Singapore',
+ destination: 'Los Angeles, CA',
+ base: 6800,
+ fuel: 3200,
+ customs: 2100,
+ insurance: 780,
+ revenue: 15400,
+ shipments: [
+ { id: 'ship-007', trackingNumber: 'NV-007', cost: 12880, revenue: 15400 },
+ ],
+ },
+ {
+ route: 'Houston, TX → Seattle, WA',
+ origin: 'Houston, TX',
+ destination: 'Seattle, WA',
+ base: 3300,
+ fuel: 1400,
+ customs: 360,
+ insurance: 300,
+ revenue: 6900,
+ shipments: [
+ { id: 'ship-008', trackingNumber: 'NV-008', cost: 5360, revenue: 6900 },
+ ],
+ },
+ {
+ route: 'Miami, FL → Chicago, IL',
+ origin: 'Miami, FL',
+ destination: 'Chicago, IL',
+ base: 2900,
+ fuel: 1150,
+ customs: 250,
+ insurance: 240,
+ revenue: 5600,
+ shipments: [
+ { id: 'ship-009', trackingNumber: 'NV-009', cost: 4540, revenue: 5600 },
+ ],
+ },
+ {
+ route: 'Denver, CO → Boston, MA',
+ origin: 'Denver, CO',
+ destination: 'Boston, MA',
+ base: 2400,
+ fuel: 900,
+ customs: 160,
+ insurance: 190,
+ revenue: 4300,
+ shipments: [
+ { id: 'ship-012', trackingNumber: 'NV-012', cost: 3650, revenue: 4300 },
+ ],
+ },
+ {
+ route: 'Dallas, TX → San Francisco, CA',
+ origin: 'Dallas, TX',
+ destination: 'San Francisco, CA',
+ base: 2700,
+ fuel: 1050,
+ customs: 200,
+ insurance: 210,
+ revenue: 5000,
+ shipments: [
+ { id: 'ship-013', trackingNumber: 'NV-013', cost: 4160, revenue: 5000 },
+ ],
+ },
+];
diff --git a/frontend/src/components/dashboard/RevenueTargetWidget/RevenueTargetWidget.test.tsx b/frontend/src/components/dashboard/RevenueTargetWidget/RevenueTargetWidget.test.tsx
new file mode 100644
index 0000000..ff0910c
--- /dev/null
+++ b/frontend/src/components/dashboard/RevenueTargetWidget/RevenueTargetWidget.test.tsx
@@ -0,0 +1,17 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import RevenueTargetWidget from './RevenueTargetWidget';
+import { MOCK_REVENUE_TARGET_DATA } from './mockRevenueTargetData';
+
+describe('RevenueTargetWidget', () => {
+ it('renders gauge center text, actual vs target, and projected EOM', () => {
+ render();
+
+ expect(screen.getByText('Revenue vs Target')).toBeInTheDocument();
+ expect(screen.getByText('Actual')).toBeInTheDocument();
+ expect(screen.getByText('Target')).toBeInTheDocument();
+ expect(screen.getByText('Projected EOM')).toBeInTheDocument();
+ expect(screen.getByText(/of target/)).toBeInTheDocument();
+ expect(screen.getByLabelText('Daily revenue sparkline')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/dashboard/RevenueTargetWidget/RevenueTargetWidget.tsx b/frontend/src/components/dashboard/RevenueTargetWidget/RevenueTargetWidget.tsx
new file mode 100644
index 0000000..1bc27fd
--- /dev/null
+++ b/frontend/src/components/dashboard/RevenueTargetWidget/RevenueTargetWidget.tsx
@@ -0,0 +1,114 @@
+import React, { useMemo } from 'react';
+import { Target } from 'lucide-react';
+import { RadialBar, RadialBarChart, ResponsiveContainer } from 'recharts';
+import {
+ buildRevenueTargetData,
+ type RevenueTargetData,
+} from './mockRevenueTargetData';
+import {
+ buildSparklinePath,
+ formatCurrency,
+ getAchievementPercent,
+ getGaugeColor,
+ getProjectedEndOfMonthRevenue,
+} from './revenueTargetUtils';
+
+interface RevenueTargetWidgetProps {
+ data?: RevenueTargetData;
+}
+
+const RevenueTargetWidget: React.FC = ({
+ data = buildRevenueTargetData(),
+}) => {
+ const achievementPercent = getAchievementPercent(data.actual, data.target);
+ const gaugeColor = getGaugeColor(achievementPercent);
+ const projectedRevenue = getProjectedEndOfMonthRevenue(data.actual);
+ const sparklinePath = useMemo(
+ () => buildSparklinePath(data.dailyRevenue.map((entry) => entry.amount)),
+ [data.dailyRevenue],
+ );
+
+ const gaugeData = [
+ {
+ name: 'Achievement',
+ value: Math.min(achievementPercent, 100),
+ fill: gaugeColor,
+ },
+ ];
+
+ return (
+
+
+
+ Monthly Target
+
+
+
+ Revenue vs Target
+
+
+
+
+
+
+
+
+
+
+
+
{formatCurrency(data.actual)}
+
+ {achievementPercent.toFixed(1)}% of target
+
+
+
+
+
+
+ Actual
+ {formatCurrency(data.actual)}
+
+
+ Target
+ {formatCurrency(data.target)}
+
+
+ Projected EOM
+ {formatCurrency(projectedRevenue)}
+
+
+
+
+
+ Daily revenue
+ This month
+
+
+
+
+ );
+};
+
+export default RevenueTargetWidget;
diff --git a/frontend/src/components/dashboard/RevenueTargetWidget/index.ts b/frontend/src/components/dashboard/RevenueTargetWidget/index.ts
new file mode 100644
index 0000000..17a2ac8
--- /dev/null
+++ b/frontend/src/components/dashboard/RevenueTargetWidget/index.ts
@@ -0,0 +1,2 @@
+export { default } from './RevenueTargetWidget';
+export { default as RevenueTargetWidget } from './RevenueTargetWidget';
diff --git a/frontend/src/components/dashboard/RevenueTargetWidget/mockRevenueTargetData.ts b/frontend/src/components/dashboard/RevenueTargetWidget/mockRevenueTargetData.ts
new file mode 100644
index 0000000..3d4f963
--- /dev/null
+++ b/frontend/src/components/dashboard/RevenueTargetWidget/mockRevenueTargetData.ts
@@ -0,0 +1,52 @@
+export interface DailyRevenue {
+ date: string;
+ amount: number;
+}
+
+export interface RevenueTargetData {
+ actual: number;
+ target: number;
+ dailyRevenue: DailyRevenue[];
+}
+
+function mulberry32(seed: number) {
+ return function random() {
+ let t = (seed += 0x6d2b79f5);
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+ };
+}
+
+export function buildCurrentMonthDailyRevenue(now = new Date()): DailyRevenue[] {
+ const year = now.getFullYear();
+ const month = now.getMonth();
+ const dayOfMonth = now.getDate();
+ const random = mulberry32(year * 100 + month + 1);
+ const days: DailyRevenue[] = [];
+
+ for (let day = 1; day <= dayOfMonth; day += 1) {
+ const date = new Date(year, month, day);
+ const amount = Math.round((4200 + random() * 2800) * 100) / 100;
+ days.push({
+ date: date.toISOString().slice(0, 10),
+ amount,
+ });
+ }
+
+ return days;
+}
+
+export function buildRevenueTargetData(now = new Date()): RevenueTargetData {
+ const dailyRevenue = buildCurrentMonthDailyRevenue(now);
+ const actual = dailyRevenue.reduce((sum, entry) => sum + entry.amount, 0);
+ const target = 150000;
+
+ return {
+ actual,
+ target,
+ dailyRevenue,
+ };
+}
+
+export const MOCK_REVENUE_TARGET_DATA = buildRevenueTargetData();
diff --git a/frontend/src/components/dashboard/RevenueTargetWidget/revenueTargetUtils.test.ts b/frontend/src/components/dashboard/RevenueTargetWidget/revenueTargetUtils.test.ts
new file mode 100644
index 0000000..14266a9
--- /dev/null
+++ b/frontend/src/components/dashboard/RevenueTargetWidget/revenueTargetUtils.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from 'vitest';
+import {
+ getAchievementPercent,
+ getGaugeColor,
+ getProjectedEndOfMonthRevenue,
+} from './revenueTargetUtils';
+
+describe('revenueTargetUtils', () => {
+ it('calculates achievement percent', () => {
+ expect(getAchievementPercent(75000, 100000)).toBe(75);
+ expect(getAchievementPercent(120000, 100000)).toBe(120);
+ });
+
+ it('returns gauge colors by threshold', () => {
+ expect(getGaugeColor(120)).toBe('#10b981');
+ expect(getGaugeColor(85)).toBe('#3b82f6');
+ expect(getGaugeColor(60)).toBe('#f59e0b');
+ expect(getGaugeColor(30)).toBe('#ef4444');
+ });
+
+ it('projects end-of-month revenue from daily run rate', () => {
+ const projected = getProjectedEndOfMonthRevenue(50000, new Date(2026, 5, 15));
+ expect(projected).toBe(100000);
+ });
+});
diff --git a/frontend/src/components/dashboard/RevenueTargetWidget/revenueTargetUtils.ts b/frontend/src/components/dashboard/RevenueTargetWidget/revenueTargetUtils.ts
new file mode 100644
index 0000000..8e3cff3
--- /dev/null
+++ b/frontend/src/components/dashboard/RevenueTargetWidget/revenueTargetUtils.ts
@@ -0,0 +1,42 @@
+export type GaugeColor = '#10b981' | '#3b82f6' | '#f59e0b' | '#ef4444';
+
+export function getAchievementPercent(actual: number, target: number): number {
+ if (target <= 0) return 0;
+ return (actual / target) * 100;
+}
+
+export function getGaugeColor(percent: number): GaugeColor {
+ if (percent >= 100) return '#10b981';
+ if (percent >= 75) return '#3b82f6';
+ if (percent >= 50) return '#f59e0b';
+ return '#ef4444';
+}
+
+export function getProjectedEndOfMonthRevenue(
+ actual: number,
+ now = new Date(),
+): number {
+ const dayOfMonth = now.getDate();
+ if (dayOfMonth <= 0) return actual;
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
+ return (actual / dayOfMonth) * daysInMonth;
+}
+
+export function formatCurrency(value: number): string {
+ return `$${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
+}
+
+export function buildSparklinePath(values: number[]): string {
+ if (values.length === 0) return '';
+ const max = Math.max(...values);
+ const min = Math.min(...values);
+ const range = max - min || 1;
+
+ return values
+ .map((value, index) => {
+ const x = (index / Math.max(values.length - 1, 1)) * 100;
+ const y = 100 - ((value - min) / range) * 100;
+ return `${index === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`;
+ })
+ .join(' ');
+}
diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts
index bd65ed5..fbc7f90 100644
--- a/frontend/src/components/index.ts
+++ b/frontend/src/components/index.ts
@@ -50,6 +50,8 @@ export { default as StatCard } from './dashboard/StatCard';
export type { StatCardProps } from './dashboard/StatCard';
export { default as DeliverySuccessChart } from './dashboard/Charts/DeliverySuccessChart';
export { default as ShipmentVolumeChart } from './dashboard/Charts/ShipmentVolumeChart';
+export { default as RevenueTargetWidget } from './dashboard/RevenueTargetWidget';
+export { default as CostPerRouteWidget } from './dashboard/CostPerRouteWidget';
// Shipment
export { default as TrackingTimeline } from './shipment/TrackingTimeline';
diff --git a/frontend/src/pages/dashboard/Company/CompanyDashboard.tsx b/frontend/src/pages/dashboard/Company/CompanyDashboard.tsx
index dee23ca..2b9f943 100644
--- a/frontend/src/pages/dashboard/Company/CompanyDashboard.tsx
+++ b/frontend/src/pages/dashboard/Company/CompanyDashboard.tsx
@@ -10,6 +10,7 @@ import RecentShipments from './RecentShipments/RecentShipments';
import RecentActivityFeed from './RecentActivity/RecentActivityFeed';
import ShipmentsMapWidget from './ShipmentsMap/ShipmentsMapWidget';
import RevenueSummaryWidget from './RevenueSummary/RevenueSummaryWidget';
+import { CostPerRouteWidget, RevenueTargetWidget } from '@components';
import OnboardingTour, { isTourComplete } from '@components/onboarding/OnboardingTour';
import type { TourStep } from '@components/onboarding/OnboardingTour';
@@ -163,6 +164,11 @@ const CompanyDashboard: React.FC = () => {
+
+
+
+
+
{/* Shipments */}