@burdenoff/microfe-movethewheels 2026.804.1 → 2026.805.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/UpcomingDemandPanel.js +184 -0
- package/dist/generated/wspace-operations.d.ts +147 -0
- package/dist/generated/wspace-types.d.ts +63 -0
- package/dist/hooks/useDemandForecast.js +54 -0
- package/dist/movethewheels/components/UpcomingDemandPanel.d.ts +13 -0
- package/dist/movethewheels/hooks/useDemandForecast.d.ts +39 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/trending-down.js +11 -0
- package/dist/pages/AnalyticsPage.js +79 -68
- 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/UpcomingDemandPanel.tsx +228 -0
- package/src/movethewheels/hooks/useDemandForecast.ts +155 -0
- package/src/movethewheels/pages/AnalyticsPage.tsx +28 -1
- package/src/operations/wspace/movethewheels/queries/forecast.graphql +47 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpcomingDemandPanel
|
|
3
|
+
*
|
|
4
|
+
* Surfaces next-week demand forecasts by lane / customer alongside the
|
|
5
|
+
* most recent closed-period variance (BOFF-4393). Pure read-side view
|
|
6
|
+
* over `wspace-movethewheels-svc`'s DemandForecast surface.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Calendar, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from './ui';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ForecastPoint,
|
|
15
|
+
ForecastVariance,
|
|
16
|
+
} from '../hooks/useDemandForecast';
|
|
17
|
+
|
|
18
|
+
interface UpcomingDemandPanelProps {
|
|
19
|
+
laneForecast: ForecastPoint[];
|
|
20
|
+
customerForecast: ForecastPoint[];
|
|
21
|
+
laneVariance: ForecastVariance[];
|
|
22
|
+
customerVariance: ForecastVariance[];
|
|
23
|
+
isLoading: boolean;
|
|
24
|
+
error: Error | null;
|
|
25
|
+
onRetry: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const formatWeek = (iso: string): string => {
|
|
29
|
+
const date = new Date(iso);
|
|
30
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
31
|
+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const topByVolume = (rows: ForecastPoint[], weekStart: string): ForecastPoint[] => {
|
|
35
|
+
const filtered = rows.filter((row) => row.periodStart === weekStart);
|
|
36
|
+
return [...filtered].sort((a, b) => b.predictedVolume - a.predictedVolume).slice(0, 3);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const topByRevenue = (rows: ForecastPoint[], weekStart: string): ForecastPoint[] => {
|
|
40
|
+
const filtered = rows.filter((row) => row.periodStart === weekStart);
|
|
41
|
+
return [...filtered].sort((a, b) => b.predictedRevenue - a.predictedRevenue).slice(0, 3);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const totalVolume = (rows: ForecastPoint[], weekStart: string): number =>
|
|
45
|
+
rows.filter((row) => row.periodStart === weekStart).reduce((sum, row) => sum + row.predictedVolume, 0);
|
|
46
|
+
|
|
47
|
+
const formatConfidence = (confidence: number): string => {
|
|
48
|
+
if (confidence <= 0) return 'Not enough history';
|
|
49
|
+
return `${Math.round(confidence * 100)}% confidence`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function UpcomingDemandPanel({
|
|
53
|
+
laneForecast,
|
|
54
|
+
customerForecast,
|
|
55
|
+
laneVariance,
|
|
56
|
+
customerVariance,
|
|
57
|
+
isLoading,
|
|
58
|
+
error,
|
|
59
|
+
onRetry,
|
|
60
|
+
}: UpcomingDemandPanelProps) {
|
|
61
|
+
if (error && !isLoading) {
|
|
62
|
+
return (
|
|
63
|
+
<Card className="border-status-error-border">
|
|
64
|
+
<CardContent className="p-6 text-center space-y-3">
|
|
65
|
+
<p className="text-status-error-text font-medium">Couldn't load demand forecast</p>
|
|
66
|
+
<p className="text-sm text-text-secondary">{error.message}</p>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={onRetry}
|
|
70
|
+
className="text-sm font-medium text-primary hover:underline"
|
|
71
|
+
>
|
|
72
|
+
Retry
|
|
73
|
+
</button>
|
|
74
|
+
</CardContent>
|
|
75
|
+
</Card>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isLoading) {
|
|
80
|
+
return (
|
|
81
|
+
<Card>
|
|
82
|
+
<CardHeader>
|
|
83
|
+
<CardTitle>Upcoming demand</CardTitle>
|
|
84
|
+
</CardHeader>
|
|
85
|
+
<CardContent className="space-y-4">
|
|
86
|
+
<Skeleton className="h-16 w-full" />
|
|
87
|
+
<Skeleton className="h-16 w-full" />
|
|
88
|
+
<Skeleton className="h-16 w-full" />
|
|
89
|
+
</CardContent>
|
|
90
|
+
</Card>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const nextWeekStart = laneForecast[0]?.periodStart ?? customerForecast[0]?.periodStart ?? null;
|
|
95
|
+
|
|
96
|
+
if (!nextWeekStart || laneForecast.length === 0) {
|
|
97
|
+
return (
|
|
98
|
+
<Card>
|
|
99
|
+
<CardHeader>
|
|
100
|
+
<CardTitle className="flex items-center gap-2">
|
|
101
|
+
<Calendar size={18} aria-hidden="true" />
|
|
102
|
+
Upcoming demand
|
|
103
|
+
</CardTitle>
|
|
104
|
+
</CardHeader>
|
|
105
|
+
<CardContent>
|
|
106
|
+
<div className="flex items-start gap-2 rounded-lg border border-border/60 bg-muted/40 p-4 text-sm text-text-secondary">
|
|
107
|
+
<AlertCircle size={16} aria-hidden="true" className="mt-0.5 shrink-0" />
|
|
108
|
+
<p>
|
|
109
|
+
Not enough history to forecast next week yet. Once you have at least 4 weeks of
|
|
110
|
+
completed orders, this panel will surface demand by lane and customer.
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</CardContent>
|
|
114
|
+
</Card>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const topLanes = topByVolume(laneForecast, nextWeekStart);
|
|
119
|
+
const topLanesByRevenue = topByRevenue(laneForecast, nextWeekStart);
|
|
120
|
+
const topCustomers = topByVolume(customerForecast, nextWeekStart);
|
|
121
|
+
const total = totalVolume(laneForecast, nextWeekStart);
|
|
122
|
+
|
|
123
|
+
// Most-recent variance row per scope key — show the lane that overshot /
|
|
124
|
+
// undershot the most in absolute volume terms.
|
|
125
|
+
const mostRecentVariance = [...laneVariance, ...customerVariance]
|
|
126
|
+
.sort((a, b) => Math.abs(b.volumeVariance) - Math.abs(a.volumeVariance))
|
|
127
|
+
.slice(0, 3);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Card>
|
|
131
|
+
<CardHeader>
|
|
132
|
+
<CardTitle className="flex items-center gap-2">
|
|
133
|
+
<Calendar size={18} aria-hidden="true" />
|
|
134
|
+
Upcoming demand — week of {formatWeek(nextWeekStart)}
|
|
135
|
+
</CardTitle>
|
|
136
|
+
</CardHeader>
|
|
137
|
+
<CardContent className="space-y-4">
|
|
138
|
+
<div className="flex items-baseline justify-between rounded-lg border border-border/60 bg-muted/40 px-4 py-3">
|
|
139
|
+
<div>
|
|
140
|
+
<p className="text-xs uppercase tracking-wide text-text-secondary">
|
|
141
|
+
Predicted jobs (all lanes)
|
|
142
|
+
</p>
|
|
143
|
+
<p className="text-2xl font-bold">{total}</p>
|
|
144
|
+
</div>
|
|
145
|
+
<p className="text-xs text-text-secondary">
|
|
146
|
+
{topLanes[0] ? formatConfidence(topLanes[0].confidence) : ''}
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
151
|
+
<ForecastList title="Top lanes (volume)" rows={topLanes} />
|
|
152
|
+
<ForecastList title="Top lanes (revenue)" rows={topLanesByRevenue} />
|
|
153
|
+
<ForecastList title="Top customers (volume)" rows={topCustomers} />
|
|
154
|
+
<VarianceList rows={mostRecentVariance} />
|
|
155
|
+
</div>
|
|
156
|
+
</CardContent>
|
|
157
|
+
</Card>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface ForecastListProps {
|
|
162
|
+
title: string;
|
|
163
|
+
rows: ForecastPoint[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ForecastList({ title, rows }: ForecastListProps) {
|
|
167
|
+
if (rows.length === 0) {
|
|
168
|
+
return (
|
|
169
|
+
<div className="rounded-lg border border-border/60 p-4">
|
|
170
|
+
<p className="text-sm font-medium mb-2">{title}</p>
|
|
171
|
+
<p className="text-xs text-text-secondary">No forecasts yet for this week.</p>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return (
|
|
176
|
+
<div className="rounded-lg border border-border/60 p-4">
|
|
177
|
+
<p className="text-sm font-medium mb-3">{title}</p>
|
|
178
|
+
<ul className="space-y-2">
|
|
179
|
+
{rows.map((row) => (
|
|
180
|
+
<li key={`${row.scopeType}-${row.scopeKey}`} className="flex items-center justify-between text-sm">
|
|
181
|
+
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
182
|
+
<span className="ml-2 shrink-0 text-text-secondary">
|
|
183
|
+
{row.predictedVolume} jobs ·{' '}
|
|
184
|
+
{new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(
|
|
185
|
+
row.predictedRevenue
|
|
186
|
+
)}
|
|
187
|
+
</span>
|
|
188
|
+
</li>
|
|
189
|
+
))}
|
|
190
|
+
</ul>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface VarianceListProps {
|
|
196
|
+
rows: ForecastVariance[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function VarianceList({ rows }: VarianceListProps) {
|
|
200
|
+
return (
|
|
201
|
+
<div className="rounded-lg border border-border/60 p-4">
|
|
202
|
+
<p className="text-sm font-medium mb-3">Recent forecast variance</p>
|
|
203
|
+
{rows.length === 0 ? (
|
|
204
|
+
<p className="text-xs text-text-secondary">No closed periods to compare yet.</p>
|
|
205
|
+
) : (
|
|
206
|
+
<ul className="space-y-2">
|
|
207
|
+
{rows.map((row) => {
|
|
208
|
+
const isOvershoot = row.volumeVariance > 0;
|
|
209
|
+
const Icon = isOvershoot ? TrendingUp : TrendingDown;
|
|
210
|
+
const tone = isOvershoot ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400';
|
|
211
|
+
return (
|
|
212
|
+
<li key={`${row.scopeType}-${row.scopeKey}-${row.periodStart}`} className="flex items-center justify-between text-sm">
|
|
213
|
+
<span className="flex items-center gap-2 truncate">
|
|
214
|
+
<Icon size={14} className={tone} aria-hidden="true" />
|
|
215
|
+
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
216
|
+
</span>
|
|
217
|
+
<span className={`ml-2 shrink-0 ${tone}`}>
|
|
218
|
+
{isOvershoot ? '+' : ''}
|
|
219
|
+
{row.volumeVariance} jobs
|
|
220
|
+
</span>
|
|
221
|
+
</li>
|
|
222
|
+
);
|
|
223
|
+
})}
|
|
224
|
+
</ul>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
|
138
|
+
? fetchError
|
|
139
|
+
: new Error('Failed to load demand forecast')
|
|
140
|
+
);
|
|
141
|
+
} finally {
|
|
142
|
+
setIsLoading(false);
|
|
143
|
+
}
|
|
144
|
+
}, [graphQLOptions, weeks, varianceLimit]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
void load();
|
|
148
|
+
}, [load]);
|
|
149
|
+
|
|
150
|
+
const refetch = useCallback(() => {
|
|
151
|
+
void load();
|
|
152
|
+
}, [load]);
|
|
153
|
+
|
|
154
|
+
return { laneForecast, customerForecast, laneVariance, customerVariance, isLoading, error, refetch };
|
|
155
|
+
}
|
|
@@ -12,8 +12,14 @@ 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';
|
|
15
16
|
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
useAnalytics,
|
|
19
|
+
useOrderAnalytics,
|
|
20
|
+
useFleetAnalytics,
|
|
21
|
+
} from '../hooks/useAnalytics';
|
|
22
|
+
import { useDemandForecast } from '../hooks/useDemandForecast';
|
|
17
23
|
import { formatCurrency } from '../utils/formatters';
|
|
18
24
|
|
|
19
25
|
const statusLabel = (status: string) =>
|
|
@@ -23,6 +29,15 @@ export default function AnalyticsPage() {
|
|
|
23
29
|
const { analytics, isLoading, error, refetch } = useAnalytics();
|
|
24
30
|
const { orderData } = useOrderAnalytics();
|
|
25
31
|
const { fleetData } = useFleetAnalytics();
|
|
32
|
+
const {
|
|
33
|
+
laneForecast,
|
|
34
|
+
customerForecast,
|
|
35
|
+
laneVariance,
|
|
36
|
+
customerVariance,
|
|
37
|
+
isLoading: forecastLoading,
|
|
38
|
+
error: forecastError,
|
|
39
|
+
refetch: refetchForecast,
|
|
40
|
+
} = useDemandForecast(4, 8);
|
|
26
41
|
|
|
27
42
|
const orderSpark = useMemo(
|
|
28
43
|
() => analytics.trends.dailyOrders.map((d) => d.orders),
|
|
@@ -129,6 +144,18 @@ export default function AnalyticsPage() {
|
|
|
129
144
|
</CardContent>
|
|
130
145
|
</Card>
|
|
131
146
|
|
|
147
|
+
{/* Upcoming demand (BOFF-4393) — heuristic forecast, surfaces only
|
|
148
|
+
when the workspace has enough history (>=4 weeks). */}
|
|
149
|
+
<UpcomingDemandPanel
|
|
150
|
+
laneForecast={laneForecast}
|
|
151
|
+
customerForecast={customerForecast}
|
|
152
|
+
laneVariance={laneVariance}
|
|
153
|
+
customerVariance={customerVariance}
|
|
154
|
+
isLoading={forecastLoading}
|
|
155
|
+
error={forecastError}
|
|
156
|
+
onRetry={refetchForecast}
|
|
157
|
+
/>
|
|
158
|
+
|
|
132
159
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
133
160
|
<Card>
|
|
134
161
|
<CardHeader>
|
|
@@ -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
|
+
}
|