@seedtactics/insight-client 17.0.0-beta.0 → 17.0.0-beta.2

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.
@@ -2,6 +2,7 @@ import { Atom } from "jotai";
2
2
  import { ServerEventAndTime } from "./loading";
3
3
  import { ILogEntry, IRebooking, IRecentHistoricData } from "../network/api";
4
4
  import { OrderedMap } from "@seedtactics/immutable-collections";
5
+ export declare function mapUnscheduledRebookings(bookings: ReadonlyArray<Readonly<IRebooking>> | null | undefined): OrderedMap<string, Readonly<IRebooking>>;
5
6
  export declare const last30Rebookings: Atom<OrderedMap<string, Readonly<IRebooking>>>;
6
7
  export declare const canceledRebookings: Atom<OrderedMap<string, Date>>;
7
8
  type ScheduledBooking = {
@@ -46,8 +46,11 @@ const unschRebookings = unwrap(atom(async (_, { signal }) => {
46
46
  return OrderedMap.empty();
47
47
  }
48
48
  const bookings = await JobsBackend.unscheduledRebookings(signal);
49
- return OrderedMap.build(bookings, (booking) => booking.bookingId);
49
+ return mapUnscheduledRebookings(bookings);
50
50
  }), (prev) => prev ?? OrderedMap.empty());
