@burdenoff/microfe-movethewheels 2026.804.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.
Files changed (28) hide show
  1. package/dist/components/CarbonFootprintPanel.js +108 -0
  2. package/dist/components/UpcomingDemandPanel.js +184 -0
  3. package/dist/components/index.js +1 -0
  4. package/dist/generated/wspace-operations.d.ts +147 -0
  5. package/dist/generated/wspace-types.d.ts +63 -0
  6. package/dist/hooks/useAnalytics.js +45 -4
  7. package/dist/hooks/useDemandForecast.js +54 -0
  8. package/dist/index.js +29 -28
  9. package/dist/movethewheels/components/CarbonFootprintPanel.d.ts +7 -0
  10. package/dist/movethewheels/components/UpcomingDemandPanel.d.ts +13 -0
  11. package/dist/movethewheels/components/index.d.ts +1 -0
  12. package/dist/movethewheels/hooks/index.d.ts +1 -1
  13. package/dist/movethewheels/hooks/useAnalytics.d.ts +22 -0
  14. package/dist/movethewheels/hooks/useDemandForecast.d.ts +39 -0
  15. package/dist/node_modules/lucide-react/dist/esm/icons/leaf.js +11 -0
  16. package/dist/node_modules/lucide-react/dist/esm/icons/trending-down.js +11 -0
  17. package/dist/pages/AnalyticsPage.js +82 -69
  18. package/package.json +2 -2
  19. package/src/generated/wspace-operations.ts +177 -0
  20. package/src/generated/wspace-types.ts +74 -0
  21. package/src/movethewheels/components/CarbonFootprintPanel.tsx +133 -0
  22. package/src/movethewheels/components/UpcomingDemandPanel.tsx +237 -0
  23. package/src/movethewheels/components/index.ts +3 -0
  24. package/src/movethewheels/hooks/index.ts +2 -0
  25. package/src/movethewheels/hooks/useAnalytics.ts +86 -0
  26. package/src/movethewheels/hooks/useDemandForecast.ts +161 -0
  27. package/src/movethewheels/pages/AnalyticsPage.tsx +27 -0
  28. package/src/operations/wspace/movethewheels/queries/forecast.graphql +47 -0
@@ -74,3 +74,6 @@ export { Notice, useNotice } from './Notice';
74
74
 
75
75
  // Documents (reuse of wspace-files-svc)
76
76
  export { DocumentsCard } from './DocumentsCard';
77
+
78
+ // Sustainability / carbon tracking (BOFF-4391)
79
+ export { CarbonFootprintPanel } from './CarbonFootprintPanel';
@@ -43,6 +43,8 @@ export {
43
43
  usePerformanceAnalytics,
44
44
  useFinanceMetrics,
45
45
  type FinanceMetrics,
46
+ useEmissionsSummary,
47
+ type EmissionsSummary,
46
48
  } from './useAnalytics';
47
49
 
48
50
  // Tracking
