@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.
Files changed (29) hide show
  1. package/dist/MoveTheWheelsRoot.js +7 -0
  2. package/dist/MoveTheWheelsRoutes.js +39 -35
  3. package/dist/components/pallet-preview/PalletPreviewResults.js +189 -0
  4. package/dist/generated/pallet-preview-operations.d.ts +273 -0
  5. package/dist/hooks/usePalletPreview.js +86 -0
  6. package/dist/movethewheels/components/pallet-preview/PalletPreviewResults.d.ts +6 -0
  7. package/dist/movethewheels/hooks/usePalletPreview.d.ts +98 -0
  8. package/dist/movethewheels/pages/PalletPreviewPage.d.ts +1 -0
  9. package/dist/movethewheels/utils/graphqlClient.d.ts +4 -0
  10. package/dist/movethewheels/utils/palletPreviewInput.d.ts +54 -0
  11. package/dist/movethewheels/utils/palletPreviewResponse.d.ts +7 -0
  12. package/dist/pages/PalletPreviewPage.js +501 -0
  13. package/dist/pages/WarehousesPage.js +83 -78
  14. package/dist/src/generated/pallet-preview-operations.js +594 -0
  15. package/dist/utils/graphqlClient.js +27 -19
  16. package/dist/utils/palletPreviewInput.js +138 -0
  17. package/dist/utils/palletPreviewResponse.js +191 -0
  18. package/package.json +5 -4
  19. package/src/generated/pallet-preview-operations.ts +204 -0
  20. package/src/movethewheels/MoveTheWheelsRoot.tsx +7 -0
  21. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  22. package/src/movethewheels/components/pallet-preview/PalletPreviewResults.tsx +220 -0
  23. package/src/movethewheels/hooks/usePalletPreview.ts +114 -0
  24. package/src/movethewheels/pages/PalletPreviewPage.tsx +588 -0
  25. package/src/movethewheels/pages/WarehousesPage.tsx +9 -1
  26. package/src/movethewheels/utils/graphqlClient.ts +19 -2
  27. package/src/movethewheels/utils/palletPreviewInput.ts +199 -0
  28. package/src/movethewheels/utils/palletPreviewResponse.ts +256 -0
  29. 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
+ }