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) => ( +
+
+ ))} +
+ +
+ + + + + + } cursor={{ fill: 'rgba(59, 130, 246, 0.08)' }} /> + {COST_SEGMENTS.map((segment) => ( + openRoute(String(barData.route))} + className="cursor-pointer" + /> + ))} + + +
+ + ) : ( +
+ + + + + + + + + + + {sortedRoutes.map((route) => { + const margin = getRouteMarginPercent(route); + return ( + setSelectedRoute(route)} + > + + + + + + ); + })} + +
RouteCostRevenueMargin
{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)}% +

+
+
+ +
+ + + + + + + + + + + {route.shipments.map((shipment) => ( + + + + + + + ))} + +
Tracking #Shipment IDCostRevenue
{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 */}