51
+ export function mapUnscheduledRebookings(bookings) {
52
+ return OrderedMap.build(bookings ?? [], (booking) => booking.bookingId);
53
+ }
51
54
  export const last30Rebookings = atom((get) => {
52
55
  const evts = get(rebookingEvts);
53
56
  const unsch = get(unschRebookings);
@@ -2,6 +2,7 @@ import { ReactNode, ComponentType } from "react";
2
2
  import { ChooseModeItem } from "./ChooseMode.js";
3
3
  import * as serverSettings from "../network/server-settings.js";
4
4
  import type { SubmitBasketLoadStationCommand } from "./station-monitor/BasketLoadStationWork.js";
5
+ import type { SubmitBasketLocationCorrection, SubmitBasketMovementCompletion } from "./station-monitor/BasketMovementArrival.js";
5
6
  export declare function NavTabs({ children }: {
6
7
  children?: ReactNode;
7
8
  }): import("react").JSX.Element;
@@ -12,6 +13,8 @@ export interface AppProps {
12
13
  };
13
14
  readonly chooseModes?: (i: serverSettings.FMSInfoAndUser) => ReadonlyArray<ChooseModeItem> | null;
14
15
  readonly submitBasketLoadStationCommand?: SubmitBasketLoadStationCommand;
16
+ readonly submitBasketMovementCompletion?: SubmitBasketMovementCompletion;
17
+ readonly submitBasketLocationCorrection?: SubmitBasketLocationCorrection;
15
18
  }
16
19
  declare const App: import("react").MemoExoticComponent<(props: AppProps) => import("react").JSX.Element>;
17
20
  export default App;
@@ -310,7 +310,7 @@ const App = memo(function App(props) {
310
310
  else {
311
311
  switch (route.route) {
312
312
  case routes.RouteLocation.Station_LoadMonitor:
313
- page = (_jsx(LoadStation, { loadNum: route.loadNum, queues: route.queues, completed: route.completed, submitBasketLoadStationCommand: props.submitBasketLoadStationCommand }));
313
+ page = (_jsx(LoadStation, { loadNum: route.loadNum, queues: route.queues, completed: route.completed, submitBasketLoadStationCommand: props.submitBasketLoadStationCommand, submitBasketMovementCompletion: props.submitBasketMovementCompletion, submitBasketLocationCorrection: props.submitBasketLocationCorrection }, route.loadNum));
314
314
  nav1 = StationToolbar;
315
315
  nav2 = StationToolbarOverviewButton;
316
316
  showOperator = true;
@@ -93,10 +93,22 @@ function logType(entry, fmsInfo) {
93
93
  else {
94
94
  return "Depart";
95
95
  }
96
- case api.LogType.BasketIdentityHint:
97
- return `${basketName} Identity Hint`;
96
+ case api.LogType.BasketIdentityAssociation:
97
+ return `${basketName} Identity Association`;
98
+ case api.LogType.BasketIdentityAssociationCorrection:
99
+ return `${basketName} Identity Correction`;
98
100
  case api.LogType.BasketContentSnapshot:
99
101
  return `${basketName} Content Snapshot`;
102
+ case api.LogType.BasketLocationObservation:
103
+ return `${basketName} Location Observation`;
104
+ case api.LogType.BasketLocationObservationCorrection:
105
+ return `${basketName} Location Correction`;
106
+ case api.LogType.BasketRegionSurvey:
107
+ return `${basketName} Region Survey`;
108
+ case api.LogType.BasketMisload:
109
+ return `${basketName} Inspection Hold`;
110
+ case api.LogType.BasketMisloadResolution:
111
+ return `${basketName} Inspection Resolution`;
100
112
  case api.LogType.MachineCycle:
101
113
  if (entry.startofcycle) {
102
114
  return "Start Cycle";
@@ -141,6 +153,12 @@ function logType(entry, fmsInfo) {
141
153
  return "Message";
142
154
  }
143
155
  }
156
+ function evidencePosition(entry) {
157
+ const title = entry.details?.locationTitle ?? entry.details?.location ?? entry.loc;
158
+ const locationNumber = entry.locnum > 0 ? ` ${entry.locnum}` : "";
159
+ const zone = entry.details?.zone === undefined ? "" : ` zone ${entry.details.zone}`;
160
+ return `${title}${locationNumber}${zone}`;
161
+ }
144
162
  export function isLogEntryInvalidated(e) {
145
163
  return e.details?.["PalletCycleInvalidated"] === "1";
146
164
  }
@@ -202,14 +220,57 @@ function display(props, fmsInfo) {
202
220
  return (_jsxs("span", { children: [basketContainerName(entry, basketName), " departed from ", entry.loc, entry.locnum > 0 ? ` position ${entry.locnum}` : ""] }));
203
221
  }
204
222
  }
205
- case api.LogType.BasketIdentityHint: {
223
+ case api.LogType.BasketIdentityAssociation: {
224
+ const basketName = basketDisplayName(fmsInfo.basketName);
225
+ const episodeCount = entry.details?.episodeCount;
226
+ const source = entry.details?.sourceName;
227
+ const note = entry.details?.note;
228
+ return (_jsxs("span", { children: [source === undefined ? "" : `${source} recorded `, episodeCount === undefined
229
+ ? `content episodes associated with ${basketName} ${entry.pal}`
230
+ : `${episodeCount} content episode${episodeCount === "1" ? "" : "s"} associated with ${basketName} ${entry.pal}`, entry.details?.location === undefined && entry.details?.locationTitle === undefined
231
+ ? ""
232
+ : ` at ${evidencePosition(entry)}`, note === undefined ? "" : `: ${note}`] }));
233
+ }
234
+ case api.LogType.BasketIdentityAssociationCorrection: {
206
235
  const basketName = basketDisplayName(fmsInfo.basketName);
207
- return (_jsxs("span", { children: [basketContainerName(entry, basketName), " presently associated with ", basketName, " ", entry.pal] }));
236
+ return (_jsxs("span", { children: ["Identity association for ", basketName, " ", entry.pal, " corrected"] }));
208
237
  }
209
238
  case api.LogType.BasketContentSnapshot: {
210
239
  const basketName = basketDisplayName(fmsInfo.basketName);
211
240
  return (_jsxs("span", { children: [basketContainerName(entry, basketName), " contained ", displayMat(entry.material)] }));
212
241
  }
242
+ case api.LogType.BasketLocationObservation: {
243
+ const basketName = basketDisplayName(fmsInfo.basketName);
244
+ const operator = entry.details?.sourceName ?? entry.details?.operator ?? "Operator";
245
+ return (_jsxs("span", { children: [operator, " observed ", basketName, " ", entry.pal, " at ", evidencePosition(entry)] }));
246
+ }
247
+ case api.LogType.BasketLocationObservationCorrection: {
248
+ const basketName = basketDisplayName(fmsInfo.basketName);
249
+ const operator = entry.details?.sourceName ?? entry.details?.operator ?? "Operator";
250
+ const location = evidencePosition(entry);
251
+ const replacementBasketId = entry.details?.replacementBasketId;
252
+ return replacementBasketId === undefined ? (_jsxs("span", { children: [operator, " retracted the observation of ", basketName, " ", entry.pal, " at ", location] })) : (_jsxs("span", { children: [operator, " changed the observation of ", basketName, " ", entry.pal, " at ", location, " to", " ", basketName, " ", replacementBasketId, " at ", location] }));
253
+ }
254
+ case api.LogType.BasketRegionSurvey: {
255
+ const basketName = basketDisplayName(fmsInfo.basketName);
256
+ const source = entry.details?.sourceName;
257
+ const observed = entry.details?.observedBasketCount;
258
+ const unidentified = entry.details?.unidentifiedBasketCount;
259
+ return (_jsxs("span", { children: [source === undefined ? "Operator" : source, " recorded a ", entry.result.toLowerCase(), " ", basketName.toLowerCase(), " survey at ", evidencePosition(entry), observed === undefined ? "" : ` (${observed} identified`, unidentified === undefined ? "" : `, ${unidentified} unidentified`, observed === undefined && unidentified === undefined ? "" : ")"] }));
260
+ }
261
+ case api.LogType.BasketMisload: {
262
+ const basketName = basketDisplayName(fmsInfo.basketName);
263
+ const source = entry.details?.sourceName;
264
+ const reason = entry.details?.reason;
265
+ const subject = entry.pal > 0 ? `${basketName} ${entry.pal}` : `Unnumbered ${basketName.toLowerCase()}`;
266
+ return (_jsxs("span", { children: [source === undefined ? subject : `${source} reported ${subject}`, " requires inspection at", " ", evidencePosition(entry), reason === undefined ? "" : `: ${reason}`] }));
267
+ }
268
+ case api.LogType.BasketMisloadResolution: {
269
+ const basketName = basketDisplayName(fmsInfo.basketName);
270
+ const source = entry.details?.sourceName;
271
+ const subject = entry.pal > 0 ? `${basketName} ${entry.pal}` : `Unnumbered ${basketName.toLowerCase()}`;
272
+ return (_jsxs("span", { children: [source === undefined ? "Inspection hold" : `${source} resolved inspection hold`, " for", " ", subject, " at ", evidencePosition(entry), ": ", entry.result] }));
273
+ }
213
274
  case api.LogType.MachineCycle:
214
275
  return (_jsxs("span", { children: [displayMat(entry.material), " on", " ", _jsxs(ColoredSpan, { $type: "pallet", children: ["pallet ", entry.pal] }), " at", " ", _jsxs(ColoredSpan, { $type: "machine", children: [entry.loc, " ", entry.locnum.toString()] }), entry.program && entry.program !== "" ? (_jsxs("span", { children: [" with program ", entry.program] })) : undefined] }));
215
276
  case api.LogType.PartMark:
@@ -29,7 +29,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
29
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
30
  */
31
31
  import { useMemo, useState } from "react";
32
- import { Alert, Box, Button, Card, CardContent, Stack, Typography } from "@mui/material";
32
+ import { Alert, Box, Button, Stack, Typography } from "@mui/material";
33
33
  import { LazySeq } from "@seedtactics/immutable-collections";
34
34
  import * as api from "../../network/api.js";
35
35
  import { InProcMaterial } from "./Material.js";
@@ -150,6 +150,10 @@ export function BasketLoadStationWorkflow({ stationNumber, basket, material, fsi
150
150
  const slotMaterial = materialBySlot.get(slot) ?? [];
151
151
  const loads = loadsBySlot.get(slot) ?? [];
152
152
  const isUnknown = basket.unknownSlots?.includes(slot) === true;
153
- return (_jsx(Card, { "data-testid": `basket-load-station-slot-${slot}`, variant: "outlined", children: _jsxs(CardContent, { children: [_jsxs(Typography, { variant: "h6", children: ["Slot ", slot] }), slotMaterial.length > 0 ? (_jsx(SlotMaterial, { material: slotMaterial, fsize: fsize })) : (_jsx(Typography, { color: isUnknown ? "warning.main" : "text.secondary", children: isUnknown ? "Unknown" : "Empty" })), loads.map((mat, index) => (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { children: loadingInstruction(mat) }), _jsxs(Typography, { variant: "body2", color: "text.secondary", children: [mat.partName, " \u00B7 Process ", mat.action.processAfterLoad ?? mat.process] })] }, `${mat.jobUnique}-${mat.process}-${index}`)))] }) }, slot));
153
+ return (_jsx(MoveMaterialArrowNode, { kind: {
154
+ type: MoveMaterialNodeKindType.BasketSlotZone,
155
+ basketId: basket.basketId,
156
+ slot,
157
+ }, children: _jsxs(Box, { component: "section", "data-testid": `basket-load-station-slot-${slot}`, sx: { border: "1px solid", borderColor: "text.primary", p: 2 }, children: [_jsxs(Typography, { variant: "h6", children: ["Slot ", slot] }), slotMaterial.length > 0 ? (_jsx(SlotMaterial, { material: slotMaterial, fsize: fsize })) : (_jsx(Typography, { color: isUnknown ? "warning.main" : "text.secondary", children: isUnknown ? "Unknown" : "Empty" })), loads.map((mat, index) => (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { children: loadingInstruction(mat) }), _jsxs(Typography, { variant: "body2", color: "text.secondary", children: [mat.partName, " \u00B7 Process ", mat.action.processAfterLoad ?? mat.process] })] }, `${mat.jobUnique}-${mat.process}-${index}`)))] }) }, slot));
154
158
  }) }), work && submitCommand ? (_jsx(Box, { sx: { display: "flex", justifyContent: "flex-end" }, children: _jsx(Button, { variant: "contained", disabled: submissionDisabled, onClick: () => void submit(), children: "Confirm" }) })) : work ? (_jsx(Alert, { severity: "warning", children: "Basket work confirmation is not configured." })) : workState.type === "inconsistent" ? (_jsx(Alert, { severity: "warning", children: "Basket work is inconsistent. Wait for refreshed material actions before confirming." })) : null, submissionState === "accepted" ? (_jsx(Alert, { severity: "success", children: "Confirmation accepted. Waiting for refreshed work." })) : submissionState === "conflict" ? (_jsx(Alert, { severity: "warning", children: "Basket work changed before confirmation. Review the refreshed slots and try again." })) : submissionState === "error" ? (_jsx(Alert, { severity: "error", children: "Unable to confirm basket work. No work was assumed complete." })) : null] }));