@@ -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
+ }
@@ -0,0 +1,161 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
4
+ import { executeGraphQL } from '../utils/graphqlClient';
5
+
6
+ /**
7
+ * Demand forecasting hook (BOFF-4393). Pulls next-N-week forecasts by lane
8
+ * and customer plus the recent closed-period variance. Read-side only — the
9
+ * admin console exposes `recalculateDemandForecasts` separately.
10
+ */
11
+
12
+ export interface ForecastPoint {
13
+ scopeType: 'LANE' | 'CUSTOMER';
14
+ scopeKey: string;
15
+ periodStart: string;
16
+ periodEnd: string;
17
+ predictedVolume: number;
18
+ predictedRevenue: number;
19
+ actualVolume: number | null;
20
+ actualRevenue: number | null;
21
+ confidence: number;
22
+ }
23
+
24
+ export interface ForecastVariance {
25
+ scopeType: 'LANE' | 'CUSTOMER';
26
+ scopeKey: string;
27
+ periodStart: string;
28
+ periodEnd: string;
29
+ predictedVolume: number;
30
+ actualVolume: number;
31
+ volumeVariance: number;
32
+ predictedRevenue: number;
33
+ actualRevenue: number;
34
+ revenueVariance: number;
35
+ }
36
+
37
+ interface UseDemandForecastResult {
38
+ laneForecast: ForecastPoint[];
39
+ customerForecast: ForecastPoint[];
40
+ laneVariance: ForecastVariance[];
41
+ customerVariance: ForecastVariance[];
42
+ isLoading: boolean;
43
+ error: Error | null;
44
+ refetch: () => void;
45
+ }
46
+
47
+ const FORECAST_QUERY = `
48
+ query GetMovethewheelsDemandForecast($weeks: Int!, $varianceLimit: Int!) {
49
+ demandForecastByLane(weeks: $weeks) {
50
+ scopeType
51
+ scopeKey
52
+ periodStart
53
+ periodEnd
54
+ predictedVolume
55
+ predictedRevenue
56
+ actualVolume
57
+ actualRevenue
58
+ confidence
59
+ }
60
+ demandForecastByCustomer(weeks: $weeks) {
61
+ scopeType
62
+ scopeKey
63
+ periodStart
64
+ periodEnd
65
+ predictedVolume
66
+ predictedRevenue
67
+ actualVolume
68
+ actualRevenue
69
+ confidence
70
+ }
71
+ laneVariance: demandForecastVariance(scopeType: LANE, limit: $varianceLimit) {
72
+ scopeType
73
+ scopeKey
74
+ periodStart
75
+ periodEnd
76
+ predictedVolume
77
+ actualVolume
78
+ volumeVariance
79
+ predictedRevenue
80
+ actualRevenue
81
+ revenueVariance
82
+ }
83
+ customerVariance: demandForecastVariance(scopeType: CUSTOMER, limit: $varianceLimit) {
84
+ scopeType
85
+ scopeKey
86
+ periodStart
87
+ periodEnd
88
+ predictedVolume
89
+ actualVolume
90
+ volumeVariance
91
+ predictedRevenue
92
+ actualRevenue
93
+ revenueVariance
94
+ }
95
+ }
96
+ `;
97
+
98
+ export function useDemandForecast(weeks = 4, varianceLimit = 8): UseDemandForecastResult {
99
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
100
+ useMoveTheWheels();
101
+ const [laneForecast, setLaneForecast] = useState<ForecastPoint[]>([]);
102
+ const [customerForecast, setCustomerForecast] = useState<ForecastPoint[]>([]);
103
+ const [laneVariance, setLaneVariance] = useState<ForecastVariance[]>([]);
104
+ const [customerVariance, setCustomerVariance] = useState<ForecastVariance[]>([]);
105
+ const [isLoading, setIsLoading] = useState(false);
106
+ const [error, setError] = useState<Error | null>(null);
107
+
108
+ const graphQLOptions = useMemo(
109
+ () => ({
110
+ apiGatewayUrl,
111
+ authToken,
112
+ workspaceToken,
113
+ workspaceId,
114
+ tenantId,
115
+ actorId: currentUser?.id,
116
+ }),
117
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
118
+ );
119
+
120
+ const load = useCallback(async () => {
121
+ setIsLoading(true);
122
+ setError(null);
123
+ try {
124
+ const data = await executeGraphQL<{
125
+ demandForecastByLane: ForecastPoint[];
126
+ demandForecastByCustomer: ForecastPoint[];
127
+ laneVariance: ForecastVariance[];
128
+ customerVariance: ForecastVariance[];
129
+ }>(graphQLOptions, FORECAST_QUERY, { weeks, varianceLimit });
130
+
131
+ setLaneForecast(data.demandForecastByLane ?? []);
132
+ setCustomerForecast(data.demandForecastByCustomer ?? []);
133
+ setLaneVariance(data.laneVariance ?? []);
134
+ setCustomerVariance(data.customerVariance ?? []);
135
+ } catch (fetchError) {
136
+ setError(
137
+ fetchError instanceof Error ? fetchError : new Error('Failed to load demand forecast')
138
+ );
139
+ } finally {
140
+ setIsLoading(false);
141
+ }
142
+ }, [graphQLOptions, weeks, varianceLimit]);
143
+
144
+ useEffect(() => {
145
+ void load();
146
+ }, [load]);
147
+
148
+ const refetch = useCallback(() => {
149
+ void load();
150
+ }, [load]);
151
+
152
+ return {
153
+ laneForecast,
154
+ customerForecast,
155
+ laneVariance,
156
+ customerVariance,
157
+ isLoading,
158
+ error,
159
+ refetch,
160
+ };
161
+ }
@@ -12,8 +12,11 @@ import { PageLayout } from '../components/PageLayout';
12
12
  import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
