@seedtactics/insight-client 17.0.1-beta.12 → 17.0.1-beta.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 (45) hide show
  1. package/README.md +16 -4
  2. package/dist/assets/ProgramHighlight-CnZe9PDO.js +3 -0
  3. package/dist/assets/index-DWZg8nkq.js +367 -0
  4. package/dist/cell-status/current-status.d.ts +1 -0
  5. package/dist/cell-status/current-status.js +2 -0
  6. package/dist/cell-status/loading.d.ts +13 -0
  7. package/dist/cell-status/loading.js +36 -3
  8. package/dist/cell-status/material-details.d.ts +2 -2
  9. package/dist/cell-status/material-details.js +26 -13
  10. package/dist/components/App.d.ts +0 -5
  11. package/dist/components/App.js +1 -1
  12. package/dist/components/analysis/BufferChart.js +3 -2
  13. package/dist/components/operations/AllMaterial.js +9 -12
  14. package/dist/components/operations/RecentCycleChart.js +16 -21
  15. package/dist/components/station-monitor/BasketLoadStationWork.d.ts +1 -7
  16. package/dist/components/station-monitor/BasketLoadStationWork.js +9 -6
  17. package/dist/components/station-monitor/CancelLoadButton.js +2 -0
  18. package/dist/components/station-monitor/InvalidateCycle.js +24 -19
  19. package/dist/components/station-monitor/LoadStation.d.ts +0 -5
  20. package/dist/components/station-monitor/LoadStation.js +33 -72
  21. package/dist/components/station-monitor/Material.js +4 -2
  22. package/dist/components/station-monitor/PrintedLabel.js +15 -19
  23. package/dist/components/station-monitor/QuarantineButton.js +17 -26
  24. package/dist/components/station-monitor/SystemOverview.js +3 -2
  25. package/dist/components/station-monitor/Whiteboard.js +14 -15
  26. package/dist/data/material-operation-policy.d.ts +6 -0
  27. package/dist/data/material-operation-policy.js +26 -2
  28. package/dist/extension.d.ts +1 -1
  29. package/dist/extension.js +1 -0
  30. package/dist/index.html +2 -39
  31. package/dist/index.js +1 -6
  32. package/dist/network/api.d.ts +14 -34
  33. package/dist/network/api.js +62 -64
  34. package/dist/network/backend-mock.js +3 -0
  35. package/dist/network/backend.d.ts +1 -0
  36. package/dist/network/websocket.d.ts +1 -0
  37. package/dist/network/websocket.js +122 -36
  38. package/docs/material-quarantine.md +5 -4
  39. package/docs/mazak.md +9 -12
  40. package/docs/operator-procedures.md +14 -0
  41. package/package.json +22 -17
  42. package/dist/assets/ProgramHighlight-C6zSEXCX.js +0 -3
  43. package/dist/assets/index-kk-GRa5i.js +0 -388
  44. package/dist/components/station-monitor/BasketMovementArrival.d.ts +0 -40
  45. package/dist/components/station-monitor/BasketMovementArrival.js +0 -165
@@ -5,6 +5,7 @@ export declare const currentStatus: Atom<ICurrentStatus>;
5
5
  export declare const secondsSinceEpochAtom: import("jotai").PrimitiveAtom<number> & {
6
6
  init: number;
7
7
  };
8
+ export declare const minutesSinceEpochAtom: Atom<number>;
8
9
  export declare const setJobComment: import("jotai").WritableAtom<null, [uniq: string, newVal: string | null], void> & {
9
10
  init: null;
10
11
  };
@@ -51,6 +51,8 @@ secondsSinceEpochAtom.onMount = (setSelf) => {
51
51
  }, 1000);
52
52
  return () => clearInterval(interval);
53
53
  };
