@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.
- package/dist/components/CarbonFootprintPanel.js +108 -0
- package/dist/components/UpcomingDemandPanel.js +184 -0
- package/dist/components/index.js +1 -0
- package/dist/generated/wspace-operations.d.ts +147 -0
- package/dist/generated/wspace-types.d.ts +63 -0
- package/dist/hooks/useAnalytics.js +45 -4
- package/dist/hooks/useDemandForecast.js +54 -0
- package/dist/index.js +29 -28
- package/dist/movethewheels/components/CarbonFootprintPanel.d.ts +7 -0
- package/dist/movethewheels/components/UpcomingDemandPanel.d.ts +13 -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/movethewheels/hooks/useDemandForecast.d.ts +39 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/leaf.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/trending-down.js +11 -0
- package/dist/pages/AnalyticsPage.js +82 -69
- package/package.json +2 -2
- package/src/generated/wspace-operations.ts +177 -0
- package/src/generated/wspace-types.ts +74 -0
- package/src/movethewheels/components/CarbonFootprintPanel.tsx +133 -0
- package/src/movethewheels/components/UpcomingDemandPanel.tsx +237 -0
- 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 +161 -0
- package/src/movethewheels/pages/AnalyticsPage.tsx +27 -0
- package/src/operations/wspace/movethewheels/queries/forecast.graphql +47 -0
|
@@ -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
|
+
}
|