@burdenoff/microfe-movethewheels 2026.1017.12 → 2026.1017.14
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/MoveTheWheelsRoot.js +7 -0
- package/dist/MoveTheWheelsRoutes.js +39 -35
- package/dist/components/pallet-preview/PalletPreviewResults.js +189 -0
- package/dist/generated/pallet-preview-operations.d.ts +273 -0
- package/dist/hooks/usePalletPreview.js +86 -0
- package/dist/movethewheels/components/pallet-preview/PalletPreviewResults.d.ts +6 -0
- package/dist/movethewheels/hooks/usePalletPreview.d.ts +98 -0
- package/dist/movethewheels/pages/PalletPreviewPage.d.ts +1 -0
- package/dist/movethewheels/utils/graphqlClient.d.ts +4 -0
- package/dist/movethewheels/utils/palletPreviewInput.d.ts +54 -0
- package/dist/movethewheels/utils/palletPreviewResponse.d.ts +7 -0
- package/dist/pages/PalletPreviewPage.js +501 -0
- package/dist/pages/WarehousesPage.js +83 -78
- package/dist/src/generated/pallet-preview-operations.js +594 -0
- package/dist/utils/graphqlClient.js +27 -19
- package/dist/utils/palletPreviewInput.js +138 -0
- package/dist/utils/palletPreviewResponse.js +191 -0
- package/package.json +5 -4
- package/src/generated/pallet-preview-operations.ts +204 -0
- package/src/movethewheels/MoveTheWheelsRoot.tsx +7 -0
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
- package/src/movethewheels/components/pallet-preview/PalletPreviewResults.tsx +220 -0
- package/src/movethewheels/hooks/usePalletPreview.ts +114 -0
- package/src/movethewheels/pages/PalletPreviewPage.tsx +588 -0
- package/src/movethewheels/pages/WarehousesPage.tsx +9 -1
- package/src/movethewheels/utils/graphqlClient.ts +19 -2
- package/src/movethewheels/utils/palletPreviewInput.ts +199 -0
- package/src/movethewheels/utils/palletPreviewResponse.ts +256 -0
- package/src/operations/pallet-preview/preview.graphql +26 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { MtwPalletGeometryPreviewInput } from '../../../generated/pallet-preview-operations';
|
|
2
|
+
import { FormSection } from '@burdenoff/fe-libs/form';
|
|
3
|
+
import { Badge } from '../ui';
|
|
4
|
+
import { useTr } from '../../hooks';
|
|
5
|
+
import {
|
|
6
|
+
PALLET_BOUND_KEYS,
|
|
7
|
+
PALLET_UNKNOWN_KEYS,
|
|
8
|
+
type PalletPreview,
|
|
9
|
+
} from '../../utils/palletPreviewResponse';
|
|
10
|
+
|
|
11
|
+
const boundLabels = {
|
|
12
|
+
placedContainment: 'Placed subset containment',
|
|
13
|
+
placedNonOverlap: 'Placed subset non-overlap',
|
|
14
|
+
permittedUprightYaw: 'Permitted upright yaw',
|
|
15
|
+
reportedSingleLayerHeight: 'Reported single-layer height',
|
|
16
|
+
palletPayloadMass: 'Pallet payload mass',
|
|
17
|
+
palletFootprintWithinLoadEnvelope: 'Pallet footprint within load envelope',
|
|
18
|
+
loadedPalletGrossMass: 'Loaded pallet gross mass',
|
|
19
|
+
};
|
|
20
|
+
const unknownLabels = {
|
|
21
|
+
additionalPackingGeometry: 'Additional packing geometry',
|
|
22
|
+
handling: 'Handling',
|
|
23
|
+
securing: 'Securing',
|
|
24
|
+
centreOfGravityAndStability: 'Centre of gravity and stability',
|
|
25
|
+
supportAndFloorContactLoads: 'Support and floor contact loads',
|
|
26
|
+
loadingAndUnloadingPath: 'Loading and unloading path',
|
|
27
|
+
segregationAndCargoCompatibility: 'Cargo segregation and compatibility',
|
|
28
|
+
};
|
|
29
|
+
export function PalletPreviewResults({
|
|
30
|
+
result,
|
|
31
|
+
input,
|
|
32
|
+
}: {
|
|
33
|
+
result: PalletPreview;
|
|
34
|
+
input: MtwPalletGeometryPreviewInput;
|
|
35
|
+
}) {
|
|
36
|
+
const tr = useTr();
|
|
37
|
+
const unknown = tr('movethewheels.palletPreview.unknown', 'Unknown');
|
|
38
|
+
const status =
|
|
39
|
+
result.layoutStatus === 'PLACED_BY_METHOD'
|
|
40
|
+
? tr('movethewheels.palletPreview.placed', 'Placed by this method')
|
|
41
|
+
: result.layoutStatus === 'NO_LAYOUT_FOUND_BY_METHOD'
|
|
42
|
+
? tr('movethewheels.palletPreview.noLayout', 'No layout found by this method')
|
|
43
|
+
: tr('movethewheels.palletPreview.unknownFacts', 'Unknown from supplied facts');
|
|
44
|
+
const sourceText = (source: { reference: string; version: string } | null) =>
|
|
45
|
+
source ? `${source.reference} · ${source.version}` : unknown;
|
|
46
|
+
return (
|
|
47
|
+
<section
|
|
48
|
+
aria-label={tr('movethewheels.palletPreview.result', 'Preview result')}
|
|
49
|
+
className="space-y-5 min-w-0"
|
|
50
|
+
>
|
|
51
|
+
<h2 className="text-xl font-semibold" tabIndex={-1}>
|
|
52
|
+
{status}
|
|
53
|
+
</h2>
|
|
54
|
+
<p className="text-sm text-text-secondary">
|
|
55
|
+
{tr(
|
|
56
|
+
'movethewheels.palletPreview.method',
|
|
57
|
+
'Geometric proposal only · Deterministic single-layer shelf method · mm / g'
|
|
58
|
+
)}
|
|
59
|
+
</p>
|
|
60
|
+
<dl className="grid gap-3 sm:grid-cols-2">
|
|
61
|
+
<div>
|
|
62
|
+
<dt>{tr('movethewheels.palletPreview.counts', 'Placed / requested units')}</dt>
|
|
63
|
+
<dd className="font-semibold">
|
|
64
|
+
{result.placedQuantity} / {result.requestedQuantity}
|
|
65
|
+
</dd>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<dt>{tr('movethewheels.palletPreview.payload', 'Whole-request payload mass')}</dt>
|
|
69
|
+
<dd className="font-semibold">
|
|
70
|
+
{result.requestedPayloadMassG === null ? unknown : `${result.requestedPayloadMassG} g`}
|
|
71
|
+
</dd>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<dt>{tr('movethewheels.palletPreview.gross', 'Whole-request gross mass')}</dt>
|
|
75
|
+
<dd className="font-semibold">
|
|
76
|
+
{result.requestedGrossMassG === null ? unknown : `${result.requestedGrossMassG} g`}
|
|
77
|
+
</dd>
|
|
78
|
+
</div>
|
|
79
|
+
</dl>
|
|
80
|
+
<p className="text-sm">
|
|
81
|
+
{tr(
|
|
82
|
+
'movethewheels.palletPreview.allUnitsMass',
|
|
83
|
+
'Requested mass covers all requested units, including unplaced units.'
|
|
84
|
+
)}
|
|
85
|
+
</p>
|
|
86
|
+
{result.unplaced.length > 0 && (
|
|
87
|
+
<div>
|
|
88
|
+
<h3 className="font-medium">
|
|
89
|
+
{tr('movethewheels.palletPreview.unplaced', 'Unplaced units')}
|
|
90
|
+
</h3>
|
|
91
|
+
<ul className="list-disc pl-5">
|
|
92
|
+
{result.unplaced.map((p) => (
|
|
93
|
+
<li key={p.packageId} className="break-words">
|
|
94
|
+
{p.packageId}: {p.quantity}
|
|
95
|
+
</li>
|
|
96
|
+
))}
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
<FormSection
|
|
101
|
+
title={tr('movethewheels.palletPreview.diagram', 'Placed subset — top view (mm)')}
|
|
102
|
+
>
|
|
103
|
+
<p className="text-sm mb-3">
|
|
104
|
+
{tr(
|
|
105
|
+
'movethewheels.palletPreview.subset',
|
|
106
|
+
'Containment and non-overlap concern only the placed subset. This diagram is not a handling or loading-path simulation.'
|
|
107
|
+
)}
|
|
108
|
+
</p>
|
|
109
|
+
{input.pallet.widthMm != null &&
|
|
110
|
+
input.pallet.lengthMm != null &&
|
|
111
|
+
result.placements.length > 0 ? (
|
|
112
|
+
<figure className="min-w-0">
|
|
113
|
+
<svg
|
|
114
|
+
role="img"
|
|
115
|
+
aria-label={tr(
|
|
116
|
+
'movethewheels.palletPreview.diagramLabel',
|
|
117
|
+
'Proposed placed rectangles on the supplied pallet footprint'
|
|
118
|
+
)}
|
|
119
|
+
viewBox={`0 0 ${input.pallet.widthMm} ${input.pallet.lengthMm}`}
|
|
120
|
+
width="100%"
|
|
121
|
+
height="320"
|
|
122
|
+
style={{ height: 320, maxHeight: 320 }}
|
|
123
|
+
className="max-w-full text-text-primary border border-border rounded-lg"
|
|
124
|
+
preserveAspectRatio="xMidYMid meet"
|
|
125
|
+
>
|
|
126
|
+
<rect
|
|
127
|
+
width={input.pallet.widthMm}
|
|
128
|
+
height={input.pallet.lengthMm}
|
|
129
|
+
className="fill-transparent stroke-current"
|
|
130
|
+
vectorEffect="non-scaling-stroke"
|
|
131
|
+
/>
|
|
132
|
+
{result.placements.map((p) => (
|
|
133
|
+
<rect
|
|
134
|
+
key={`${p.packageId}:${p.unit}`}
|
|
135
|
+
x={p.xMm}
|
|
136
|
+
y={p.yMm}
|
|
137
|
+
width={p.widthMm}
|
|
138
|
+
height={p.lengthMm}
|
|
139
|
+
className="fill-primary/15 stroke-primary"
|
|
140
|
+
vectorEffect="non-scaling-stroke"
|
|
141
|
+
>
|
|
142
|
+
<title>{`${p.packageId} #${p.unit}: ${p.xMm}, ${p.yMm} mm; ${p.widthMm} × ${p.lengthMm} mm; ${p.yawDegrees}°`}</title>
|
|
143
|
+
</rect>
|
|
144
|
+
))}
|
|
145
|
+
</svg>
|
|
146
|
+
<figcaption className="text-sm text-text-secondary mt-2">
|
|
147
|
+
{input.pallet.widthMm} × {input.pallet.lengthMm} mm
|
|
148
|
+
</figcaption>
|
|
149
|
+
</figure>
|
|
150
|
+
) : (
|
|
151
|
+
<p>
|
|
152
|
+
{tr('movethewheels.palletPreview.noDiagram', 'No placed subset is available to draw.')}
|
|
153
|
+
</p>
|
|
154
|
+
)}
|
|
155
|
+
</FormSection>
|
|
156
|
+
<FormSection title={tr('movethewheels.palletPreview.bounds', 'Seven reported bounds')}>
|
|
157
|
+
<ul className="grid gap-3 md:grid-cols-2">
|
|
158
|
+
{PALLET_BOUND_KEYS.map((key) => (
|
|
159
|
+
<li key={key} className="rounded-lg border border-border p-3 min-w-0">
|
|
160
|
+
<h3 className="font-medium">
|
|
161
|
+
{tr(`movethewheels.palletPreview.bounds.${key}`, boundLabels[key])}
|
|
162
|
+
</h3>
|
|
163
|
+
<Badge variant="outline">
|
|
164
|
+
{tr(
|
|
165
|
+
`movethewheels.palletPreview.status.${result.bounds[key].status}`,
|
|
166
|
+
result.bounds[key].status
|
|
167
|
+
)}
|
|
168
|
+
</Badge>
|
|
169
|
+
<p className="text-sm mt-1 break-words">{result.bounds[key].reason}</p>
|
|
170
|
+
</li>
|
|
171
|
+
))}
|
|
172
|
+
</ul>
|
|
173
|
+
</FormSection>
|
|
174
|
+
<FormSection
|
|
175
|
+
title={tr('movethewheels.palletPreview.unsupported', 'Seven concerns remain unknown')}
|
|
176
|
+
>
|
|
177
|
+
<ul className="grid gap-2 md:grid-cols-2">
|
|
178
|
+
{PALLET_UNKNOWN_KEYS.map((key) => (
|
|
179
|
+
<li key={key} className="rounded border border-border p-3">
|
|
180
|
+
{tr(`movethewheels.palletPreview.unsupported.${key}`, unknownLabels[key])}:{' '}
|
|
181
|
+
<strong>{unknown}</strong>
|
|
182
|
+
</li>
|
|
183
|
+
))}
|
|
184
|
+
</ul>
|
|
185
|
+
</FormSection>
|
|
186
|
+
<FormSection
|
|
187
|
+
title={tr('movethewheels.palletPreview.sourceSnapshot', 'Submitted source snapshot')}
|
|
188
|
+
description={tr(
|
|
189
|
+
'movethewheels.palletPreview.sourceBoundary',
|
|
190
|
+
'Your declared measurement sources; not independent verification.'
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
<dl className="space-y-2 break-words">
|
|
194
|
+
{result.sources.packages.map((p) => (
|
|
195
|
+
<div key={p.packageId}>
|
|
196
|
+
<dt className="font-medium">{p.packageId}</dt>
|
|
197
|
+
<dd>{sourceText(p.source)}</dd>
|
|
198
|
+
</div>
|
|
199
|
+
))}
|
|
200
|
+
{(['pallet', 'loadEnvelope', 'additionalPacking', 'clearance'] as const).map((key) => (
|
|
201
|
+
<div key={key}>
|
|
202
|
+
<dt className="font-medium">
|
|
203
|
+
{tr(
|
|
204
|
+
`movethewheels.palletPreview.source.${key}`,
|
|
205
|
+
{
|
|
206
|
+
pallet: 'Pallet',
|
|
207
|
+
loadEnvelope: 'Load envelope',
|
|
208
|
+
additionalPacking: 'Additional packing',
|
|
209
|
+
clearance: 'Clearance',
|
|
210
|
+
}[key]
|
|
211
|
+
)}
|
|
212
|
+
</dt>
|
|
213
|
+
<dd>{sourceText(result.sources[key])}</dd>
|
|
214
|
+
</div>
|
|
215
|
+
))}
|
|
216
|
+
</dl>
|
|
217
|
+
</FormSection>
|
|
218
|
+
</section>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { print } from 'graphql';
|
|
3
|
+
import {
|
|
4
|
+
MtwPalletGeometryPreviewDocument,
|
|
5
|
+
type MtwPalletGeometryPreviewInput,
|
|
6
|
+
} from '../../generated/pallet-preview-operations';
|
|
7
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
8
|
+
import { executeGraphQL } from '../utils/graphqlClient';
|
|
9
|
+
import {
|
|
10
|
+
parsePalletPreview,
|
|
11
|
+
validatePalletPreviewEnvelope,
|
|
12
|
+
type PalletPreview,
|
|
13
|
+
} from '../utils/palletPreviewResponse';
|
|
14
|
+
|
|
15
|
+
const query = print(MtwPalletGeometryPreviewDocument);
|
|
16
|
+
export function usePalletPreview() {
|
|
17
|
+
const { workspaceId, tenantId, currentUser, apiGatewayUrl, authToken, workspaceToken } =
|
|
18
|
+
useMoveTheWheels();
|
|
19
|
+
const context = useMemo(
|
|
20
|
+
() => ({
|
|
21
|
+
workspaceId,
|
|
22
|
+
tenantId,
|
|
23
|
+
actorId: currentUser?.id,
|
|
24
|
+
apiGatewayUrl,
|
|
25
|
+
authToken,
|
|
26
|
+
workspaceToken,
|
|
27
|
+
}),
|
|
28
|
+
[workspaceId, tenantId, currentUser?.id, apiGatewayUrl, authToken, workspaceToken]
|
|
29
|
+
);
|
|
30
|
+
const active = useRef(context);
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
active.current = context;
|
|
33
|
+
}, [context]);
|
|
34
|
+
const generation = useRef(0);
|
|
35
|
+
const pending = useRef<AbortController | null>(null);
|
|
36
|
+
const [state, setState] = useState<{
|
|
37
|
+
context: typeof context;
|
|
38
|
+
generation: number;
|
|
39
|
+
loading: boolean;
|
|
40
|
+
result: PalletPreview | null;
|
|
41
|
+
input: MtwPalletGeometryPreviewInput | null;
|
|
42
|
+
failed: boolean;
|
|
43
|
+
} | null>(null);
|
|
44
|
+
const invalidate = useCallback(() => {
|
|
45
|
+
generation.current++;
|
|
46
|
+
pending.current?.abort();
|
|
47
|
+
pending.current = null;
|
|
48
|
+
setState(null);
|
|
49
|
+
}, []);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
invalidate();
|
|
52
|
+
return () => {
|
|
53
|
+
generation.current++;
|
|
54
|
+
pending.current?.abort();
|
|
55
|
+
pending.current = null;
|
|
56
|
+
};
|
|
57
|
+
}, [context, invalidate]);
|
|
58
|
+
const run = useCallback(
|
|
59
|
+
async (input: MtwPalletGeometryPreviewInput) => {
|
|
60
|
+
if (active.current !== context || pending.current || !context.workspaceId || !context.actorId)
|
|
61
|
+
return;
|
|
62
|
+
input = structuredClone(input);
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
pending.current = controller;
|
|
65
|
+
const ticket = ++generation.current;
|
|
66
|
+
setState({ context, generation: ticket, loading: true, result: null, input, failed: false });
|
|
67
|
+
try {
|
|
68
|
+
const data = await executeGraphQL<unknown>(
|
|
69
|
+
{
|
|
70
|
+
...context,
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
validateResponse: validatePalletPreviewEnvelope,
|
|
73
|
+
},
|
|
74
|
+
query,
|
|
75
|
+
{ input }
|
|
76
|
+
);
|
|
77
|
+
const result = parsePalletPreview(data, input);
|
|
78
|
+
if (
|
|
79
|
+
!controller.signal.aborted &&
|
|
80
|
+
active.current === context &&
|
|
81
|
+
generation.current === ticket
|
|
82
|
+
)
|
|
83
|
+
setState({ context, generation: ticket, loading: false, result, input, failed: false });
|
|
84
|
+
} catch {
|
|
85
|
+
if (
|
|
86
|
+
!controller.signal.aborted &&
|
|
87
|
+
active.current === context &&
|
|
88
|
+
generation.current === ticket
|
|
89
|
+
)
|
|
90
|
+
setState({
|
|
91
|
+
context,
|
|
92
|
+
generation: ticket,
|
|
93
|
+
loading: false,
|
|
94
|
+
result: null,
|
|
95
|
+
input,
|
|
96
|
+
failed: true,
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
if (pending.current === controller) pending.current = null;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[context]
|
|
103
|
+
);
|
|
104
|
+
const current = state?.context === context;
|
|
105
|
+
return {
|
|
106
|
+
run,
|
|
107
|
+
invalidate,
|
|
108
|
+
ready: !!context.workspaceId && !!context.actorId,
|
|
109
|
+
loading: current ? state.loading : false,
|
|
110
|
+
result: current ? state.result : null,
|
|
111
|
+
input: current ? state.input : null,
|
|
112
|
+
failed: current ? state.failed : false,
|
|
113
|
+
};
|
|
114
|
+
}
|