155
159
  }
@@ -0,0 +1,38 @@
1
+ import * as api from "../../network/api.js";
2
+ export interface BasketMovementCompletionCommand {
3
+ readonly commandId: string;
4
+ readonly instructionId: string;
5
+ readonly observedBasketId: number;
6
+ }
7
+ export interface BasketMovementCompletionReceipt {
8
+ readonly stationNumber: number;
9
+ readonly instructionId: string;
10
+ readonly observationId: string;
11
+ readonly observedBasketId: number;
12
+ }
13
+ export type SubmitBasketMovementCompletion = (stationNumber: number, command: BasketMovementCompletionCommand) => Promise<BasketMovementCompletionReceipt | "conflict">;
14
+ export interface BasketLocationCorrectionCommand {
15
+ readonly correctionId: string;
16
+ readonly targetObservationId: string;
17
+ readonly replacementBasketId: number | null;
18
+ readonly replacementObservationId: string | null;
19
+ }
20
+ export type SubmitBasketLocationCorrection = (stationNumber: number, command: BasketLocationCorrectionCommand) => Promise<"accepted" | "conflict">;
21
+ export interface BasketArrivalReceipt {
22
+ readonly stationNumber: number;
23
+ readonly instruction: Readonly<api.IBasketMoveInstruction>;
24
+ readonly command: BasketMovementCompletionCommand;
25
+ readonly receipt: BasketMovementCompletionReceipt;
26
+ readonly status: "recorded" | "corrected" | "retracted";
27
+ }
28
+ export declare function loadStationArrivalInstruction(instructions: ReadonlyArray<Readonly<api.IBasketMoveInstruction>>, stationNumber: number): Readonly<api.IBasketMoveInstruction> | undefined;
29
+ export declare function BasketMovementArrival({ stationNumber, basketName, instruction, submitCommand, submitCorrection, receipt, onAccepted, onCorrected, }: {
30
+ readonly stationNumber: number;
31
+ readonly basketName: string;
32
+ readonly instruction: Readonly<api.IBasketMoveInstruction>;
33
+ readonly submitCommand: SubmitBasketMovementCompletion | undefined;
34
+ readonly submitCorrection: SubmitBasketLocationCorrection | undefined;
35
+ readonly receipt?: BasketArrivalReceipt;
36
+ readonly onAccepted?: (receipt: BasketArrivalReceipt) => void;
37
+ readonly onCorrected?: (command: BasketLocationCorrectionCommand) => void;
38
+ }): import("react").JSX.Element;
@@ -0,0 +1,175 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Copyright (c) 2026, John Lenz
3
+
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright
10
+ notice, this list of conditions and the following disclaimer.
11
+ * Redistributions in binary form must reproduce the above copyright
12
+ notice, this list of conditions and the following disclaimer in the
13
+ documentation and/or other materials provided with the distribution.
14
+ * Neither the name of John Lenz, Black Maple Software, SeedTactics,
15
+ nor the names of other contributors may be used to endorse or
16
+ promote products derived from this software without specific
17
+ prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ */
31
+ import { useEffect, useRef, useState } from "react";
32
+ import { Alert, Box, Button, Paper, Stack, TextField, Typography } from "@mui/material";
33
+ import * as api from "../../network/api.js";
34
+ export function loadStationArrivalInstruction(instructions, stationNumber) {
35
+ const activeIds = new Set(instructions.map((instruction) => instruction.instructionId));
36
+ return instructions.find((instruction) => instruction.basketId !== undefined &&
37
+ instruction.destination.location === api.BasketLocationEnum.LoadUnload &&
38
+ instruction.destination.locationNum === stationNumber &&
39
+ (instruction.prerequisiteInstructionId === undefined ||
40
+ !activeIds.has(instruction.prerequisiteInstructionId)));
41
+ }
42
+ export function BasketMovementArrival({ stationNumber, basketName, instruction, submitCommand, submitCorrection, receipt, onAccepted, onCorrected, }) {
43
+ const [chooseDifferent, setChooseDifferent] = useState(false);
44
+ const [differentBasket, setDifferentBasket] = useState("");
45
+ const [submission, setSubmission] = useState();
46
+ const [changingRecordedBasket, setChangingRecordedBasket] = useState(false);
47
+ const [correctionSubmission, setCorrectionSubmission] = useState();
48
+ const currentInstructionId = useRef(instruction.instructionId);
49
+ const currentStationNumber = useRef(stationNumber);
50
+ const currentCorrectionTargetId = useRef(undefined);
51
+ currentInstructionId.current = instruction.instructionId;
52
+ currentStationNumber.current = stationNumber;
53
+ useEffect(() => {
54
+ setChooseDifferent(false);
55
+ setDifferentBasket("");
56
+ setSubmission((current) => current?.state === "error" && current.command.instructionId === instruction.instructionId
57
+ ? current
58
+ : undefined);
59
+ setChangingRecordedBasket(false);
60
+ setCorrectionSubmission(undefined);
61
+ }, [instruction.instructionId, stationNumber]);
62
+ useEffect(() => {
63
+ setChangingRecordedBasket(false);
64
+ setDifferentBasket("");
65
+ setCorrectionSubmission((current) => current?.state === "error" &&
66
+ receipt?.status !== "retracted" &&
67
+ current.command.targetObservationId === receipt?.receipt.observationId
68
+ ? current
69
+ : undefined);
70
+ }, [receipt?.receipt.observationId, receipt?.status]);
71
+ const expectedBasketId = instruction.basketId;
72
+ if (expectedBasketId === undefined) {
73
+ return (_jsx(Alert, { severity: "warning", children: "This tracked basket must be handled through the recovery workflow." }));
74
+ }
75
+ async function record(command) {
76
+ if (submitCommand === undefined)
77
+ return;
78
+ setSubmission({ state: "submitting", command });
79
+ try {
80
+ const result = await submitCommand(stationNumber, command);
81
+ if (currentStationNumber.current !== stationNumber ||
82
+ currentInstructionId.current !== command.instructionId)
83
+ return;
84
+ if (result === "conflict") {
85
+ setSubmission({ state: result, command });
86
+ }
87
+ else {
88
+ setSubmission({ state: "accepted", command, receipt: result });
89
+ onAccepted?.({
90
+ stationNumber,
91
+ instruction,
92
+ command,
93
+ receipt: result,
94
+ status: "recorded",
95
+ });
96
+ }
97
+ }
98
+ catch (error) {
99
+ if (currentStationNumber.current !== stationNumber ||
100
+ currentInstructionId.current !== command.instructionId)
101
+ return;
102
+ setSubmission({
103
+ state: "error",
104
+ command,
105
+ message: error instanceof Error ? error.message : "Unable to record basket arrival",
106
+ });
107
+ }
108
+ }
109
+ async function correct(command) {
110
+ if (submitCorrection === undefined || activeCommand === undefined)
111
+ return;
112
+ setCorrectionSubmission({ state: "submitting", command });
113
+ try {
114
+ const result = await submitCorrection(stationNumber, command);
115
+ if (currentStationNumber.current !== stationNumber ||
116
+ currentCorrectionTargetId.current !== command.targetObservationId)
117
+ return;
118
+ setCorrectionSubmission({ state: result, command });
119
+ if (result === "accepted")
120
+ onCorrected?.(command);
121
+ }
122
+ catch {
123
+ if (currentStationNumber.current !== stationNumber ||
124
+ currentCorrectionTargetId.current !== command.targetObservationId)
125
+ return;
126
+ setCorrectionSubmission({ state: "error", command });
127
+ }
128
+ }
129
+ function createCommand(basketId) {
130
+ return {
131
+ commandId: crypto.randomUUID(),
132
+ instructionId: instruction.instructionId,
133
+ observedBasketId: basketId,
134
+ };
135
+ }
136
+ const parsedDifferentBasket = Number(differentBasket);
137
+ const activeCommand = receipt?.status === "retracted"
138
+ ? undefined
139
+ : (receipt?.command ??
140
+ (submission?.state === "accepted" &&
141
+ submission.command.instructionId === instruction.instructionId
142
+ ? submission.command
143
+ : undefined));
144
+ const activeObservationId = receipt?.status === "retracted"
145
+ ? undefined
146
+ : (receipt?.receipt.observationId ??
147
+ (submission?.state === "accepted" ? submission.receipt.observationId : undefined));
148
+ currentCorrectionTargetId.current = activeObservationId;
149
+ function createCorrection(replacementBasketId) {
150
+ return {
151
+ correctionId: crypto.randomUUID(),
152
+ targetObservationId: activeObservationId ?? "",
153
+ replacementBasketId,
154
+ replacementObservationId: replacementBasketId === null ? null : crypto.randomUUID(),
155
+ };
156
+ }
157
+ const validDifferentBasket = Number.isInteger(parsedDifferentBasket) &&
158
+ parsedDifferentBasket > 0 &&
159
+ parsedDifferentBasket !== (activeCommand?.observedBasketId ?? expectedBasketId);
160
+ const disabled = submitCommand === undefined ||
161
+ activeCommand !== undefined ||
162
+ (submission !== undefined && submission.state !== "error");
163
+ const receiptMessage = receipt?.status === "retracted"
164
+ ? "Arrival observation retracted."
165
+ : receipt?.status === "corrected"
166
+ ? `Arrival corrected to ${basketName.toLocaleLowerCase()} ${receipt.command.observedBasketId}.`
167
+ : "Arrival recorded. Waiting for updated status.";
168
+ return (_jsx(Paper, { component: "section", elevation: 2, sx: { margin: 1, padding: 2 }, children: _jsxs(Stack, { spacing: 2, children: [_jsxs(Box, { children: [_jsx(Typography, { variant: "overline", children: receipt === undefined ? "Basket arrival" : "Basket arrival receipt" }), _jsx(Typography, { variant: "h4", children: receipt?.status === "retracted"
169
+ ? "Retracted basket arrival"
170
+ : receipt === undefined
171
+ ? `Bring ${basketName.toLocaleLowerCase()} ${expectedBasketId} to this station`
172
+ : `${basketName} ${receipt.command.observedBasketId} at this station` }), receipt === undefined ? (_jsx(Typography, { children: "Record arrival after the basket is physically in place." })) : null] }), receipt === undefined ? (_jsxs(Stack, { direction: { xs: "column", sm: "row" }, spacing: 1, children: [_jsx(Button, { disabled: disabled, onClick: () => void record(createCommand(expectedBasketId)), size: "large", variant: "contained", children: submission?.state === "submitting"
173
+ ? "Recording arrival…"
174
+ : `${basketName} ${expectedBasketId} arrived` }), _jsxs(Button, { disabled: disabled, onClick: () => setChooseDifferent((visible) => !visible), size: "large", variant: "outlined", children: ["Different ", basketName.toLocaleLowerCase()] })] })) : null, receipt === undefined && chooseDifferent && !disabled ? (_jsxs(Stack, { direction: { xs: "column", sm: "row" }, spacing: 1, children: [_jsx(TextField, { label: `${basketName} number`, onChange: (event) => setDifferentBasket(event.target.value), type: "number", value: differentBasket, slotProps: { htmlInput: { inputMode: "numeric", min: 1 } } }), _jsxs(Button, { disabled: !validDifferentBasket, onClick: () => void record(createCommand(parsedDifferentBasket)), variant: "contained", children: ["Record ", basketName.toLocaleLowerCase(), " arrival"] })] })) : null, submitCommand === undefined ? (_jsx(Alert, { severity: "warning", children: "Basket arrival completion is not configured." })) : activeCommand !== undefined || receipt?.status === "retracted" ? (_jsxs(Stack, { spacing: 1, children: [_jsx(Alert, { severity: "success", children: receiptMessage }), submitCorrection === undefined || activeCommand === undefined ? null : (_jsxs(Stack, { direction: { xs: "column", sm: "row" }, spacing: 1, children: [_jsx(Button, { disabled: correctionSubmission !== undefined, onClick: () => setChangingRecordedBasket((visible) => !visible), variant: "outlined", children: "Change" }), _jsx(Button, { color: "error", disabled: correctionSubmission !== undefined, onClick: () => void correct(createCorrection(null)), variant: "text", children: "Undo" })] })), changingRecordedBasket && correctionSubmission === undefined ? (_jsxs(Stack, { direction: { xs: "column", sm: "row" }, spacing: 1, children: [_jsx(TextField, { label: `Correct ${basketName.toLocaleLowerCase()} number`, onChange: (event) => setDifferentBasket(event.target.value), type: "number", value: differentBasket, slotProps: { htmlInput: { inputMode: "numeric", min: 1 } } }), _jsx(Button, { disabled: !validDifferentBasket, onClick: () => void correct(createCorrection(parsedDifferentBasket)), variant: "contained", children: "Save correction" })] })) : null, correctionSubmission?.state === "accepted" ? (_jsx(Alert, { severity: "success", children: "Correction recorded." })) : correctionSubmission?.state === "conflict" ? (_jsx(Alert, { severity: "warning", children: "Basket evidence changed. Review the refreshed status." })) : correctionSubmission?.state === "error" ? (_jsx(Alert, { action: _jsx(Button, { color: "inherit", onClick: () => void correct(correctionSubmission.command), children: "Retry correction" }), severity: "error", children: "Unable to correct the recorded arrival." })) : null] })) : submission?.state === "conflict" ? (_jsx(Alert, { severity: "warning", children: "Basket movement changed. Review the refreshed instruction." })) : submission?.state === "error" ? (_jsx(Alert, { action: _jsx(Button, { onClick: () => void record(submission.command), color: "inherit", children: "Retry arrival" }), severity: "error", children: submission.message })) : null] }) }));
175
+ }
@@ -1,11 +1,14 @@
1
1
  import { ReactNode } from "react";