54
+ // Minute precision keeps report time windows current without redrawing charts every second.
55
+ export const minutesSinceEpochAtom = atom((get) => Math.floor(get(secondsSinceEpochAtom) / 60));
54
56
  export const setJobComment = atom(null, (_, set, uniq, newVal) => {
55
57
  const newComment = newVal === null ? "" : newVal;
56
58
  set(currentStatusRW, (st) => {
@@ -1,4 +1,5 @@
1
1
  import { ICurrentStatus, IHistoricData, ILogEntry, IRecentHistoricData, IServerEvent } from "../network/api.js";
2
+ import * as currentSt from "./current-status.js";
2
3
  import { Atom } from "jotai";
3
4
  export interface ServerEventAndTime {
4
5
  readonly evt: Readonly<IServerEvent>;
@@ -6,6 +7,12 @@ export interface ServerEventAndTime {
6
7
  readonly expire: boolean;
7
8
  }
8
9
  export declare const lastEventCounter: Atom<number | null>;
10
+ export declare const onLiveServerEvent: import("jotai").WritableAtom<null, [evt: ServerEventAndTime], void> & {
11
+ init: null;
12
+ };
13
+ export declare const onHistoricalServerEvent: import("jotai").WritableAtom<null, [evt: ServerEventAndTime], void> & {
14
+ init: null;
15
+ };
9
16
  export declare const onServerEvent: import("jotai").WritableAtom<null, [evt: ServerEventAndTime], void> & {
10
17
  init: null;
11
18
  };
@@ -18,6 +25,12 @@ export declare const onLoadLast30Log: import("jotai").WritableAtom<null, [log: r
18
25
  export declare const onLoadCurrentSt: import("jotai").WritableAtom<null, [curSt: Readonly<ICurrentStatus>], void> & {
19
26
  init: null;
20
27
  };
28
+ export interface QueuedMatMove extends currentSt.QueueReordering {
29
+ readonly operator: string | null;
30
+ }
31
+ export declare const moveQueuedMatInCurrentStatus: import("jotai").WritableAtom<null, [QueuedMatMove], Promise<void>> & {
32
+ init: null;
33
+ };
21
34
  export declare const onLoadSpecificMonthJobs: import("jotai").WritableAtom<null, [historicData: Readonly<IHistoricData>], void> & {
22
35
  init: null;
23
36
  };
@@ -30,6 +30,8 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30
30
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
31
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
32
  */
33
+ import { QueuePosition, } from "../network/api.js";
34
+ import { JobsBackend } from "../network/backend.js";
33
35
  import { LazySeq } from "@seedtactics/immutable-collections";
34
36
  import * as simProd from "./sim-production.js";
35
37
  import * as simUse from "./sim-station-use.js";
@@ -50,16 +52,20 @@ import * as simDayUsage from "./sim-day-usage.js";
50
52
  import * as rebookings from "./rebookings.js";
51
53
  import { atom } from "jotai";
52
54
  const lastEventCounterRW = atom(null);
55
+ // Highest counter applied to historical projections. Buffered websocket events do not advance it.
56
+ // This is a reconnect cursor, not proof that lower counters cannot arrive later on the socket.
53
57
  export const lastEventCounter = lastEventCounterRW;
54
- export const onServerEvent = atom(null, (_, set, evt) => {
58
+ export const onLiveServerEvent = atom(null, (_, set, evt) => {
55
59
  if (evt.evt.newCurrentStatus) {
56
60
  set(customSt.replaceCustomState, evt.evt.newCurrentStatus.customState ?? null);
57
61
  }
62
+ set(currentSt.updateCurrentStatus, evt);
63
+ });
64
+ export const onHistoricalServerEvent = atom(null, (_, set, evt) => {
58
65
  set(simProd.updateLast30JobProduction, evt);
59
66
  set(simUse.updateLast30SimStatUse, evt);
60
67
  set(schJobs.updateLast30Jobs, evt);
61
68
  set(buffers.updateLast30Buffer, evt);
62
- set(currentSt.updateCurrentStatus, evt);
63
69
  set(mats.updateLast30MatSummary, evt);
64
70
  set(insp.updateLast30Inspections, evt);
65
71
  set(names.updateNames, evt);
@@ -72,9 +78,13 @@ export const onServerEvent = atom(null, (_, set, evt) => {
72
78
  set(simDayUsage.updateLatestSimDayUsage, evt);
73
79
  if (evt.evt.logEntry) {
74
80
  const newCntr = evt.evt.logEntry.counter;
75
- set(lastEventCounterRW, (old) => (old === null ? old : Math.max(old, newCntr)));
81
+ set(lastEventCounterRW, (old) => Math.max(old ?? newCntr, newCntr));
76
82
  }
77
83
  });
84
+ export const onServerEvent = atom(null, (_, set, evt) => {
85
+ set(onLiveServerEvent, evt);
86
+ set(onHistoricalServerEvent, evt);
87
+ });
78
88
  export const onLoadLast30Jobs = atom(null, (get, set, historicData) => {
79
89
  const filtered = schJobs.filterExistingJobs(get(schJobs.last30SchIds), historicData);
80
90
  set(simUse.setLast30SimStatUse, filtered);
@@ -104,6 +114,29 @@ export const onLoadCurrentSt = atom(null, (_, set, curSt) => {
104
114
  set(customSt.replaceCustomState, curSt.customState ?? null);
105
115
  set(names.setNamesFromCurrentStatus, curSt);
106
116
  });
117
+ // Show queue drops immediately. Recovery must not replace intervening live updates or drags.
118
+ export const moveQueuedMatInCurrentStatus = atom(null, async (get, set, { operator, ...move }) => {
119
+ const before = get(currentSt.currentStatus);
120
+ set(currentSt.reorderQueuedMatInCurrentStatus, move);
121
+ const optimistic = get(currentSt.currentStatus);
122
+ try {
123
+ await JobsBackend.setMaterialInQueue(move.matId, operator, new QueuePosition({ queue: move.queue, position: move.newIdx }));
124
+ }
125
+ catch (error) {
126
+ const beforeReload = get(currentSt.currentStatus);
127
+ try {
128
+ const refreshed = await JobsBackend.currentStatus();
129
+ if (get(currentSt.currentStatus) === beforeReload)
130
+ set(onLoadCurrentSt, refreshed);
131
+ }
132
+ catch {
133
+ // If the server is unreachable, undo only this move's untouched optimistic snapshot.
134
+ if (get(currentSt.currentStatus) === optimistic)
135
+ set(onLoadCurrentSt, before);
136
+ }
137
+ throw error;
138
+ }
139
+ });
107
140
  export const onLoadSpecificMonthJobs = atom(null, (_, set, historicData) => {
108
141
  set(simUse.setSpecificMonthSimStatUse, historicData);
109
142
  set(simProd.setSpecificMonthJobProduction, historicData);
@@ -44,7 +44,7 @@ export declare const barcodePotentialNewMaterial: Atom<Promise<Readonly<IScanned
44
44
  export declare const materialInDialogInfo: Atom<Promise<MaterialToShowInfo | null>>;
45
45
  export declare const materialInDialogInfoUnwrapped: Atom<MaterialToShowInfo | null>;
46
46
  export declare const materialInDialogInfoState: Atom<AsyncAtomState<MaterialToShowInfo | null>>;
47
- export declare const inProcessMaterialInDialog: Atom<Promise<IInProcessMaterial | null>>;
47
+ export declare const inProcessMaterialInDialog: Atom<IInProcessMaterial | Promise<IInProcessMaterial | null> | null>;
48
48
  export declare const serialInMaterialDialog: Atom<Promise<string | null>>;
49
49
  export declare const serialInMaterialDialogUnwrapped: Atom<string | null>;
50
50
  export declare const serialInMaterialDialogState: Atom<AsyncAtomState<string | null>>;
@@ -61,7 +61,7 @@ export interface MaterialToShowInspections {
61
61
  readonly completedInspections: ReadonlyArray<string>;
62
62
  }
63
63
  export declare const materialInDialogInspections: Atom<MaterialToShowInspections>;
64
- export declare const possibleWorkordersForMaterialInDialog: Atom<Promise<readonly IActiveWorkorder[]>>;
64
+ export declare const possibleWorkordersForMaterialInDialog: Atom<Promise<readonly IActiveWorkorder[]> | readonly IActiveWorkorder[]>;
65
65
  export interface ForceInspectionData {
66
66
  readonly mat: MaterialToShowInfo;
67
67
  readonly inspType: string;
@@ -116,11 +116,26 @@ function asyncAtomState(source) {
116
116
  return unwrap(atom((get) => get(source).then((data) => ({ state: "hasData", data }), (error) => ({ state: "hasError", error }))), () => ({ state: "loading" }));
117
117
  }
118
118
  export const materialInDialogInfoState = asyncAtomState(materialInDialogInfo);
119
- export const inProcessMaterialInDialog = atom(async (get) => {
120
- const status = get(currentStatus);
119
+ // Some cells publish current status every second. Dialog atoms that combine current status with the
120
+ // dialog material must not suspend on each status update, since that hides the dialog content behind
121
+ // its loading fallback. They compute synchronously once the dialog material is known and suspend
122
+ // only while it is still loading (e.g., a barcode or serial lookup).
123
+ function withDialogMaterial(get, compute) {
124
+ const info = get(materialInDialogInfoState);
125
+ switch (info.state) {
126
+ case "hasData":
127
+ return compute(info.data);
128
+ case "hasError":
129
+ throw info.error;
130
+ case "loading":
131
+ return get(materialInDialogInfo).then(compute);
132
+ }
133
+ }
134
+ export const inProcessMaterialInDialog = atom((get) => {
121
135
  const toShow = get(matToShow);
122
136
  if (toShow === null)
123
137
  return null;
138
+ const status = get(currentStatus);
124
139
  if (toShow.type === "InProcMat") {
125
140
  return (status.material.find((m) => m.materialID === toShow.inproc.materialID &&
126
141
  (m.materialID >= 0 ||
@@ -130,10 +145,9 @@ export const inProcessMaterialInDialog = atom(async (get) => {
130
145
  m.action.loadToBasketId === toShow.inproc.action.loadToBasketId &&
131
146
  m.jobUnique === toShow.inproc.jobUnique))) ?? null);
132
147
  }
133
- const matId = (await get(materialInDialogInfo))?.materialID ?? null;
134
- return matId !== null && matId >= 0
135
- ? (status.material.find((m) => m.materialID === matId) ?? null)
136
- : null;
148
+ return withDialogMaterial(get, (mat) => mat !== null && mat.materialID >= 0
149
+ ? (status.material.find((m) => m.materialID === mat.materialID) ?? null)
150
+ : null);
137
151
  });
138
152
  export const serialInMaterialDialog = atom(async (get) => {
139
153
  const toShow = get(matToShow);
@@ -284,14 +298,13 @@ export const materialInDialogInspections = atom((get) => {
284
298
  //--------------------------------------------------------------------------------
285
299
  // Workorders
286
300
  //--------------------------------------------------------------------------------
287
- export const possibleWorkordersForMaterialInDialog = atom(async (get) => {
288
- const mat = await get(materialInDialogInfo);
289
- if (mat === null || mat.partName === "")
290
- return [];
301
+ export const possibleWorkordersForMaterialInDialog = atom((get) => {
291
302
  const works = get(currentStatus)?.workorders ?? [];
292
- return LazySeq.of(works)
293
- .filter((w) => w.part === mat.partName)
294
- .toSortedArray((w) => w.dueDate.getTime(), (w) => -w.priority);
303
+ return withDialogMaterial(get, (mat) => mat === null || mat.partName === ""
304
+ ? []
305
+ : LazySeq.of(works)
306
+ .filter((w) => w.part === mat.partName)
307
+ .toSortedArray((w) => w.dueDate.getTime(), (w) => -w.priority));
295
308
  });
296
309
  export function useForceInspection() {
297
310
  const [updating, setUpdating] = useState(false);
@@ -1,8 +1,6 @@
1
1
  import { ReactNode, ComponentType } from "react";
2
2
  import { ChooseModeItem } from "./ChooseMode.js";
3
3
  import * as serverSettings from "../network/server-settings.js";
4
- import type { SubmitBasketLoadStationCommand } from "./station-monitor/BasketLoadStationWork.js";
5
- import type { SubmitBasketLocationCorrection, SubmitBasketMovementCompletion } from "./station-monitor/BasketMovementArrival.js";
6
4
  export declare function NavTabs({ children }: {
7
5
  children?: ReactNode;
8
6
  }): import("react").JSX.Element;
@@ -12,9 +10,6 @@ export interface AppProps {
12
10
  readonly page: ReactNode;
13
11
  };
14
12
  readonly chooseModes?: (i: serverSettings.FMSInfoAndUser) => ReadonlyArray<ChooseModeItem> | null;
15
- readonly submitBasketLoadStationCommand?: SubmitBasketLoadStationCommand;
16
- readonly submitBasketMovementCompletion?: SubmitBasketMovementCompletion;
17
- readonly submitBasketLocationCorrection?: SubmitBasketLocationCorrection;
18
13
  }
19
14
  declare const App: import("react").MemoExoticComponent<(props: AppProps) => import("react").JSX.Element>;
20
15
  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, submitBasketMovementCompletion: props.submitBasketMovementCompletion, submitBasketLocationCorrection: props.submitBasketLocationCorrection }, route.loadNum));
313
+ page = (_jsx(LoadStation, { loadNum: route.loadNum, queues: route.queues, completed: route.completed }, route.loadNum));
314
314
  nav1 = StationToolbar;
315
315
  nav2 = StationToolbarOverviewButton;
316
316
  showOperator = true;
@@ -74,6 +74,7 @@ const marginBottom = 50;
74
74
  const marginTop = 20;
75
75
  const marginRight = 20;
76
76
  function BufferChartSVG({ width, height, series, disabled, }) {
77
+ const [emptyDate] = useState(() => new Date());
77
78
  const [dateMin, dateMax] = useMemo(() => {
78
79
  let min = null;
79
80
  let max = null;
@@ -87,8 +88,8 @@ function BufferChartSVG({ width, height, series, disabled, }) {
87
88
  }
88
89
  }
89
90
  }
90
- return [min ?? new Date(), max ?? new Date()];
91
- }, [series]);
91
+ return [min ?? emptyDate, max ?? emptyDate];
92
+ }, [series, emptyDate]);
92
93
  const xScale = useMemo(() => scaleTime()
93
94
  .range([0, width - marginLeft - marginRight])
94
95
  .domain([dateMin, dateMax]), [width, dateMin, dateMax]);
@@ -33,8 +33,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
33
  */
34
34
  import { forwardRef, memo, PureComponent, useState, useCallback, useMemo, } from "react";
35
35
  import { selectAllMaterialIntoBins, MaterialBinType, moveMaterialBin, currentMaterialBinOrder, moveMaterialInBin, findMaterialInQuarantineQueues, findQueueInQuarantineQueues, } from "../../data/all-material-bins.js";
36
- import * as matDetails from "../../cell-status/material-details.js";
37
36
  import * as currentSt from "../../cell-status/current-status.js";
37
+ import { moveQueuedMatInCurrentStatus } from "../../cell-status/loading.js";
38
38
  import { Box } from "@mui/material";
39
39
  import { Typography } from "@mui/material";
40
40
  import { LazySeq } from "@seedtactics/immutable-collections";
@@ -48,6 +48,7 @@ import { useSetTitle } from "../routes.js";
48
48
  import { useAtom, useAtomValue, useSetAtom } from "jotai";
49
49
  import { fmsInformation } from "../../network/server-settings.js";
50
50
  import { basketDisplayName, loadStationDisplayName } from "../../cell-status/station-cycles.js";
51
+ import { canSetMaterialInQueue } from "../../data/material-operation-policy.js";
51
52
  const ColumnWithTitle = forwardRef(function MaterialBin(props, ref) {
52
53
  return (_jsxs(Box, { sx: {
53
54
  margin: props.isDragOverlay ? undefined : "0.75em",
@@ -187,8 +188,7 @@ export function AllMaterial(props) {
187
188
  useSetTitle("All Material");
188
189
  const st = useAtomValue(currentSt.currentStatus);
189
190
  const [matBinOrder, setMatBinOrder] = useAtom(currentMaterialBinOrder);
190
- const [addExistingMatToQueue] = matDetails.useAddExistingMaterialToQueue();
191
- const reorderQueuedMat = useSetAtom(currentSt.reorderQueuedMatInCurrentStatus);
191
+ const moveQueuedMat = useSetAtom(moveQueuedMatInCurrentStatus);
192
192
  const [activeDrag, setActiveDrag] = useState(null);
193
193
  const allBins = useMemo(() => {
194
194
  const bins = selectAllMaterialIntoBins(st, matBinOrder);
@@ -232,6 +232,8 @@ export function AllMaterial(props) {
232
232
  : findMaterialInQuarantineQueues(over.id, allBins);
233
233
  if (!overCol)
234
234
  return;
235
+ if (!canSetMaterialInQueue(activeDrag.mat, overCol.bin.queueName))
236
+ return;
235
237
  if (overCol.bin.binId !== activeDrag.curOverBinId) {
236
238
  setActiveDrag({
237
239
  ...activeDrag,
@@ -258,18 +260,13 @@ export function AllMaterial(props) {
258
260
  const overCol = typeof over.id === "string"
259
261
  ? findQueueInQuarantineQueues(over.id, allBins)
260
262
  : findMaterialInQuarantineQueues(over.id, allBins);
261
- if (overCol) {
262
- void addExistingMatToQueue({
263
- materialId: activeDrag.mat.materialID,
264
- queue: overCol.bin.queueName,
265
- queuePosition: overCol.idx,
266
- operator: null,
267
- }).catch(console.error);
268
- reorderQueuedMat({
263
+ if (overCol && canSetMaterialInQueue(activeDrag.mat, overCol.bin.queueName)) {
264
+ moveQueuedMat({
269
265
  queue: overCol.bin.queueName,
270
266
  matId: activeDrag.mat.materialID,
271
267
  newIdx: overCol.idx,
272
- });
268
+ operator: null,
269
+ }).catch(console.error);
273
270
  }
274
271
  }
275
272
  setActiveDrag(null);
@@ -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, useCallback, useState, useRef, useEffect, } from "react";
34
+ import { useMemo, useCallback, useRef } from "react";
35
35
  import { last30StationCycles } from "../../cell-status/station-cycles.js";
36
36
  import { last30EstimatedCycleTimes } from "../../cell-status/estimated-cycle-times.js";
37
37
  import { recentCycles } from "../../data/results.cycles.js";
@@ -43,7 +43,7 @@ import { localPoint } from "../../util/chart-helpers.js";
43
43
  import { Stack } from "@mui/material";
44
44
  import { Tooltip } from "../ChartTooltip.js";
45
45
  import { currentCycles } from "../../data/current-cycles.js";
46
- import { currentStatus } from "../../cell-status/current-status.js";
46
+ import { currentStatus, minutesSinceEpochAtom } from "../../cell-status/current-status.js";
47
47
  import { last30Jobs } from "../../cell-status/scheduled-jobs.js";
48
48
  import { atom, useAtomValue, useSetAtom } from "jotai";
49
49
  import { fmsInformation } from "../../network/server-settings.js";
@@ -56,11 +56,12 @@ const occupiedNonOutlierColor = green[900];
56
56
  const occupiedOutlierColor = red[700];
57
57
  const simColor = grey[400];
58
58
  const downtimeColor = grey[100];
59
- function useSimCycles() {
59
+ const chartTime = atom((get) => Math.floor(get(minutesSinceEpochAtom) / 5) * 5 * 60000);
60
+ function useSimCycles(now) {
60
61
  const jobs = useAtomValue(last30Jobs);
61
62
  const statUse = useAtomValue(last30SimStationUse);
62
63
  return useMemo(() => {
63
- const cutoff = addHours(new Date(), -12);
64
+ const cutoff = addHours(now, -12);
64
65
  return (LazySeq.of(statUse)
65
66
  .filter((s) => s.end >= cutoff)
66
67
  // make sure all planned downtimes come last so they are drawn over the top of any cycles
@@ -81,7 +82,7 @@ function useSimCycles() {
81
82
  .toRArray(),
82
83
  }))
83
84
  .toRArray());
84
- }, [jobs, statUse]);
85
+ }, [jobs, statUse, now]);
85
86
  }
86
87
  const tooltipData = atom(null);
87
88
  const stationFontSize = 14;
@@ -213,30 +214,24 @@ function NowLine({ now, xScale, yScale, }) {
213
214
  export function RecentCycleChart({ height, width }) {
214
215
  const last30Cycles = useAtomValue(last30StationCycles);
215
216
  const estimated = useAtomValue(last30EstimatedCycleTimes);
216
- const sim = useSimCycles();
217
217
  const currentSt = useAtomValue(currentStatus);
218
+ const time = useAtomValue(chartTime);
219
+ // Keep history filtering independent of frequent status updates.
220
+ const historyNow = useMemo(() => new Date(time), [time]);
221
+ // Align the axis and now marker with cycles started after the chart's last clock tick.
222
+ const displayNow = useMemo(() => new Date(Math.max(time, currentSt.timeOfCurrentStatusUTC.getTime())), [time, currentSt.timeOfCurrentStatusUTC]);
223
+ const sim = useSimCycles(historyNow);
218
224
  const fmsInfo = useAtomValue(fmsInformation);
219
225
  const cycles = useMemo(() => {
220
- const cutoff = addHours(new Date(), -12);
226
+ const cutoff = addHours(historyNow, -12);
221
227
  return recentCycles(last30Cycles.valuesToLazySeq().filter((e) => e.endTime >= cutoff));
222
- }, [last30Cycles]);
228
+ }, [last30Cycles, historyNow]);
223
229
  const current = useMemo(() => {
224
230
  return currentCycles(currentSt, estimated, fmsInfo.loadStationNames);
225
231
  }, [currentSt, estimated, fmsInfo.loadStationNames]);
226
- // ensure a re-render at least every 5 minutes, but reset the timer if the data changes
227
- const now = new Date();
228
- const [, forceRerender] = useState(0);
229
- const refreshRef = useRef(null);
230
- useEffect(() => {
231
- if (refreshRef.current !== null)
232
- clearTimeout(refreshRef.current);
233
- refreshRef.current = setTimeout(() => {
234
- forceRerender((x) => x + 1);
235
- }, 5 * 60 * 1000);
236
- });
237
- const { xScale, yScale, actualPlannedScale, marginLeft } = useScales(cycles, current, now, width, height);
232
+ const { xScale, yScale, actualPlannedScale, marginLeft } = useScales(cycles, current, displayNow, width, height);
238
233
  const hideTooltipRef = useRef(null);
239
234
  if (height <= 0 || width <= 0)
240
235
  return null;
241
- return (_jsxs("div", { style: { position: "relative", overflow: "hidden" }, children: [_jsx("svg", { height: height, width: width, children: _jsxs("g", { transform: `translate(${marginLeft}, ${marginTop})`, children: [_jsx("clipPath", { id: "recent-cycle-clip-body", children: _jsx("rect", { x: 0, y: 0, width: width - marginRight - marginLeft, height: height - marginBottom - marginTop }) }), _jsx(AxisAndGrid, { xScale: xScale, yScale: yScale }), _jsxs("g", { clipPath: "url(#recent-cycle-clip-body)", children: [_jsx(RecentSeries, { cycles: cycles, xScale: xScale, yScale: yScale, hideTooltipRef: hideTooltipRef, actualPlannedScale: actualPlannedScale }), _jsx(CurrentSeries, { now: now, cycles: current, xScale: xScale, yScale: yScale, hideTooltipRef: hideTooltipRef, actualPlannedScale: actualPlannedScale }), _jsx(SimSeries, { sim: sim, xScale: xScale, yScale: yScale, actualPlannedScale: actualPlannedScale, hideTooltipRef: hideTooltipRef })] }), _jsx(NowLine, { now: now, xScale: xScale, yScale: yScale })] }) }), _jsx(Tooltip, { chartHeight: height, chartWidth: width, atom: tooltipData, TooltipContent: RecentTooltip })] }));
236
+ return (_jsxs("div", { style: { position: "relative", overflow: "hidden" }, children: [_jsx("svg", { height: height, width: width, children: _jsxs("g", { transform: `translate(${marginLeft}, ${marginTop})`, children: [_jsx("clipPath", { id: "recent-cycle-clip-body", children: _jsx("rect", { x: 0, y: 0, width: width - marginRight - marginLeft, height: height - marginBottom - marginTop }) }), _jsx(AxisAndGrid, { xScale: xScale, yScale: yScale }), _jsxs("g", { clipPath: "url(#recent-cycle-clip-body)", children: [_jsx(RecentSeries, { cycles: cycles, xScale: xScale, yScale: yScale, hideTooltipRef: hideTooltipRef, actualPlannedScale: actualPlannedScale }), _jsx(CurrentSeries, { now: displayNow, cycles: current, xScale: xScale, yScale: yScale, hideTooltipRef: hideTooltipRef, actualPlannedScale: actualPlannedScale }), _jsx(SimSeries, { sim: sim, xScale: xScale, yScale: yScale, actualPlannedScale: actualPlannedScale, hideTooltipRef: hideTooltipRef })] }), _jsx(NowLine, { now: displayNow, xScale: xScale, yScale: yScale })] }) }), _jsx(Tooltip, { chartHeight: height, chartWidth: width, atom: tooltipData, TooltipContent: RecentTooltip })] }));
242
237
  }
@@ -1,15 +1,9 @@
1
1
  import * as api from "../../network/api.js";
2
2
  import { MatCardFontSize } from "./Material.js";
3
- export interface BasketLoadStationCommand {
4
- readonly workId: string;
5
- }
6
- export type SubmitBasketLoadStationCommand = (stationNumber: number, command: BasketLoadStationCommand) => Promise<"accepted" | "conflict">;
7
3
  interface BasketLoadStationWorkflowProps {
8
- readonly stationNumber: number;
9
4
  readonly basket: Readonly<api.IBasketStatus>;
10
5
  readonly material: ReadonlyArray<Readonly<api.IInProcessMaterial>>;
11
6
  readonly fsize: MatCardFontSize;
12
- readonly submitCommand: SubmitBasketLoadStationCommand | undefined;
13
7
  }
14
- export declare function BasketLoadStationWorkflow({ stationNumber, basket, material, fsize, submitCommand, }: BasketLoadStationWorkflowProps): import("react").JSX.Element;
8
+ export declare function BasketLoadStationWorkflow({ basket, material, fsize, }: BasketLoadStationWorkflowProps): import("react").JSX.Element;
15
9
  export {};
@@ -33,6 +33,7 @@ import { Alert, Box, Button, Stack, Typography } from "@mui/material";
33
33
  import { LazySeq } from "@seedtactics/immutable-collections";
34
34
  import { basketSlotLabel } from "../../cell-status/station-cycles.js";
35
35
  import * as api from "../../network/api.js";
36
+ import { JobsBackend } from "../../network/backend.js";
36
37
  import { InProcMaterial } from "./Material.js";
37
38
  import { MoveMaterialArrowNode } from "./MoveMaterialArrows.js";
38
39
  import { MoveMaterialNodeKindType } from "../../data/move-arrows.js";
@@ -138,7 +139,7 @@ function loadingInstruction(mat) {
138
139
  function SlotMaterial({ material, fsize, }) {
139
140
  return material.map((mat) => (_jsx(MoveMaterialArrowNode, { kind: { type: MoveMaterialNodeKindType.Material, material: mat }, children: _jsx(InProcMaterial, { mat: mat, fsize: fsize }) }, mat.materialID)));
140
141
  }
141
- export function BasketLoadStationWorkflow({ stationNumber, basket, material, fsize, submitCommand, }) {
142
+ export function BasketLoadStationWorkflow({ basket, material, fsize, }) {
142
143
  const [submission, setSubmission] = useState();
143
144
  const workState = useMemo(() => stationWork(material, basket), [basket, material]);
144
145
  const blockedReason = basket.loadStationWork?.confirmationBlockedReason?.trim();
@@ -170,17 +171,19 @@ export function BasketLoadStationWorkflow({ stationNumber, basket, material, fsi
170
171
  .distinctBy((slot) => slot)
171
172
  .toSortedArray((slot) => slot), [basket.emptySlots, basket.unknownSlots, materialBySlot, loadsBySlot]);
172
173
  async function submit() {
173
- if (work === undefined || !work.ready || submitCommand === undefined)
174
+ if (work === undefined || !work.ready)
174
175
  return;
175
176
  const submittedWorkId = work.workId;
176
177
  const pending = { workId: submittedWorkId, basket, material, state: "submitting" };
177
178
  setSubmission(pending);
178
179
  try {
179
- const state = await submitCommand(stationNumber, { workId: submittedWorkId });
180
+ await JobsBackend.completeBasketLoadStation(new api.BasketLoadStationCompletion({ workId: submittedWorkId }));
181
+ const state = "accepted";
180
182
  setSubmission((current) => (current === pending ? { ...pending, state } : current));
181
183
  }
182
- catch {
183
- setSubmission((current) => (current === pending ? { ...pending, state: "error" } : current));
184
+ catch (error) {
185
+ const state = api.ApiException.isApiException(error) && error.status === 409 ? "conflict" : "error";
186
+ setSubmission((current) => (current === pending ? { ...pending, state } : current));
184
187
  }
185
188
  }
186
189
  const submissionDisabled = !work?.ready ||
@@ -205,5 +208,5 @@ export function BasketLoadStationWorkflow({ stationNumber, basket, material, fsi
205
208
  }) }), work?.confirmEmpty && _jsxs(Typography, { children: ["Confirm basket ", basket.basketId, " is empty."] }), work && !work.ready && (_jsx(Alert, { severity: "info", children: blockedReason ||
206
209
  (work.awaitingSlots.length > 0
207
210
  ? `Waiting for material for slots ${work.awaitingSlots.map(basketSlotLabel).join(", ")}.`
208
- : "Basket work is not ready for confirmation.") })), 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] }));
211
+ : "Basket work is not ready for confirmation.") })), work ? (_jsx(Box, { sx: { display: "flex", justifyContent: "flex-end" }, children: _jsx(Button, { variant: "contained", disabled: submissionDisabled, onClick: () => void submit(), children: "Confirm" }) })) : 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] }));
209
212
  }
@@ -41,6 +41,8 @@ export function CancelLoadButton({ onClose, ignoreOperator = false, }) {
41
41
  expectedLoadCancellationId: snapshot.cancellationId,
42
42
  reason: reason || undefined,
43
43
  }));
44
+ setOpen(false);
45
+ setSnapshot(null);
44
46
  setMaterialDialogOpen(null);
45
47
  onClose?.();
46
48
  }
@@ -41,7 +41,7 @@ import { currentStatus } from "../../cell-status/current-status.js";
41
41
  import { barcodePotentialNewMaterial, inProcessMaterialInDialog, materialDialogOpen, materialInDialogInfo, materialInDialogLargestUsedProcess, materialInDialogLocalEvents, } from "../../cell-status/material-details.js";
42
42
  import { currentOperator } from "../../data/operators.js";
43
43
  import { fmsInformation } from "../../network/server-settings.js";
44
- import { useAtomValue, useSetAtom } from "jotai";
44
+ import { useAtomValue, useAtomValueRawSync, useSetAtom } from "jotai";
45
45
  import { useEffect, useRef } from "react";
46
46
  import { last30Jobs } from "../../cell-status/scheduled-jobs.js";
47
47
  import { PartIdenticon } from "./Material.js";
@@ -82,12 +82,15 @@ function InvalidateSelect(props) {
82
82
  job: currentSt.jobs[j.jobUnique] ?? history.get(j.jobUnique) ?? null,
83
83
  }));
84
84
  const hasProc = lastMat !== null && lastMat.process >= 1;
85
- const hasChangeMat = castings.length > 0 || jobs.length > 0;
85
+ const canChangeAssignment = lastMat === null || lastMat.process <= 1;
86
+ const hasChangeMat = canChangeAssignment && (castings.length > 0 || jobs.length > 0);
86
87
  const selectedCasting = props.st?.changeRawMat !== null &&
87
88
  props.st?.changeRawMat !== undefined &&
89
+ canChangeAssignment &&
88
90
  castings.includes(props.st.changeRawMat);
89
91
  const selectedJob = props.st?.changeJobUnique !== null &&
90
92
  props.st?.changeJobUnique !== undefined &&
93
+ canChangeAssignment &&
91
94
  jobs.some((job) => job.jobUnique === props.st?.changeJobUnique);
92
95
  function change(e) {
93
96
  props.setState({
@@ -102,7 +105,7 @@ function InvalidateSelect(props) {
102
105
  ? "rawMat" + props.st.changeRawMat
103
106
  : selectedJob
104
107
  ? "job" + props.st.changeJobUnique
105
- : props.st?.process && hasProc
108
+ : props.st?.process && hasProc && props.st.process === lastMat.process
106
109
  ? "proc" + props.st.process.toString()
107
110
  : "", select: true, onChange: (e) => change(e.target.value), variant: "outlined", label: props.st?.changeRawMat
108
111
  ? "Change Raw Material"
@@ -119,11 +122,6 @@ function InvalidateSelect(props) {
119
122
  hasProc
120
123
  ? [
121
124
  _jsxs(MenuItem, { value: "proc" + lastMat.process.toString(), children: ["Invalidate Process ", lastMat.process] }, lastMat.process),
122
- ...(lastMat.process > 1
123
- ? [
124
- _jsx(MenuItem, { value: "proc1", children: "Invalidate All Processes" }, "all"),
125
- ]
126
- : []),
127
125
  ]
128
126
  : [],
129
127
  hasProc && hasChangeMat
@@ -131,15 +129,17 @@ function InvalidateSelect(props) {
131
129
  : [],
132
130
  hasChangeMat
133
131
  ? [
134
- ...castings.map((c) => (_jsxs(MenuItem, { value: "rawMat" + c, children: ["Invalidate Everything and Change to ", c] }, "rawMat" + c))),
135
- ...jobs.map(({ jobUnique, job }) => (_jsxs(MenuItem, { value: "job" + jobUnique, children: [job ? (_jsx(ListItemIcon, { children: _jsx(PartIdenticon, { part: job.partName }) })) : undefined, _jsx(ListItemText, { primary: "Invalidate Everything and Change to Job " + jobUnique, secondary: job?.partName })] }, "job" + jobUnique))),
132
+ ...castings.map((c) => (_jsxs(MenuItem, { value: "rawMat" + c, children: [hasProc ? "Invalidate Process 1 and Change to " : "Change to ", c] }, "rawMat" + c))),
133
+ ...jobs.map(({ jobUnique, job }) => (_jsxs(MenuItem, { value: "job" + jobUnique, children: [job ? (_jsx(ListItemIcon, { children: _jsx(PartIdenticon, { part: job.partName }) })) : undefined, _jsx(ListItemText, { primary: (hasProc ? "Invalidate Process 1 and Change to Job " : "Change to Job ") +
134
+ jobUnique, secondary: job?.partName })] }, "job" + jobUnique))),
136
135
  ]
137
136
  : [],
138
137
  ] }));
139
138
  }
140
139
  export function InvalidateCycleDialogContent(props) {
141
140
  const curMat = useAtomValue(materialInDialogInfo);
142
- const events = useAtomValue(materialInDialogLocalEvents);
141
+ // History can resolve while InvalidateSelect suspends, before this component subscribes.
142
+ const events = useAtomValueRawSync(materialInDialogLocalEvents);
143
143
  const status = useAtomValue(currentStatus);
144
144
  const show = props.st !== null && curMat !== null;
145
145
  const boxRef = useRef(null);
@@ -182,13 +182,15 @@ export function InvalidateCycleDialogButton(props) {
182
182
  return null;
183
183
  if (!canInvalidateMaterial(inProcMat))
184
184
  return null;
185
- const allowChange = possibleNew !== null &&
185
+ const allowChange = (lastMat === null || lastMat.process <= 1) &&
186
+ possibleNew !== null &&
186
187
  (!LazySeq.ofObject(possibleNew.possibleCastingsByQueue ?? {}).isEmpty() ||
187
188
  !LazySeq.ofObject(possibleNew.possibleJobsByQueue ?? {}).isEmpty());
188
189
  if (!allowChange && (lastMat === null || lastMat.process < 1))
189
190
  return null;
190
191
  if (props.ignoreOperator)
191
192
  operator = null;
193
+ const changingAssignment = props.st !== null && (props.st.changeRawMat !== null || props.st.changeJobUnique !== null);
192
194
  function showInvalidate() {
193
195
  props.setState({
194
196
  process: null,
@@ -229,17 +231,20 @@ export function InvalidateCycleDialogButton(props) {
229
231
  }
230
232
  }
231
233
  return (_jsxs(_Fragment, { children: [props.st === null ? (_jsx(Button, { color: "primary", onClick: showInvalidate, children: "Invalidate Cycle" })) : undefined, props.st !== null ? (_jsx(Button, { color: "primary", onClick: invalidateCycle, disabled: props.st.updating ||
234
+ (changingAssignment && !allowChange) ||
235
+ (!changingAssignment &&
236
+ props.st.process !== null &&
237
+ props.st.process !== lastMat?.process) ||
232
238
  (props.st.process === null &&
233
239
  props.st.changeRawMat === null &&
234
240
  props.st.changeJobUnique === null), children: props.st.changeJobUnique !== null
235
- ? "Invalidate Everything and Change to Job " + props.st.changeJobUnique
241
+ ? (lastMat?.process === 1
242
+ ? "Invalidate Process 1 and Change to Job "
243
+ : "Change to Job ") + props.st.changeJobUnique
236
244
  : props.st.changeRawMat !== null
237
- ? "Invalidate Everything and Change to " + props.st.changeRawMat
245
+ ? (lastMat?.process === 1 ? "Invalidate Process 1 and Change to " : "Change to ") +
246
+ props.st.changeRawMat
238
247
  : props.st.process !== null
239
- ? props.st.process === 1
240
- ? lastMat && lastMat.process > 1
241
- ? "Invalidate All Processes"
242
- : "Invalidate Process 1"
243
- : "Invalidate Process " + props.st.process.toString()
248
+ ? "Invalidate Process " + props.st.process.toString()
244
249
  : "Invalidate Cycle" })) : undefined] }));
245
250
  }
@@ -1,14 +1,9 @@
1
1
  import { ReactNode } from "react";
2
- import { SubmitBasketLoadStationCommand } from "./BasketLoadStationWork.js";
3
- import { SubmitBasketLocationCorrection, SubmitBasketMovementCompletion } from "./BasketMovementArrival.js";
4
2
  interface LoadStationProps {
5
3
  readonly loadNum: number;
6
4
  readonly queues: ReadonlyArray<string>;
7
5
  readonly completed: boolean;
8
6
  readonly whiteBackground?: boolean;
9
- readonly submitBasketLoadStationCommand?: SubmitBasketLoadStationCommand;
10
- readonly submitBasketMovementCompletion?: SubmitBasketMovementCompletion;
11
- readonly submitBasketLocationCorrection?: SubmitBasketLocationCorrection;
12
7
  }
13
8
  export declare function LoadStation(props: LoadStationProps): import("react").JSX.Element;
14
9
  declare function LoadStationCheckWidth(props: LoadStationProps): ReactNode;