13
13
  import { Card, CardContent, CardHeader, CardTitle, Progress, Button } from '../components/ui';
14
14
  import { TrendChart, VolumeBar, StatusDonut, type DonutSlice } from '../components/charts';
15
+ import { UpcomingDemandPanel } from '../components/UpcomingDemandPanel';
16
+ import { CarbonFootprintPanel } from '../components/CarbonFootprintPanel';
15
17
  import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
16
18
  import { useAnalytics, useOrderAnalytics, useFleetAnalytics } from '../hooks/useAnalytics';
19
+ import { useDemandForecast } from '../hooks/useDemandForecast';
17
20
  import { formatCurrency } from '../utils/formatters';
18
21
 
19
22
  const statusLabel = (status: string) =>
@@ -23,6 +26,15 @@ export default function AnalyticsPage() {
23
26
  const { analytics, isLoading, error, refetch } = useAnalytics();
24
27
  const { orderData } = useOrderAnalytics();
25
28
  const { fleetData } = useFleetAnalytics();
29
+ const {
30
+ laneForecast,
31
+ customerForecast,
32
+ laneVariance,
33
+ customerVariance,
34
+ isLoading: forecastLoading,
35
+ error: forecastError,
36
+ refetch: refetchForecast,
37
+ } = useDemandForecast(4, 8);
26
38
 
27
39
  const orderSpark = useMemo(
28
40
  () => analytics.trends.dailyOrders.map((d) => d.orders),
@@ -129,6 +141,18 @@ export default function AnalyticsPage() {
129
141
  </CardContent>
130
142
  </Card>
131
143
 
144
+ {/* Upcoming demand (BOFF-4393) — heuristic forecast, surfaces only
145
+ when the workspace has enough history (>=4 weeks). */}
146
+ <UpcomingDemandPanel
147
+ laneForecast={laneForecast}
148
+ customerForecast={customerForecast}
149
+ laneVariance={laneVariance}
150
+ customerVariance={customerVariance}
151
+ isLoading={forecastLoading}
152
+ error={forecastError}
153
+ onRetry={refetchForecast}
154
+ />
155
+
132
156
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
133
157
  <Card>
134
158
  <CardHeader>
@@ -214,6 +238,9 @@ export default function AnalyticsPage() {
214
238
  </div>
215
239
  </EmphasisPanel>
216
240
  </div>
241
+
242
+ {/* Carbon footprint (BOFF-4391) */}
243
+ <CarbonFootprintPanel />
217
244
  </PageLayout>
218
245
  );
219
246
  }
@@ -0,0 +1,47 @@
1
+ # Demand forecasting (BOFF-4393) — query operations against the
2
+ # wspace-movethewheels-svc DemandForecast surface. Read-side only on the
3
+ # UI; recalculation is triggered from the admin console via a separate
4
+ # mutation that is not wired into the MFE yet.
5
+
6
+ query GetMovethewheelsDemandForecastByLane($weeks: Int!) {
7
+ demandForecastByLane(weeks: $weeks) {
8
+ scopeType
9
+ scopeKey
10
+ periodStart
11
+ periodEnd
12
+ predictedVolume
13
+ predictedRevenue
14
+ actualVolume
15
+ actualRevenue
16
+ confidence
17
+ }
18
+ }
19
+
20
+ query GetMovethewheelsDemandForecastByCustomer($weeks: Int!) {
21
+ demandForecastByCustomer(weeks: $weeks) {
22
+ scopeType
23
+ scopeKey
24
+ periodStart
25
+ periodEnd
26
+ predictedVolume
27
+ predictedRevenue
28
+ actualVolume
29
+ actualRevenue
30
+ confidence
31
+ }
32
+ }
33
+
34
+ query GetMovethewheelsDemandForecastVariance($scopeType: DemandForecastScope!, $limit: Int!) {
35
+ demandForecastVariance(scopeType: $scopeType, limit: $limit) {
36
+ scopeType
37
+ scopeKey
38
+ periodStart
39
+ periodEnd
40
+ predictedVolume
41
+ actualVolume
42
+ volumeVariance
43
+ predictedRevenue
44
+ actualRevenue
45
+ revenueVariance
46
+ }
47
+ }