2
2
  import { SubmitBasketLoadStationCommand } from "./BasketLoadStationWork.js";
3
+ import { SubmitBasketLocationCorrection, SubmitBasketMovementCompletion } from "./BasketMovementArrival.js";
3
4
  interface LoadStationProps {
4
5
  readonly loadNum: number;
5
6
  readonly queues: ReadonlyArray<string>;
6
7
  readonly completed: boolean;
7
8
  readonly whiteBackground?: boolean;
8
9
  readonly submitBasketLoadStationCommand?: SubmitBasketLoadStationCommand;
10
+ readonly submitBasketMovementCompletion?: SubmitBasketMovementCompletion;
11
+ readonly submitBasketLocationCorrection?: SubmitBasketLocationCorrection;
9
12
  }
10
13
  export declare function LoadStation(props: LoadStationProps): import("react").JSX.Element;
11
14
  declare function LoadStationCheckWidth(props: LoadStationProps): ReactNode;
@@ -31,7 +31,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31
31
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
32
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
33
  */
34
- import { useMemo, memo, useState, useCallback } from "react";
34
+ import { useMemo, memo, useState, useCallback, useEffect } from "react";
35
35
  import { Box, useMediaQuery, Button, Typography } from "@mui/material";
36
36
  import { LazySeq, mkCompareByProperties, OrderedMap } from "@seedtactics/immutable-collections";
