@burdenoff/microfe-movethewheels 2026.805.1 → 2026.822.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CarbonFootprintPanel.js +108 -0
- package/dist/components/index.js +1 -0
- package/dist/hooks/useAnalytics.js +45 -4
- package/dist/index.js +29 -28
- package/dist/movethewheels/components/CarbonFootprintPanel.d.ts +7 -0
- package/dist/movethewheels/components/index.d.ts +1 -0
- package/dist/movethewheels/hooks/index.d.ts +1 -1
- package/dist/movethewheels/hooks/useAnalytics.d.ts +22 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/leaf.js +11 -0
- package/dist/pages/AnalyticsPage.js +81 -79
- package/package.json +1 -1
- package/src/movethewheels/components/CarbonFootprintPanel.tsx +133 -0
- package/src/movethewheels/components/UpcomingDemandPanel.tsx +20 -11
- package/src/movethewheels/components/index.ts +3 -0
- package/src/movethewheels/hooks/index.ts +2 -0
- package/src/movethewheels/hooks/useAnalytics.ts +86 -0
- package/src/movethewheels/hooks/useDemandForecast.ts +10 -4
- package/src/movethewheels/pages/AnalyticsPage.tsx +5 -5
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Leaf as e } from "../node_modules/lucide-react/dist/esm/icons/leaf.js";
|
|
2
|
+
import { Truck as t } from "../node_modules/lucide-react/dist/esm/icons/truck.js";
|
|
3
|
+
import { useEmissionsSummary as n } from "../hooks/useAnalytics.js";
|
|
4
|
+
import { Card as r, CardContent as i, CardHeader as a, CardTitle as o } from "./ui.js";
|
|
5
|
+
import { MetricCard as s, MetricGrid as c } from "./MetricCard.js";
|
|
6
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
7
|
+
//#region src/movethewheels/components/CarbonFootprintPanel.tsx
|
|
8
|
+
function d(e) {
|
|
9
|
+
return e >= 1e3 ? `${(e / 1e3).toFixed(2)} t` : `${e.toFixed(1)} kg`;
|
|
10
|
+
}
|
|
11
|
+
function f(e) {
|
|
12
|
+
return e >= 1e3 ? `${(e / 1e3).toFixed(1)}k km` : `${e.toFixed(1)} km`;
|
|
13
|
+
}
|
|
14
|
+
function p() {
|
|
15
|
+
let { summary: p, isLoading: m, error: h } = n();
|
|
16
|
+
if (h) return /* @__PURE__ */ l(r, {
|
|
17
|
+
className: "border-status-error-border",
|
|
18
|
+
children: /* @__PURE__ */ l(i, {
|
|
19
|
+
className: "p-6 text-center",
|
|
20
|
+
children: /* @__PURE__ */ l("p", {
|
|
21
|
+
className: "text-sm text-status-error-text",
|
|
22
|
+
children: "Couldn't load emissions data"
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
if (m) return /* @__PURE__ */ u(r, { children: [/* @__PURE__ */ l(a, { children: /* @__PURE__ */ u(o, {
|
|
27
|
+
className: "flex items-center gap-2",
|
|
28
|
+
children: [/* @__PURE__ */ l(e, {
|
|
29
|
+
size: 18,
|
|
30
|
+
className: "text-status-success-text"
|
|
31
|
+
}), "Carbon footprint"]
|
|
32
|
+
}) }), /* @__PURE__ */ l(i, { children: /* @__PURE__ */ l(c, {
|
|
33
|
+
columns: 3,
|
|
34
|
+
children: Array.from({ length: 3 }).map((e, t) => /* @__PURE__ */ l("div", { className: "h-24 rounded-xl bg-bg-sunken motion-safe:animate-pulse" }, t))
|
|
35
|
+
}) })] });
|
|
36
|
+
let g = p.totalDistanceKm > 0;
|
|
37
|
+
return /* @__PURE__ */ u(r, { children: [/* @__PURE__ */ l(a, { children: /* @__PURE__ */ u(o, {
|
|
38
|
+
className: "flex items-center gap-2",
|
|
39
|
+
children: [/* @__PURE__ */ l(e, {
|
|
40
|
+
size: 18,
|
|
41
|
+
className: "text-status-success-text"
|
|
42
|
+
}), "Carbon footprint"]
|
|
43
|
+
}) }), /* @__PURE__ */ u(i, {
|
|
44
|
+
className: "space-y-6",
|
|
45
|
+
children: [
|
|
46
|
+
/* @__PURE__ */ u(c, {
|
|
47
|
+
columns: 3,
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ l(s, {
|
|
50
|
+
label: "Total CO2e",
|
|
51
|
+
value: d(p.totalCo2eKg),
|
|
52
|
+
icon: /* @__PURE__ */ l(e, { size: 20 }),
|
|
53
|
+
accent: "success"
|
|
54
|
+
}),
|
|
55
|
+
/* @__PURE__ */ l(s, {
|
|
56
|
+
label: "Total distance",
|
|
57
|
+
value: f(p.totalDistanceKm),
|
|
58
|
+
icon: /* @__PURE__ */ l(t, { size: 20 }),
|
|
59
|
+
accent: "blue"
|
|
60
|
+
}),
|
|
61
|
+
/* @__PURE__ */ l(s, {
|
|
62
|
+
label: "Avg intensity",
|
|
63
|
+
value: `${(p.averageCo2eKgPerKm * 1e3).toFixed(1)} g/km`,
|
|
64
|
+
icon: /* @__PURE__ */ l(e, { size: 20 }),
|
|
65
|
+
accent: "warning"
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
}),
|
|
69
|
+
g && p.byVehicle.length > 0 && /* @__PURE__ */ u("div", {
|
|
70
|
+
className: "space-y-3",
|
|
71
|
+
children: [/* @__PURE__ */ l("h4", {
|
|
72
|
+
className: "text-sm font-medium text-text-secondary",
|
|
73
|
+
children: "By vehicle"
|
|
74
|
+
}), /* @__PURE__ */ l("div", {
|
|
75
|
+
className: "space-y-2",
|
|
76
|
+
children: p.byVehicle.filter((e) => e.totalDistanceKm > 0 || e.co2eKg > 0).slice(0, 5).map((e) => /* @__PURE__ */ u("div", {
|
|
77
|
+
className: "flex items-center justify-between rounded-lg bg-bg-sunken px-3 py-2",
|
|
78
|
+
children: [/* @__PURE__ */ u("div", {
|
|
79
|
+
className: "flex items-center gap-2",
|
|
80
|
+
children: [/* @__PURE__ */ l(t, {
|
|
81
|
+
size: 16,
|
|
82
|
+
className: "text-text-secondary"
|
|
83
|
+
}), /* @__PURE__ */ l("span", {
|
|
84
|
+
className: "text-sm font-medium text-text-primary",
|
|
85
|
+
children: e.vehicleNumber ?? "Unassigned"
|
|
86
|
+
})]
|
|
87
|
+
}), /* @__PURE__ */ u("div", {
|
|
88
|
+
className: "text-right",
|
|
89
|
+
children: [/* @__PURE__ */ l("p", {
|
|
90
|
+
className: "text-sm font-medium text-text-primary",
|
|
91
|
+
children: d(e.co2eKg)
|
|
92
|
+
}), /* @__PURE__ */ l("p", {
|
|
93
|
+
className: "text-xs text-text-secondary",
|
|
94
|
+
children: f(e.totalDistanceKm)
|
|
95
|
+
})]
|
|
96
|
+
})]
|
|
97
|
+
}, e.vehicleId ?? "unassigned"))
|
|
98
|
+
})]
|
|
99
|
+
}),
|
|
100
|
+
!g && /* @__PURE__ */ l("p", {
|
|
101
|
+
className: "text-sm text-text-secondary",
|
|
102
|
+
children: "No route distance recorded yet. Assign vehicles and distances to routes to see emissions estimates."
|
|
103
|
+
})
|
|
104
|
+
]
|
|
105
|
+
})] });
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { p as CarbonFootprintPanel };
|
package/dist/components/index.js
CHANGED
|
@@ -287,8 +287,8 @@ function m() {
|
|
|
287
287
|
error: n
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
|
-
var h = "\n query GetMovethewheelsFinanceMetrics {\n financeMetrics {\n revenue {\n totalRevenue\n deliveredRevenue\n averageOrderValue\n }\n fuelCost\n openInvoices\n }\n }\n";
|
|
291
|
-
function
|
|
290
|
+
var h = "\n query GetMovethewheelsFinanceMetrics {\n financeMetrics {\n revenue {\n totalRevenue\n deliveredRevenue\n averageOrderValue\n }\n fuelCost\n openInvoices\n }\n }\n", g = "\n query GetMovethewheelsEmissionsSummary {\n emissionsSummary {\n totalCo2eKg\n totalDistanceKm\n averageCo2eKgPerKm\n byVehicle {\n vehicleId\n vehicleNumber\n totalDistanceKm\n co2eKg\n }\n }\n }\n";
|
|
291
|
+
function _() {
|
|
292
292
|
let { apiGatewayUrl: o, authToken: s, workspaceToken: c, workspaceId: l, tenantId: u, currentUser: d } = e(), [f, p] = a({
|
|
293
293
|
totalRevenue: 0,
|
|
294
294
|
deliveredRevenue: 0,
|
|
@@ -337,7 +337,7 @@ function g() {
|
|
|
337
337
|
}, [b])
|
|
338
338
|
};
|
|
339
339
|
}
|
|
340
|
-
function
|
|
340
|
+
function v() {
|
|
341
341
|
let { analytics: e, isLoading: t, error: n } = u();
|
|
342
342
|
return {
|
|
343
343
|
performanceData: i(() => ({
|
|
@@ -348,5 +348,46 @@ function _() {
|
|
|
348
348
|
error: n
|
|
349
349
|
};
|
|
350
350
|
}
|
|
351
|
+
function y() {
|
|
352
|
+
let { apiGatewayUrl: o, authToken: s, workspaceToken: c, workspaceId: l, tenantId: u, currentUser: d } = e(), [f, p] = a({
|
|
353
|
+
totalCo2eKg: 0,
|
|
354
|
+
totalDistanceKm: 0,
|
|
355
|
+
averageCo2eKgPerKm: 0,
|
|
356
|
+
byVehicle: []
|
|
357
|
+
}), [m, h] = a(!1), [_, v] = a(null), y = i(() => ({
|
|
358
|
+
apiGatewayUrl: o,
|
|
359
|
+
authToken: s,
|
|
360
|
+
workspaceToken: c,
|
|
361
|
+
workspaceId: l,
|
|
362
|
+
tenantId: u,
|
|
363
|
+
actorId: d?.id
|
|
364
|
+
}), [
|
|
365
|
+
o,
|
|
366
|
+
s,
|
|
367
|
+
c,
|
|
368
|
+
l,
|
|
369
|
+
u,
|
|
370
|
+
d?.id
|
|
371
|
+
]), b = n(async () => {
|
|
372
|
+
h(!0), v(null);
|
|
373
|
+
try {
|
|
374
|
+
p((await t(y, g)).emissionsSummary);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
v(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to load emissions summary"));
|
|
377
|
+
} finally {
|
|
378
|
+
h(!1);
|
|
379
|
+
}
|
|
380
|
+
}, [y]);
|
|
381
|
+
return r(() => {
|
|
382
|
+
b();
|
|
383
|
+
}, [b]), {
|
|
384
|
+
summary: f,
|
|
385
|
+
isLoading: m,
|
|
386
|
+
error: _,
|
|
387
|
+
refetch: n(() => {
|
|
388
|
+
b();
|
|
389
|
+
}, [b])
|
|
390
|
+
};
|
|
391
|
+
}
|
|
351
392
|
//#endregion
|
|
352
|
-
export { u as useAnalytics, d as useDashboardMetrics,
|
|
393
|
+
export { u as useAnalytics, d as useDashboardMetrics, y as useEmissionsSummary, _ as useFinanceMetrics, m as useFleetAnalytics, p as useOrderAnalytics, v as usePerformanceAnalytics, f as useRevenueAnalytics };
|
package/dist/index.js
CHANGED
|
@@ -7,33 +7,34 @@ import { useDriver as c, useDrivers as l, useFleet as u, useFleetStats as d, use
|
|
|
7
7
|
import { useInventory as m, useInventoryStats as h, useProduct as g, useProducts as _, useWarehouse as v, useWarehouses as y } from "./hooks/useInventory.js";
|
|
8
8
|
import { useCustomer as b, useCustomerStats as x, useCustomers as S } from "./hooks/useCustomers.js";
|
|
9
9
|
import { useRoute as C, useRouteStats as w, useRoutes as T } from "./hooks/useRoutes.js";
|
|
10
|
-
import { useAnalytics as E, useDashboardMetrics as D,
|
|
11
|
-
import { useDriverTracking as
|
|
12
|
-
import { useReports as
|
|
13
|
-
import { useImportExport as
|
|
14
|
-
import { useModuleSettings as
|
|
15
|
-
import { useEntityDocuments as
|
|
16
|
-
import { useCaptureEpod as
|
|
10
|
+
import { useAnalytics as E, useDashboardMetrics as D, useEmissionsSummary as O, useFinanceMetrics as k, useFleetAnalytics as A, useOrderAnalytics as j, usePerformanceAnalytics as M, useRevenueAnalytics as N } from "./hooks/useAnalytics.js";
|
|
11
|
+
import { useDriverTracking as P, useGeofence as F, useLiveTracking as I, useOrderTracking as L, useTracking as R, useTrackingAlerts as z, useVehicleTracking as B } from "./hooks/useTracking.js";
|
|
12
|
+
import { useReports as V } from "./hooks/useReports.js";
|
|
13
|
+
import { useImportExport as H } from "./hooks/useImportExport.js";
|
|
14
|
+
import { useModuleSettings as U } from "./hooks/useSettings.js";
|
|
15
|
+
import { useEntityDocuments as W } from "./hooks/useEntityDocuments.js";
|
|
16
|
+
import { useCaptureEpod as G, useEpod as K, useEpodPhotoUpload as q, useEpodPhotoUrl as J, useMyPendingEpodDeliveryJobs as Y } from "./hooks/useEpod.js";
|
|
17
17
|
import "./hooks/index.js";
|
|
18
|
-
import { Avatar as
|
|
19
|
-
import { EmptyState as
|
|
20
|
-
import { chartTokens as
|
|
21
|
-
import { Sparkline as
|
|
22
|
-
import { MetricCard as
|
|
23
|
-
import { GlassPanel as
|
|
24
|
-
import { ChartEmpty as
|
|
25
|
-
import { TrendChart as
|
|
26
|
-
import { VolumeBar as
|
|
27
|
-
import { StatusDonut as
|
|
28
|
-
import { createCurrentIcon as
|
|
29
|
-
import { MapCanvas as
|
|
30
|
-
import { LogisticsMap as
|
|
31
|
-
import { RouteMap as
|
|
32
|
-
import { TrackingMap as
|
|
33
|
-
import { PaymentStatusBadge as
|
|
34
|
-
import { DataTable as
|
|
35
|
-
import { FormError as
|
|
36
|
-
import { Notice as
|
|
37
|
-
import { DocumentsCard as
|
|
18
|
+
import { Avatar as X, AvatarFallback as Z, AvatarImage as Q, Badge as $, Button as ee, Card as te, CardContent as ne, CardDescription as re, CardFooter as ie, CardHeader as ae, CardTitle as oe, Checkbox as se, Dialog as ce, DialogContent as le, DialogDescription as ue, DialogFooter as de, DialogHeader as fe, DialogTitle as pe, DialogTrigger as me, DropdownMenu as he, DropdownMenuContent as ge, DropdownMenuItem as _e, DropdownMenuSeparator as ve, DropdownMenuTrigger as ye, Input as be, Label as xe, Progress as Se, ScrollArea as Ce, Select as we, SelectContent as Te, SelectItem as Ee, SelectTrigger as De, SelectValue as Oe, Separator as ke, Skeleton as Ae, Switch as je, Table as Me, TableBody as Ne, TableCell as Pe, TableHead as Fe, TableHeader as Ie, TableRow as Le, Tabs as Re, TabsContent as ze, TabsList as Be, TabsTrigger as Ve, Textarea as He, Tooltip as Ue, TooltipContent as We, TooltipProvider as Ge, TooltipTrigger as Ke } from "./components/ui.js";
|
|
19
|
+
import { EmptyState as qe, ErrorState as Je, LoadingState as Ye, PageLayout as Xe, PageSection as Ze } from "./components/PageLayout.js";
|
|
20
|
+
import { chartTokens as Qe, seriesColor as $e } from "./components/charts/chartTokens.js";
|
|
21
|
+
import { Sparkline as et } from "./components/charts/Sparkline.js";
|
|
22
|
+
import { MetricCard as tt, MetricCardSkeleton as nt, MetricGrid as rt } from "./components/MetricCard.js";
|
|
23
|
+
import { GlassPanel as it } from "./components/GlassPanel.js";
|
|
24
|
+
import { ChartEmpty as at, ChartTooltip as ot } from "./components/charts/ChartShell.js";
|
|
25
|
+
import { TrendChart as st } from "./components/charts/TrendChart.js";
|
|
26
|
+
import { VolumeBar as ct } from "./components/charts/VolumeBar.js";
|
|
27
|
+
import { StatusDonut as lt } from "./components/charts/StatusDonut.js";
|
|
28
|
+
import { createCurrentIcon as ut, createPinIcon as dt, createStopIcon as ft, isRealCoord as pt } from "./components/maps/leaflet-setup.js";
|
|
29
|
+
import { MapCanvas as mt } from "./components/maps/MapCanvas.js";
|
|
30
|
+
import { LogisticsMap as ht } from "./components/maps/LogisticsMap.js";
|
|
31
|
+
import { RouteMap as gt } from "./components/maps/RouteMap.js";
|
|
32
|
+
import { TrackingMap as _t } from "./components/maps/TrackingMap.js";
|
|
33
|
+
import { PaymentStatusBadge as vt, PriorityBadge as yt, StatusBadge as bt, TypeBadge as xt, VehicleTypeBadge as St } from "./components/StatusBadge.js";
|
|
34
|
+
import { DataTable as Ct } from "./components/DataTable.js";
|
|
35
|
+
import { FormError as wt } from "./components/FormError.js";
|
|
36
|
+
import { Notice as Tt, useNotice as Et } from "./components/Notice.js";
|
|
37
|
+
import { DocumentsCard as Dt } from "./components/DocumentsCard.js";
|
|
38
|
+
import { CarbonFootprintPanel as Ot } from "./components/CarbonFootprintPanel.js";
|
|
38
39
|
import "./components/index.js";
|
|
39
|
-
export {
|
|
40
|
+
export { X as Avatar, Z as AvatarFallback, Q as AvatarImage, $ as Badge, ee as Button, Ot as CarbonFootprintPanel, te as Card, ne as CardContent, re as CardDescription, ie as CardFooter, ae as CardHeader, oe as CardTitle, at as ChartEmpty, ot as ChartTooltip, se as Checkbox, Ct as DataTable, ce as Dialog, le as DialogContent, ue as DialogDescription, de as DialogFooter, fe as DialogHeader, pe as DialogTitle, me as DialogTrigger, Dt as DocumentsCard, he as DropdownMenu, ge as DropdownMenuContent, _e as DropdownMenuItem, ve as DropdownMenuSeparator, ye as DropdownMenuTrigger, qe as EmptyState, Je as ErrorState, wt as FormError, it as GlassPanel, be as Input, xe as Label, Ye as LoadingState, ht as LogisticsMap, mt as MapCanvas, tt as MetricCard, nt as MetricCardSkeleton, rt as MetricGrid, n as MoveTheWheelsRoot, Tt as Notice, Xe as PageLayout, Ze as PageSection, vt as PaymentStatusBadge, yt as PriorityBadge, Se as Progress, gt as RouteMap, Ce as ScrollArea, we as Select, Te as SelectContent, Ee as SelectItem, De as SelectTrigger, Oe as SelectValue, ke as Separator, Ae as Skeleton, et as Sparkline, bt as StatusBadge, lt as StatusDonut, je as Switch, Me as Table, Ne as TableBody, Pe as TableCell, Fe as TableHead, Ie as TableHeader, Le as TableRow, Re as Tabs, ze as TabsContent, Be as TabsList, Ve as TabsTrigger, He as Textarea, Ue as Tooltip, We as TooltipContent, Ge as TooltipProvider, Ke as TooltipTrigger, _t as TrackingMap, st as TrendChart, xt as TypeBadge, St as VehicleTypeBadge, ct as VolumeBar, Qe as chartTokens, ut as createCurrentIcon, dt as createPinIcon, ft as createStopIcon, pt as isRealCoord, $e as seriesColor, E as useAnalytics, G as useCaptureEpod, b as useCustomer, x as useCustomerStats, S as useCustomers, D as useDashboardMetrics, c as useDriver, P as useDriverTracking, l as useDrivers, O as useEmissionsSummary, W as useEntityDocuments, K as useEpod, q as useEpodPhotoUpload, J as useEpodPhotoUrl, k as useFinanceMetrics, u as useFleet, A as useFleetAnalytics, d as useFleetStats, F as useGeofence, H as useImportExport, m as useInventory, h as useInventoryStats, I as useLiveTracking, U as useModuleSettings, e as useMoveTheWheels, t as useMoveTheWheelsNavigate, r as useMoveTheWheelsStore, Y as useMyPendingEpodDeliveryJobs, Et as useNotice, a as useOrder, j as useOrderAnalytics, o as useOrderStats, L as useOrderTracking, s as useOrders, M as usePerformanceAnalytics, g as useProduct, _ as useProducts, V as useReports, N as useRevenueAnalytics, C as useRoute, w as useRouteStats, T as useRoutes, i as useSandboxAssistantTransport, R as useTracking, z as useTrackingAlerts, f as useVehicle, B as useVehicleTracking, p as useVehicles, v as useWarehouse, y as useWarehouses };
|
|
@@ -8,7 +8,7 @@ export { useFleet, useVehicles, useVehicle, useDrivers, useDriver, useFleetStats
|
|
|
8
8
|
export { useInventory, useProducts, useProduct, useWarehouses, useWarehouse, useInventoryStats, } from './useInventory';
|
|
9
9
|
export { useCustomers, useCustomer, useCustomerStats } from './useCustomers';
|
|
10
10
|
export { useRoutes, useRoute, useRouteStats } from './useRoutes';
|
|
11
|
-
export { useAnalytics, useDashboardMetrics, useRevenueAnalytics, useOrderAnalytics, useFleetAnalytics, usePerformanceAnalytics, useFinanceMetrics, type FinanceMetrics, } from './useAnalytics';
|
|
11
|
+
export { useAnalytics, useDashboardMetrics, useRevenueAnalytics, useOrderAnalytics, useFleetAnalytics, usePerformanceAnalytics, useFinanceMetrics, type FinanceMetrics, useEmissionsSummary, type EmissionsSummary, } from './useAnalytics';
|
|
12
12
|
export { useTracking, useOrderTracking, useVehicleTracking, useDriverTracking, useLiveTracking, useTrackingAlerts, useGeofence, } from './useTracking';
|
|
13
13
|
export { useReports } from './useReports';
|
|
14
14
|
export { useImportExport } from './useImportExport';
|
|
@@ -133,4 +133,26 @@ export declare function usePerformanceAnalytics(): {
|
|
|
133
133
|
isLoading: boolean;
|
|
134
134
|
error: Error | null;
|
|
135
135
|
};
|
|
136
|
+
export interface EmissionsSummary {
|
|
137
|
+
totalCo2eKg: number;
|
|
138
|
+
totalDistanceKm: number;
|
|
139
|
+
averageCo2eKgPerKm: number;
|
|
140
|
+
byVehicle: Array<{
|
|
141
|
+
vehicleId: string | null;
|
|
142
|
+
vehicleNumber: string | null;
|
|
143
|
+
totalDistanceKm: number;
|
|
144
|
+
co2eKg: number;
|
|
145
|
+
}>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Real emissions figures straight from Query.emissionsSummary (BOFF-4391).
|
|
149
|
+
* Returns totals, per-km intensity, and a per-vehicle breakdown sourced from
|
|
150
|
+
* cached route CO2e values and actual/estimated distances.
|
|
151
|
+
*/
|
|
152
|
+
export declare function useEmissionsSummary(): {
|
|
153
|
+
summary: EmissionsSummary;
|
|
154
|
+
isLoading: boolean;
|
|
155
|
+
error: Error | null;
|
|
156
|
+
refetch: () => void;
|
|
157
|
+
};
|
|
136
158
|
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createLucideIcon as e } from "../createLucideIcon.js";
|
|
2
|
+
//#region node_modules/lucide-react/dist/esm/icons/leaf.js
|
|
3
|
+
var t = e("Leaf", [["path", {
|
|
4
|
+
d: "M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z",
|
|
5
|
+
key: "nnexq3"
|
|
6
|
+
}], ["path", {
|
|
7
|
+
d: "M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12",
|
|
8
|
+
key: "mt58a7"
|
|
9
|
+
}]]);
|
|
10
|
+
//#endregion
|
|
11
|
+
export { t as Leaf };
|
|
@@ -13,77 +13,78 @@ import { VolumeBar as b } from "../components/charts/VolumeBar.js";
|
|
|
13
13
|
import { StatusDonut as x } from "../components/charts/StatusDonut.js";
|
|
14
14
|
import "../components/charts/index.js";
|
|
15
15
|
import { formatCurrency as S } from "../utils/formatters.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
16
|
+
import { CarbonFootprintPanel as C } from "../components/CarbonFootprintPanel.js";
|
|
17
|
+
import { UpcomingDemandPanel as w } from "../components/UpcomingDemandPanel.js";
|
|
18
|
+
import { useDemandForecast as T } from "../hooks/useDemandForecast.js";
|
|
19
|
+
import { useMemo as E } from "react";
|
|
20
|
+
import { jsx as D, jsxs as O } from "react/jsx-runtime";
|
|
21
|
+
import { EmphasisPanel as k, PagePurpose as A } from "@burdenoff/fe-libs/ui";
|
|
21
22
|
//#region src/movethewheels/pages/AnalyticsPage.tsx
|
|
22
|
-
var
|
|
23
|
-
function
|
|
24
|
-
let { analytics:
|
|
23
|
+
var j = (e) => e.replace(/_/g, " ").replace(/\b\w/g, (e) => e.toUpperCase());
|
|
24
|
+
function M() {
|
|
25
|
+
let { analytics: M, isLoading: N, error: P, refetch: F } = o(), { orderData: I } = c(), { fleetData: L } = s(), { laneForecast: R, customerForecast: z, laneVariance: B, customerVariance: V, isLoading: H, error: U, refetch: W } = T(4, 8), G = E(() => M.trends.dailyOrders.map((e) => e.orders), [M.trends.dailyOrders]), K = E(() => M.trends.revenue.map((e) => ({
|
|
25
26
|
month: e.month,
|
|
26
27
|
revenue: e.revenue
|
|
27
|
-
})), [
|
|
28
|
-
name:
|
|
28
|
+
})), [M.trends.revenue]), q = E(() => I.byStatus.filter((e) => e.count > 0).map((e) => ({
|
|
29
|
+
name: j(e.status),
|
|
29
30
|
value: e.count
|
|
30
|
-
})), [
|
|
31
|
-
return /* @__PURE__ */
|
|
31
|
+
})), [I.byStatus]), J = q.reduce((e, t) => e + t.value, 0);
|
|
32
|
+
return /* @__PURE__ */ O(h, {
|
|
32
33
|
title: "Analytics",
|
|
33
34
|
subtitle: "Business performance and insights",
|
|
34
|
-
icon: /* @__PURE__ */
|
|
35
|
+
icon: /* @__PURE__ */ D(e, { size: 24 }),
|
|
35
36
|
children: [
|
|
36
|
-
/* @__PURE__ */
|
|
37
|
-
icon: /* @__PURE__ */
|
|
37
|
+
/* @__PURE__ */ D(A, {
|
|
38
|
+
icon: /* @__PURE__ */ D(e, { size: 18 }),
|
|
38
39
|
children: "See how the operation is really performing. Track order volume, revenue, on-time rate and customer satisfaction over time so you can spot trends, find bottlenecks, and decide where to invest — whether that's more vehicles, better routes, or new customers."
|
|
39
40
|
}),
|
|
40
|
-
|
|
41
|
+
P && !N ? /* @__PURE__ */ D(u, {
|
|
41
42
|
className: "border-status-error-border",
|
|
42
|
-
children: /* @__PURE__ */
|
|
43
|
+
children: /* @__PURE__ */ O(d, {
|
|
43
44
|
className: "p-6 text-center space-y-3",
|
|
44
45
|
children: [
|
|
45
|
-
/* @__PURE__ */
|
|
46
|
+
/* @__PURE__ */ D("p", {
|
|
46
47
|
className: "text-status-error-text font-medium",
|
|
47
48
|
children: "Couldn't load analytics"
|
|
48
49
|
}),
|
|
49
|
-
/* @__PURE__ */
|
|
50
|
+
/* @__PURE__ */ D("p", {
|
|
50
51
|
className: "text-sm text-text-secondary",
|
|
51
|
-
children:
|
|
52
|
+
children: P.message
|
|
52
53
|
}),
|
|
53
|
-
/* @__PURE__ */
|
|
54
|
+
/* @__PURE__ */ D(l, {
|
|
54
55
|
variant: "outline",
|
|
55
|
-
onClick:
|
|
56
|
+
onClick: F,
|
|
56
57
|
children: "Retry"
|
|
57
58
|
})
|
|
58
59
|
]
|
|
59
60
|
})
|
|
60
|
-
}) :
|
|
61
|
+
}) : N ? /* @__PURE__ */ D(v, {
|
|
61
62
|
columns: 4,
|
|
62
|
-
children: Array.from({ length: 4 }).map((e, t) => /* @__PURE__ */
|
|
63
|
-
}) : /* @__PURE__ */
|
|
63
|
+
children: Array.from({ length: 4 }).map((e, t) => /* @__PURE__ */ D(_, {}, t))
|
|
64
|
+
}) : /* @__PURE__ */ O(v, {
|
|
64
65
|
columns: 4,
|
|
65
66
|
children: [
|
|
66
|
-
/* @__PURE__ */
|
|
67
|
+
/* @__PURE__ */ D(g, {
|
|
67
68
|
label: "Order Volume",
|
|
68
|
-
value:
|
|
69
|
-
icon: /* @__PURE__ */
|
|
69
|
+
value: M.overview.totalOrders.toLocaleString(),
|
|
70
|
+
icon: /* @__PURE__ */ D(n, { size: 20 }),
|
|
70
71
|
accent: "brand",
|
|
71
|
-
sparklineData:
|
|
72
|
+
sparklineData: G.length >= 2 ? G : void 0
|
|
72
73
|
}),
|
|
73
|
-
/* @__PURE__ */
|
|
74
|
+
/* @__PURE__ */ D(g, {
|
|
74
75
|
treatment: "glass",
|
|
75
76
|
glow: !0,
|
|
76
77
|
label: "Revenue",
|
|
77
|
-
value: S(
|
|
78
|
-
icon: /* @__PURE__ */
|
|
78
|
+
value: S(M.overview.totalRevenue),
|
|
79
|
+
icon: /* @__PURE__ */ D(t, { size: 20 }),
|
|
79
80
|
accent: "success"
|
|
80
81
|
}),
|
|
81
|
-
/* @__PURE__ */
|
|
82
|
+
/* @__PURE__ */ D(g, {
|
|
82
83
|
label: "On-Time Delivery",
|
|
83
|
-
value: `${
|
|
84
|
-
icon: /* @__PURE__ */
|
|
84
|
+
value: `${M.overview.onTimeDeliveryRate.toFixed(1)}%`,
|
|
85
|
+
icon: /* @__PURE__ */ D(i, { size: 20 }),
|
|
85
86
|
accent: "blue",
|
|
86
|
-
delta:
|
|
87
|
+
delta: M.overview.onTimeDeliveryRate >= 90 ? {
|
|
87
88
|
value: "On target",
|
|
88
89
|
direction: "up"
|
|
89
90
|
} : {
|
|
@@ -91,19 +92,19 @@ function j() {
|
|
|
91
92
|
direction: "down"
|
|
92
93
|
}
|
|
93
94
|
}),
|
|
94
|
-
/* @__PURE__ */
|
|
95
|
+
/* @__PURE__ */ D(g, {
|
|
95
96
|
label: "Customer Satisfaction",
|
|
96
|
-
value:
|
|
97
|
-
icon: /* @__PURE__ */
|
|
97
|
+
value: M.overview.customerSatisfaction.toFixed(1),
|
|
98
|
+
icon: /* @__PURE__ */ D(r, { size: 20 }),
|
|
98
99
|
accent: "warning",
|
|
99
100
|
subtitle: "out of 5"
|
|
100
101
|
})
|
|
101
102
|
]
|
|
102
103
|
}),
|
|
103
|
-
/* @__PURE__ */
|
|
104
|
+
/* @__PURE__ */ O(u, { children: [/* @__PURE__ */ D(f, { children: /* @__PURE__ */ D(p, { children: "Order & delivery trend" }) }), /* @__PURE__ */ D(d, { children: /* @__PURE__ */ D(y, {
|
|
104
105
|
xKey: "date",
|
|
105
106
|
height: 300,
|
|
106
|
-
data:
|
|
107
|
+
data: M.trends.dailyOrders,
|
|
107
108
|
series: [{
|
|
108
109
|
dataKey: "orders",
|
|
109
110
|
name: "Orders"
|
|
@@ -113,98 +114,99 @@ function j() {
|
|
|
113
114
|
}],
|
|
114
115
|
emptyMessage: "No order trend recorded yet"
|
|
115
116
|
}) })] }),
|
|
116
|
-
/* @__PURE__ */
|
|
117
|
-
laneForecast:
|
|
118
|
-
customerForecast:
|
|
119
|
-
laneVariance:
|
|
120
|
-
customerVariance:
|
|
121
|
-
isLoading:
|
|
122
|
-
error:
|
|
123
|
-
onRetry:
|
|
117
|
+
/* @__PURE__ */ D(w, {
|
|
118
|
+
laneForecast: R,
|
|
119
|
+
customerForecast: z,
|
|
120
|
+
laneVariance: B,
|
|
121
|
+
customerVariance: V,
|
|
122
|
+
isLoading: H,
|
|
123
|
+
error: U,
|
|
124
|
+
onRetry: W
|
|
124
125
|
}),
|
|
125
|
-
/* @__PURE__ */
|
|
126
|
+
/* @__PURE__ */ O("div", {
|
|
126
127
|
className: "grid grid-cols-1 lg:grid-cols-2 gap-6",
|
|
127
|
-
children: [/* @__PURE__ */
|
|
128
|
+
children: [/* @__PURE__ */ O(u, { children: [/* @__PURE__ */ D(f, { children: /* @__PURE__ */ D(p, { children: "Revenue by month" }) }), /* @__PURE__ */ D(d, { children: /* @__PURE__ */ D(b, {
|
|
128
129
|
xKey: "month",
|
|
129
130
|
height: 280,
|
|
130
131
|
showLegend: !1,
|
|
131
|
-
data:
|
|
132
|
+
data: K,
|
|
132
133
|
series: [{
|
|
133
134
|
dataKey: "revenue",
|
|
134
135
|
name: "Revenue"
|
|
135
136
|
}],
|
|
136
137
|
valueFormatter: (e) => S(Number(e)),
|
|
137
138
|
emptyMessage: "No revenue recorded yet"
|
|
138
|
-
}) })] }), /* @__PURE__ */
|
|
139
|
-
data:
|
|
139
|
+
}) })] }), /* @__PURE__ */ O(u, { children: [/* @__PURE__ */ D(f, { children: /* @__PURE__ */ D(p, { children: "Orders by status" }) }), /* @__PURE__ */ D(d, { children: /* @__PURE__ */ D(x, {
|
|
140
|
+
data: q,
|
|
140
141
|
height: 240,
|
|
141
|
-
centerLabel:
|
|
142
|
+
centerLabel: J.toLocaleString(),
|
|
142
143
|
centerSubLabel: "orders",
|
|
143
144
|
emptyMessage: "No orders to break down yet"
|
|
144
145
|
}) })] })]
|
|
145
146
|
}),
|
|
146
|
-
/* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ O("div", {
|
|
147
148
|
className: "grid grid-cols-1 lg:grid-cols-2 gap-6",
|
|
148
|
-
children: [/* @__PURE__ */
|
|
149
|
+
children: [/* @__PURE__ */ O(u, { children: [/* @__PURE__ */ D(f, { children: /* @__PURE__ */ D(p, { children: "Fleet Performance" }) }), /* @__PURE__ */ O(d, {
|
|
149
150
|
className: "space-y-4",
|
|
150
|
-
children: [/* @__PURE__ */
|
|
151
|
+
children: [/* @__PURE__ */ O("div", { children: [/* @__PURE__ */ O("div", {
|
|
151
152
|
className: "flex justify-between text-sm mb-1",
|
|
152
|
-
children: [/* @__PURE__ */
|
|
153
|
-
}), /* @__PURE__ */
|
|
153
|
+
children: [/* @__PURE__ */ D("span", { children: "Vehicle Utilization" }), /* @__PURE__ */ O("span", { children: [L.vehicleUtilization.toFixed(0), "%"] })]
|
|
154
|
+
}), /* @__PURE__ */ D(m, { value: L.vehicleUtilization })] }), L.vehiclesByType.length > 0 && /* @__PURE__ */ O("div", {
|
|
154
155
|
className: "pt-2",
|
|
155
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ D("p", {
|
|
156
157
|
className: "text-sm text-text-secondary mb-2",
|
|
157
158
|
children: "Vehicles by Type"
|
|
158
|
-
}),
|
|
159
|
+
}), L.vehiclesByType.map(({ type: e, count: t }) => /* @__PURE__ */ O("div", {
|
|
159
160
|
className: "flex justify-between text-sm py-1",
|
|
160
|
-
children: [/* @__PURE__ */
|
|
161
|
+
children: [/* @__PURE__ */ D("span", {
|
|
161
162
|
className: "capitalize",
|
|
162
163
|
children: e
|
|
163
|
-
}), /* @__PURE__ */
|
|
164
|
+
}), /* @__PURE__ */ D("span", {
|
|
164
165
|
className: "font-medium",
|
|
165
166
|
children: t
|
|
166
167
|
})]
|
|
167
168
|
}, e))]
|
|
168
169
|
})]
|
|
169
|
-
})] }), /* @__PURE__ */
|
|
170
|
+
})] }), /* @__PURE__ */ O(k, {
|
|
170
171
|
className: "space-y-4",
|
|
171
172
|
children: [
|
|
172
|
-
/* @__PURE__ */
|
|
173
|
+
/* @__PURE__ */ D("h2", {
|
|
173
174
|
className: "text-base font-semibold text-text-primary",
|
|
174
175
|
children: "Performance Metrics"
|
|
175
176
|
}),
|
|
176
|
-
/* @__PURE__ */
|
|
177
|
+
/* @__PURE__ */ O("div", { children: [/* @__PURE__ */ D("p", {
|
|
177
178
|
className: "text-sm text-text-secondary",
|
|
178
179
|
children: "On-Time Delivery"
|
|
179
|
-
}), /* @__PURE__ */
|
|
180
|
+
}), /* @__PURE__ */ O("p", {
|
|
180
181
|
className: "text-2xl font-bold",
|
|
181
|
-
children: [
|
|
182
|
+
children: [M.performance.onTimeDelivery.toFixed(1), "%"]
|
|
182
183
|
})] }),
|
|
183
|
-
/* @__PURE__ */
|
|
184
|
+
/* @__PURE__ */ O("div", { children: [/* @__PURE__ */ D("p", {
|
|
184
185
|
className: "text-sm text-text-secondary",
|
|
185
186
|
children: "Customer Satisfaction"
|
|
186
|
-
}), /* @__PURE__ */
|
|
187
|
+
}), /* @__PURE__ */ O("p", {
|
|
187
188
|
className: "text-2xl font-bold",
|
|
188
|
-
children: [
|
|
189
|
+
children: [M.overview.customerSatisfaction.toFixed(1), "/5"]
|
|
189
190
|
})] }),
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
+
/* @__PURE__ */ O("div", {
|
|
191
192
|
className: "flex items-center gap-2",
|
|
192
|
-
children: [/* @__PURE__ */
|
|
193
|
+
children: [/* @__PURE__ */ D(a, {
|
|
193
194
|
size: 16,
|
|
194
195
|
className: "text-text-secondary"
|
|
195
|
-
}), /* @__PURE__ */
|
|
196
|
+
}), /* @__PURE__ */ O("div", { children: [/* @__PURE__ */ D("p", {
|
|
196
197
|
className: "text-sm text-text-secondary",
|
|
197
198
|
children: "Driver Ratings"
|
|
198
|
-
}), /* @__PURE__ */
|
|
199
|
+
}), /* @__PURE__ */ O("p", {
|
|
199
200
|
className: "text-2xl font-bold",
|
|
200
|
-
children: [
|
|
201
|
+
children: [M.performance.driverRatings.toFixed(1), "/5"]
|
|
201
202
|
})] })]
|
|
202
203
|
})
|
|
203
204
|
]
|
|
204
205
|
})]
|
|
205
|
-
})
|
|
206
|
+
}),
|
|
207
|
+
/* @__PURE__ */ D(C, {})
|
|
206
208
|
]
|
|
207
209
|
});
|
|
208
210
|
}
|
|
209
211
|
//#endregion
|
|
210
|
-
export {
|
|
212
|
+
export { M as default };
|
package/package.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carbon footprint panel (BOFF-4391).
|
|
3
|
+
*
|
|
4
|
+
* Surfaces workspace-level CO2e totals and per-vehicle intensity. Data comes
|
|
5
|
+
* from Query.emissionsSummary via useEmissionsSummary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Leaf, Truck } from 'lucide-react';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from './ui';
|
|
10
|
+
import { MetricCard, MetricGrid } from './MetricCard';
|
|
11
|
+
import { useEmissionsSummary } from '../hooks/useAnalytics';
|
|
12
|
+
|
|
13
|
+
function formatKg(value: number): string {
|
|
14
|
+
if (value >= 1000) {
|
|
15
|
+
return `${(value / 1000).toFixed(2)} t`;
|
|
16
|
+
}
|
|
17
|
+
return `${value.toFixed(1)} kg`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatKm(value: number): string {
|
|
21
|
+
if (value >= 1000) {
|
|
22
|
+
return `${(value / 1000).toFixed(1)}k km`;
|
|
23
|
+
}
|
|
24
|
+
return `${value.toFixed(1)} km`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CarbonFootprintPanel() {
|
|
28
|
+
const { summary, isLoading, error } = useEmissionsSummary();
|
|
29
|
+
|
|
30
|
+
if (error) {
|
|
31
|
+
return (
|
|
32
|
+
<Card className="border-status-error-border">
|
|
33
|
+
<CardContent className="p-6 text-center">
|
|
34
|
+
<p className="text-sm text-status-error-text">Couldn't load emissions data</p>
|
|
35
|
+
</CardContent>
|
|
36
|
+
</Card>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isLoading) {
|
|
41
|
+
return (
|
|
42
|
+
<Card>
|
|
43
|
+
<CardHeader>
|
|
44
|
+
<CardTitle className="flex items-center gap-2">
|
|
45
|
+
<Leaf size={18} className="text-status-success-text" />
|
|
46
|
+
Carbon footprint
|
|
47
|
+
</CardTitle>
|
|
48
|
+
</CardHeader>
|
|
49
|
+
<CardContent>
|
|
50
|
+
<MetricGrid columns={3}>
|
|
51
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
52
|
+
<div key={i} className="h-24 rounded-xl bg-bg-sunken motion-safe:animate-pulse" />
|
|
53
|
+
))}
|
|
54
|
+
</MetricGrid>
|
|
55
|
+
</CardContent>
|
|
56
|
+
</Card>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hasData = summary.totalDistanceKm > 0;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle className="flex items-center gap-2">
|
|
66
|
+
<Leaf size={18} className="text-status-success-text" />
|
|
67
|
+
Carbon footprint
|
|
68
|
+
</CardTitle>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent className="space-y-6">
|
|
71
|
+
<MetricGrid columns={3}>
|
|
72
|
+
<MetricCard
|
|
73
|
+
label="Total CO2e"
|
|
74
|
+
value={formatKg(summary.totalCo2eKg)}
|
|
75
|
+
icon={<Leaf size={20} />}
|
|
76
|
+
accent="success"
|
|
77
|
+
/>
|
|
78
|
+
<MetricCard
|
|
79
|
+
label="Total distance"
|
|
80
|
+
value={formatKm(summary.totalDistanceKm)}
|
|
81
|
+
icon={<Truck size={20} />}
|
|
82
|
+
accent="blue"
|
|
83
|
+
/>
|
|
84
|
+
<MetricCard
|
|
85
|
+
label="Avg intensity"
|
|
86
|
+
value={`${(summary.averageCo2eKgPerKm * 1000).toFixed(1)} g/km`}
|
|
87
|
+
icon={<Leaf size={20} />}
|
|
88
|
+
accent="warning"
|
|
89
|
+
/>
|
|
90
|
+
</MetricGrid>
|
|
91
|
+
|
|
92
|
+
{hasData && summary.byVehicle.length > 0 && (
|
|
93
|
+
<div className="space-y-3">
|
|
94
|
+
<h4 className="text-sm font-medium text-text-secondary">By vehicle</h4>
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
{summary.byVehicle
|
|
97
|
+
.filter((v) => v.totalDistanceKm > 0 || v.co2eKg > 0)
|
|
98
|
+
.slice(0, 5)
|
|
99
|
+
.map((vehicle) => (
|
|
100
|
+
<div
|
|
101
|
+
key={vehicle.vehicleId ?? 'unassigned'}
|
|
102
|
+
className="flex items-center justify-between rounded-lg bg-bg-sunken px-3 py-2"
|
|
103
|
+
>
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Truck size={16} className="text-text-secondary" />
|
|
106
|
+
<span className="text-sm font-medium text-text-primary">
|
|
107
|
+
{vehicle.vehicleNumber ?? 'Unassigned'}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="text-right">
|
|
111
|
+
<p className="text-sm font-medium text-text-primary">
|
|
112
|
+
{formatKg(vehicle.co2eKg)}
|
|
113
|
+
</p>
|
|
114
|
+
<p className="text-xs text-text-secondary">
|
|
115
|
+
{formatKm(vehicle.totalDistanceKm)}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{!hasData && (
|
|
125
|
+
<p className="text-sm text-text-secondary">
|
|
126
|
+
No route distance recorded yet. Assign vehicles and distances to routes to see emissions
|
|
127
|
+
estimates.
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
</CardContent>
|
|
131
|
+
</Card>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -10,10 +10,7 @@ import { Calendar, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
|
|
|
10
10
|
|
|
11
11
|
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from './ui';
|
|
12
12
|
|
|
13
|
-
import type {
|
|
14
|
-
ForecastPoint,
|
|
15
|
-
ForecastVariance,
|
|
16
|
-
} from '../hooks/useDemandForecast';
|
|
13
|
+
import type { ForecastPoint, ForecastVariance } from '../hooks/useDemandForecast';
|
|
17
14
|
|
|
18
15
|
interface UpcomingDemandPanelProps {
|
|
19
16
|
laneForecast: ForecastPoint[];
|
|
@@ -42,7 +39,9 @@ const topByRevenue = (rows: ForecastPoint[], weekStart: string): ForecastPoint[]
|
|
|
42
39
|
};
|
|
43
40
|
|
|
44
41
|
const totalVolume = (rows: ForecastPoint[], weekStart: string): number =>
|
|
45
|
-
rows
|
|
42
|
+
rows
|
|
43
|
+
.filter((row) => row.periodStart === weekStart)
|
|
44
|
+
.reduce((sum, row) => sum + row.predictedVolume, 0);
|
|
46
45
|
|
|
47
46
|
const formatConfidence = (confidence: number): string => {
|
|
48
47
|
if (confidence <= 0) return 'Not enough history';
|
|
@@ -177,13 +176,18 @@ function ForecastList({ title, rows }: ForecastListProps) {
|
|
|
177
176
|
<p className="text-sm font-medium mb-3">{title}</p>
|
|
178
177
|
<ul className="space-y-2">
|
|
179
178
|
{rows.map((row) => (
|
|
180
|
-
<li
|
|
179
|
+
<li
|
|
180
|
+
key={`${row.scopeType}-${row.scopeKey}`}
|
|
181
|
+
className="flex items-center justify-between text-sm"
|
|
182
|
+
>
|
|
181
183
|
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
182
184
|
<span className="ml-2 shrink-0 text-text-secondary">
|
|
183
185
|
{row.predictedVolume} jobs ·{' '}
|
|
184
|
-
{new Intl.NumberFormat(undefined, {
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
{new Intl.NumberFormat(undefined, {
|
|
187
|
+
style: 'currency',
|
|
188
|
+
currency: 'USD',
|
|
189
|
+
maximumFractionDigits: 0,
|
|
190
|
+
}).format(row.predictedRevenue)}
|
|
187
191
|
</span>
|
|
188
192
|
</li>
|
|
189
193
|
))}
|
|
@@ -207,9 +211,14 @@ function VarianceList({ rows }: VarianceListProps) {
|
|
|
207
211
|
{rows.map((row) => {
|
|
208
212
|
const isOvershoot = row.volumeVariance > 0;
|
|
209
213
|
const Icon = isOvershoot ? TrendingUp : TrendingDown;
|
|
210
|
-
const tone = isOvershoot
|
|
214
|
+
const tone = isOvershoot
|
|
215
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
216
|
+
: 'text-rose-600 dark:text-rose-400';
|
|
211
217
|
return (
|
|
212
|
-
<li
|
|
218
|
+
<li
|
|
219
|
+
key={`${row.scopeType}-${row.scopeKey}-${row.periodStart}`}
|
|
220
|
+
className="flex items-center justify-between text-sm"
|
|
221
|
+
>
|
|
213
222
|
<span className="flex items-center gap-2 truncate">
|
|
214
223
|
<Icon size={14} className={tone} aria-hidden="true" />
|
|
215
224
|
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
@@ -493,6 +493,22 @@ const FINANCE_METRICS_QUERY = `
|
|
|
493
493
|
}
|
|
494
494
|
`;
|
|
495
495
|
|
|
496
|
+
const EMISSIONS_SUMMARY_QUERY = `
|
|
497
|
+
query GetMovethewheelsEmissionsSummary {
|
|
498
|
+
emissionsSummary {
|
|
499
|
+
totalCo2eKg
|
|
500
|
+
totalDistanceKm
|
|
501
|
+
averageCo2eKgPerKm
|
|
502
|
+
byVehicle {
|
|
503
|
+
vehicleId
|
|
504
|
+
vehicleNumber
|
|
505
|
+
totalDistanceKm
|
|
506
|
+
co2eKg
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
`;
|
|
511
|
+
|
|
496
512
|
export interface FinanceMetrics {
|
|
497
513
|
totalRevenue: number;
|
|
498
514
|
deliveredRevenue: number;
|
|
@@ -593,3 +609,73 @@ export function usePerformanceAnalytics() {
|
|
|
593
609
|
|
|
594
610
|
return { performanceData, isLoading, error };
|
|
595
611
|
}
|
|
612
|
+
|
|
613
|
+
export interface EmissionsSummary {
|
|
614
|
+
totalCo2eKg: number;
|
|
615
|
+
totalDistanceKm: number;
|
|
616
|
+
averageCo2eKgPerKm: number;
|
|
617
|
+
byVehicle: Array<{
|
|
618
|
+
vehicleId: string | null;
|
|
619
|
+
vehicleNumber: string | null;
|
|
620
|
+
totalDistanceKm: number;
|
|
621
|
+
co2eKg: number;
|
|
622
|
+
}>;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Real emissions figures straight from Query.emissionsSummary (BOFF-4391).
|
|
627
|
+
* Returns totals, per-km intensity, and a per-vehicle breakdown sourced from
|
|
628
|
+
* cached route CO2e values and actual/estimated distances.
|
|
629
|
+
*/
|
|
630
|
+
export function useEmissionsSummary() {
|
|
631
|
+
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
632
|
+
useMoveTheWheels();
|
|
633
|
+
const [summary, setSummary] = useState<EmissionsSummary>({
|
|
634
|
+
totalCo2eKg: 0,
|
|
635
|
+
totalDistanceKm: 0,
|
|
636
|
+
averageCo2eKgPerKm: 0,
|
|
637
|
+
byVehicle: [],
|
|
638
|
+
});
|
|
639
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
640
|
+
const [error, setError] = useState<Error | null>(null);
|
|
641
|
+
|
|
642
|
+
const graphQLOptions = useMemo(
|
|
643
|
+
() => ({
|
|
644
|
+
apiGatewayUrl,
|
|
645
|
+
authToken,
|
|
646
|
+
workspaceToken,
|
|
647
|
+
workspaceId,
|
|
648
|
+
tenantId,
|
|
649
|
+
actorId: currentUser?.id,
|
|
650
|
+
}),
|
|
651
|
+
[apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const load = useCallback(async () => {
|
|
655
|
+
setIsLoading(true);
|
|
656
|
+
setError(null);
|
|
657
|
+
try {
|
|
658
|
+
const data = await executeGraphQL<{
|
|
659
|
+
emissionsSummary: EmissionsSummary;
|
|
660
|
+
}>(graphQLOptions, EMISSIONS_SUMMARY_QUERY);
|
|
661
|
+
|
|
662
|
+
setSummary(data.emissionsSummary);
|
|
663
|
+
} catch (fetchError) {
|
|
664
|
+
setError(
|
|
665
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load emissions summary')
|
|
666
|
+
);
|
|
667
|
+
} finally {
|
|
668
|
+
setIsLoading(false);
|
|
669
|
+
}
|
|
670
|
+
}, [graphQLOptions]);
|
|
671
|
+
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
void load();
|
|
674
|
+
}, [load]);
|
|
675
|
+
|
|
676
|
+
const refetch = useCallback(() => {
|
|
677
|
+
void load();
|
|
678
|
+
}, [load]);
|
|
679
|
+
|
|
680
|
+
return { summary, isLoading, error, refetch };
|
|
681
|
+
}
|
|
@@ -134,9 +134,7 @@ export function useDemandForecast(weeks = 4, varianceLimit = 8): UseDemandForeca
|
|
|
134
134
|
setCustomerVariance(data.customerVariance ?? []);
|
|
135
135
|
} catch (fetchError) {
|
|
136
136
|
setError(
|
|
137
|
-
fetchError instanceof Error
|
|
138
|
-
? fetchError
|
|
139
|
-
: new Error('Failed to load demand forecast')
|
|
137
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load demand forecast')
|
|
140
138
|
);
|
|
141
139
|
} finally {
|
|
142
140
|
setIsLoading(false);
|
|
@@ -151,5 +149,13 @@ export function useDemandForecast(weeks = 4, varianceLimit = 8): UseDemandForeca
|
|
|
151
149
|
void load();
|
|
152
150
|
}, [load]);
|
|
153
151
|
|
|
154
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
laneForecast,
|
|
154
|
+
customerForecast,
|
|
155
|
+
laneVariance,
|
|
156
|
+
customerVariance,
|
|
157
|
+
isLoading,
|
|
158
|
+
error,
|
|
159
|
+
refetch,
|
|
160
|
+
};
|
|
155
161
|
}
|
|
@@ -13,12 +13,9 @@ import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/Metric
|
|
|
13
13
|
import { Card, CardContent, CardHeader, CardTitle, Progress, Button } from '../components/ui';
|
|
14
14
|
import { TrendChart, VolumeBar, StatusDonut, type DonutSlice } from '../components/charts';
|
|
15
15
|
import { UpcomingDemandPanel } from '../components/UpcomingDemandPanel';
|
|
16
|
+
import { CarbonFootprintPanel } from '../components/CarbonFootprintPanel';
|
|
16
17
|
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
17
|
-
import {
|
|
18
|
-
useAnalytics,
|
|
19
|
-
useOrderAnalytics,
|
|
20
|
-
useFleetAnalytics,
|
|
21
|
-
} from '../hooks/useAnalytics';
|
|
18
|
+
import { useAnalytics, useOrderAnalytics, useFleetAnalytics } from '../hooks/useAnalytics';
|
|
22
19
|
import { useDemandForecast } from '../hooks/useDemandForecast';
|
|
23
20
|
import { formatCurrency } from '../utils/formatters';
|
|
24
21
|
|
|
@@ -241,6 +238,9 @@ export default function AnalyticsPage() {
|
|
|
241
238
|
</div>
|
|
242
239
|
</EmphasisPanel>
|
|
243
240
|
</div>
|
|
241
|
+
|
|
242
|
+
{/* Carbon footprint (BOFF-4391) */}
|
|
243
|
+
<CarbonFootprintPanel />
|
|
244
244
|
</PageLayout>
|
|
245
245
|
);
|
|
246
246
|
}
|