37
37
  import { FolderOpen as FolderOpenIcon } from "@mui/icons-material";
@@ -64,6 +64,7 @@ import { hideNonLoadingMaterialOnLoadStation } from "../../data/queue-material.j
64
64
  import { basketDisplayName, loadStationDisplayName } from "../../cell-status/station-cycles.js";
65
65
  import { canAddOrMoveMaterialToQueue } from "../../data/material-operation-policy.js";
66
66
  import { BasketLoadStationWorkflow, } from "./BasketLoadStationWork.js";
67
+ import { BasketMovementArrival, loadStationArrivalInstruction, } from "./BasketMovementArrival.js";
67
68
  function encodeLoadStationRegionIdPart(value) {
68
69
  return encodeURIComponent(value.toString());
69
70
  }
@@ -284,6 +285,21 @@ function selectLoadStationAndQueueProps(loadNum, queues, curSt, hideNonLoading)
284
285
  queueMat.forEach((queue) => queue.mats.sort(mkCompareByProperties((mat) => mat.location.queuePosition ?? 0)));
285
286
  basketMat.forEach((basket) => basket.mats.sort(mkCompareByProperties((mat) => mat.location.basketSlot ?? 0)));
286
287
  const matCount = freeLoading.length + palFaces.valuesToAscLazySeq().sumBy((x) => x.length);
288
+ const activeBasketSlotCount = activeBasket
289
+ ? LazySeq.of(activeBasket.emptySlots ?? [])
290
+ .concat(activeBasket.unknownSlots ?? [])
291
+ .concat(LazySeq.of(curSt.material).collect((mat) => mat.location.type === api.LocType.InBasket &&
292
+ mat.location.basketId === activeBasket.basketId
293
+ ? mat.location.basketSlot
294
+ : undefined))
295
+ .concat(LazySeq.of(curSt.material).collect((mat) => mat.action.type === api.ActionType.LoadingToBasket &&
296
+ mat.action.loadToBasketId === activeBasket.basketId
297
+ ? mat.action.loadToBasketSlot
298
+ : undefined))
299
+ .distinctBy((slot) => slot)
300
+ .toRArray().length
301
+ : 0;
302
+ const layoutCount = Math.max(matCount, activeBasketSlotCount);
287
303
  return {
288
304
  pallet: pal,
289
305
  activeBasket,
@@ -293,7 +309,7 @@ function selectLoadStationAndQueueProps(loadNum, queues, curSt, hideNonLoading)
293
309
  queues: queueMat,
294
310
  baskets: basketMat,
295
311
  elapsedLoadingTime,
296
- fsize: matCount <= 2 ? "x-large" : matCount <= 6 ? "large" : "normal",
312
+ fsize: layoutCount <= 2 ? "x-large" : layoutCount <= 6 ? "large" : "normal",
297
313
  };
298
314
  }
299
315
  function MultiInstructionButton({ loadData }) {
@@ -648,6 +664,20 @@ export function LoadStation(props) {
648
664
  const currentSt = useAtomValue(currentStatus);
649
665
  const hideNonLoading = useAtomValue(hideNonLoadingMaterialOnLoadStation);
650
666
  const data = useMemo(() => selectLoadStationAndQueueProps(props.loadNum, props.queues, currentSt, hideNonLoading), [currentSt, props.loadNum, props.queues, hideNonLoading]);
667
+ const arrivalInstruction = loadStationArrivalInstruction(currentSt.basketMoveInstructions ?? [], props.loadNum);
668
+ const [recentArrivalReceipt, setRecentArrivalReceipt] = useState();
669
+ useEffect(() => {
670
+ if (recentArrivalReceipt !== undefined &&
671
+ (recentArrivalReceipt.stationNumber !== props.loadNum ||
672
+ (arrivalInstruction !== undefined &&
673
+ arrivalInstruction.instructionId !== recentArrivalReceipt.instruction.instructionId)))
674
+ setRecentArrivalReceipt(undefined);
675
+ }, [arrivalInstruction, props.loadNum, recentArrivalReceipt]);
676
+ const displayedArrivalInstruction = arrivalInstruction ?? recentArrivalReceipt?.instruction;
677
+ const displayedArrivalReceipt = recentArrivalReceipt?.stationNumber === props.loadNum &&
678
+ displayedArrivalInstruction?.instructionId === recentArrivalReceipt?.instruction.instructionId
679
+ ? recentArrivalReceipt
680
+ : undefined;
651
681
  const queueCols = LazySeq.of(data.queues)
652
682
  .sortBy(([q, _]) => q)
653
683
  .map(([q, mats]) => ({
@@ -694,31 +724,53 @@ export function LoadStation(props) {
694
724
  horizontal: fillViewPort,
695
725
  showMatInCompleted: props.completed,
696
726
  });
697
- return (_jsx(MoveMaterialArrowContainer, { hideArrows: !fillViewPort, whiteBackground: props.whiteBackground, children: _jsxs(Box, { component: "main", sx: {
698
- width: "100%",
699
- display: "grid",
700
- gridTemplate: grid,
701
- minHeight: {
702
- xs: "calc(100vh - 64px - 32px)",
703
- sm: "calc(100vh - 64px - 40px)",
704
- md: "calc(100vh - 64px)",
705
- },
706
- padding: fillViewPort ? undefined : "8px",
707
- }, children: [matCols.map((col, idx) => (_jsx(Box, { "data-testid": materialColumnTestId(col), sx: {
708
- gridArea: `mat${idx}`,
709
- padding: "8px",
710
- borderRight: fillViewPort ? "1px solid black" : undefined,
711
- borderBottom: !fillViewPort ? "1px solid black" : undefined,
712
- }, children: _jsx(MaterialColumn, { data: data, region: col }) }, idx))), data.face.keysToAscLazySeq().map((faceNum, idx) => (_jsx(Box, { sx: {
713
- marginLeft: "15px",
714
- gridArea: `palface${idx}`,
715
- padding: "8px",
716
- borderTop: data.pallet && idx !== 0 ? "1px solid black" : undefined,
717
- }, children: _jsx(PalletFace, { data: data, faceNum: faceNum, loadNum: props.loadNum, submitBasketLoadStationCommand: props.submitBasketLoadStationCommand }) }, faceNum))), _jsx(Box, { "data-testid": "load-station-completed", sx: {
718
- borderLeft: fillViewPort ? "1px solid black" : undefined,
719
- borderTop: !fillViewPort ? "1px solid black" : undefined,
720
- gridArea: "completed",
721
- }, children: _jsx(CompletedCol, { fillViewPort: fillViewPort, showMaterial: props.completed }) }), _jsx(MultiInstructionButton, { loadData: data }), _jsx(SelectWorkorderDialog, {}), _jsx(SelectInspTypeDialog, {}), _jsx(LoadMatDialog, { pallet: data.pallet?.palletNum ?? null, queues: props.queues })] }) }));
727
+ return (_jsxs(_Fragment, { children: [displayedArrivalInstruction === undefined ? null : (_jsx(BasketMovementArrival, { basketName: basketName, instruction: displayedArrivalInstruction, stationNumber: props.loadNum, submitCommand: props.submitBasketMovementCompletion, submitCorrection: props.submitBasketLocationCorrection, receipt: displayedArrivalReceipt, onAccepted: setRecentArrivalReceipt, onCorrected: (command) => setRecentArrivalReceipt((current) => {
728
+ if (current === undefined ||
729
+ current.stationNumber !== props.loadNum ||
730
+ current.receipt.observationId !== command.targetObservationId)
731
+ return current;
732
+ return command.replacementBasketId === null ||
733
+ command.replacementObservationId === null
734
+ ? { ...current, status: "retracted" }
735
+ : {
736
+ ...current,
737
+ receipt: {
738
+ ...current.receipt,
739
+ observationId: command.replacementObservationId,
740
+ observedBasketId: command.replacementBasketId,
741
+ },
742
+ command: {
743
+ ...current.command,
744
+ commandId: command.replacementObservationId,
745
+ observedBasketId: command.replacementBasketId,
746
+ },
747
+ status: "corrected",
748
+ };
749
+ }) })), _jsx(MoveMaterialArrowContainer, { hideArrows: !fillViewPort, whiteBackground: props.whiteBackground, children: _jsxs(Box, { component: "main", sx: {
750
+ width: "100%",
751
+ display: "grid",
752
+ gridTemplate: grid,
753
+ minHeight: {
754
+ xs: "calc(100vh - 64px - 32px)",
755
+ sm: "calc(100vh - 64px - 40px)",
756
+ md: "calc(100vh - 64px)",
757
+ },
758
+ padding: fillViewPort ? undefined : "8px",
759
+ }, children: [matCols.map((col, idx) => (_jsx(Box, { "data-testid": materialColumnTestId(col), sx: {
760
+ gridArea: `mat${idx}`,
761
+ padding: "8px",
762
+ borderRight: fillViewPort ? "1px solid black" : undefined,
763
+ borderBottom: !fillViewPort ? "1px solid black" : undefined,
764
+ }, children: _jsx(MaterialColumn, { data: data, region: col }) }, idx))), data.face.keysToAscLazySeq().map((faceNum, idx) => (_jsx(Box, { sx: {
765
+ marginLeft: "15px",
766
+ gridArea: `palface${idx}`,
767
+ padding: "8px",
768
+ borderTop: data.pallet && idx !== 0 ? "1px solid black" : undefined,
769
+ }, children: _jsx(PalletFace, { data: data, faceNum: faceNum, loadNum: props.loadNum, submitBasketLoadStationCommand: props.submitBasketLoadStationCommand }) }, faceNum))), _jsx(Box, { "data-testid": "load-station-completed", sx: {
770
+ borderLeft: fillViewPort ? "1px solid black" : undefined,
771
+ borderTop: !fillViewPort ? "1px solid black" : undefined,
772
+ gridArea: "completed",
773
+ }, children: _jsx(CompletedCol, { fillViewPort: fillViewPort, showMaterial: props.completed }) }), _jsx(MultiInstructionButton, { loadData: data }), _jsx(SelectWorkorderDialog, {}), _jsx(SelectInspTypeDialog, {}), _jsx(LoadMatDialog, { pallet: data.pallet?.palletNum ?? null, queues: props.queues })] }) })] }));
722
774
  }
723
775
  function LoadStationCheckWidth(props) {
724
776
  const fmsInfo = useAtomValue(fmsInformation);
@@ -377,6 +377,7 @@ const MatCard = forwardRef(function MatCard(props, ref) {
377
377
  }, children: [_jsx(PartIdenticon, { part: props.mat.partName }), _jsxs(Box, { sx: {
378
378
  marginLeft: "8px",
379
379
  flexGrow: 1,
380
+ minWidth: 0,
380
381
  }, children: [_jsx(MatCardHeader, { fsize: props.fsize, children: props.mat.partName }), props.displayJob ? (_jsx(MatCardDetail, { fsize: props.fsize, children: props.mat.jobUnique && props.mat.jobUnique !== ""
381
382
  ? "Assigned to " + props.mat.jobUnique
382
383
  : "Unassigned material" })) : undefined, !props.hideEmptySerial || props.mat.serial ? (_jsxs(MatCardDetail, { fsize: props.fsize, children: ["Serial: ", props.mat.serial ? props.mat.serial : "none"] })) : undefined, props.mat.workorderId === undefined ||
@@ -391,6 +392,7 @@ const MatCard = forwardRef(function MatCard(props, ref) {
391
392
  justifyContent: "space-between",
392
393
  alignItems: "flex-end",
393
394
  alignSelf: "start",
395
+ flexShrink: 0,
394
396
  }, children: [props.mat.serial && props.mat.serial.length >= 1 && !props.hideAvatar ? (_jsx("div", { children: _jsx(Avatar, { style: { width: "30px", height: "30px" }, children: props.mat.serial.slice(-1) }) })) : undefined, props.hideWarningIcon ? undefined : (_jsx("div", { children: _jsx(Warning, { mat: props.mat }) }))] })] }) })] }));
395
397
  });
396
398
  export const MatSummary = memo(MatCard);
@@ -7,7 +7,8 @@ export declare enum MoveMaterialNodeKindType {
7
7
  CompletedExpandedMaterialZone = 3,
8
8
  PalletFaceZone = 4,
9
9
  QueueZone = 5,
10
- BasketZone = 6
10
+ BasketZone = 6,
11
+ BasketSlotZone = 7
11
12
  }
12
13
  export type MoveMaterialNodeKind = {
13
14
  readonly type: MoveMaterialNodeKindType.Material;
@@ -28,6 +29,10 @@ export type MoveMaterialNodeKind = {
28
29
  } | {
29
30
  readonly type: MoveMaterialNodeKindType.BasketZone;
30
31
  readonly basketId: number;
32
+ } | {
33
+ readonly type: MoveMaterialNodeKindType.BasketSlotZone;
34
+ readonly basketId: number;
35
+ readonly slot: number;
31
36
  };
32
37
  export type MoveMaterialIdentifier = string;
33
38
  export declare function uniqueIdForNodeKind(kind: MoveMaterialNodeKind): MoveMaterialIdentifier;