@seedtactics/insight-client 17.0.1-beta.12 → 17.0.1-beta.13

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.
@@ -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) => {
@@ -6,6 +6,12 @@ export interface ServerEventAndTime {
6
6
  readonly expire: boolean;
7
7
  }
8
8
  export declare const lastEventCounter: Atom<number | null>;
9
+ export declare const onLiveServerEvent: import("jotai").WritableAtom<null, [evt: ServerEventAndTime], void> & {
10
+ init: null;
11
+ };
12
+ export declare const onHistoricalServerEvent: import("jotai").WritableAtom<null, [evt: ServerEventAndTime], void> & {
13
+ init: null;
14
+ };
9
15
  export declare const onServerEvent: import("jotai").WritableAtom<null, [evt: ServerEventAndTime], void> & {
10
16
  init: null;
11
17
  };
@@ -50,16 +50,20 @@ import * as simDayUsage from "./sim-day-usage.js";
50
50
  import * as rebookings from "./rebookings.js";
51
51
  import { atom } from "jotai";
52
52
  const lastEventCounterRW = atom(null);
53
+ // Highest counter applied to historical projections. Buffered websocket events do not advance it.
54
+ // This is a reconnect cursor, not proof that lower counters cannot arrive later on the socket.
53
55
  export const lastEventCounter = lastEventCounterRW;
54
- export const onServerEvent = atom(null, (_, set, evt) => {
56
+ export const onLiveServerEvent = atom(null, (_, set, evt) => {
55
57
  if (evt.evt.newCurrentStatus) {
56
58
  set(customSt.replaceCustomState, evt.evt.newCurrentStatus.customState ?? null);
57
59
  }
60
+ set(currentSt.updateCurrentStatus, evt);
61
+ });
62
+ export const onHistoricalServerEvent = atom(null, (_, set, evt) => {
58
63
  set(simProd.updateLast30JobProduction, evt);
59
64
  set(simUse.updateLast30SimStatUse, evt);
60
65
  set(schJobs.updateLast30Jobs, evt);
61
66
  set(buffers.updateLast30Buffer, evt);
62
- set(currentSt.updateCurrentStatus, evt);
63
67
  set(mats.updateLast30MatSummary, evt);
64
68
  set(insp.updateLast30Inspections, evt);
65
69
  set(names.updateNames, evt);
@@ -72,9 +76,13 @@ export const onServerEvent = atom(null, (_, set, evt) => {
72
76
  set(simDayUsage.updateLatestSimDayUsage, evt);
73
77
  if (evt.evt.logEntry) {
74
78
  const newCntr = evt.evt.logEntry.counter;
75
- set(lastEventCounterRW, (old) => (old === null ? old : Math.max(old, newCntr)));
79
+ set(lastEventCounterRW, (old) => Math.max(old ?? newCntr, newCntr));
76
80
  }
77
81
  });
82
+ export const onServerEvent = atom(null, (_, set, evt) => {
83
+ set(onLiveServerEvent, evt);
84
+ set(onHistoricalServerEvent, evt);
85
+ });
78
86
  export const onLoadLast30Jobs = atom(null, (get, set, historicData) => {
79
87
  const filtered = schJobs.filterExistingJobs(get(schJobs.last30SchIds), historicData);
80
88
  set(simUse.setLast30SimStatUse, filtered);
@@ -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);
@@ -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]);
@@ -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
  }
@@ -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";
@@ -139,7 +139,8 @@ function InvalidateSelect(props) {
139
139
  }
140
140
  export function InvalidateCycleDialogContent(props) {
141
141
  const curMat = useAtomValue(materialInDialogInfo);
142
- const events = useAtomValue(materialInDialogLocalEvents);
142
+ // History can resolve while InvalidateSelect suspends, before this component subscribes.
143
+ const events = useAtomValueRawSync(materialInDialogLocalEvents);
143
144
  const status = useAtomValue(currentStatus);
144
145
  const show = props.st !== null && curMat !== null;
145
146
  const boxRef = useRef(null);
@@ -48,7 +48,7 @@ import { instructionUrl } from "../../network/backend.js";
48
48
  import { Tooltip } from "@mui/material";
49
49
  import { Fab } from "@mui/material";
50
50
  import { fmsInformation } from "../../network/server-settings.js";
51
- import { currentStatus, secondsSinceEpochAtom } from "../../cell-status/current-status.js";
51
+ import { currentStatus, secondsSinceEpochAtom, minutesSinceEpochAtom, } from "../../cell-status/current-status.js";
52
52
  import { useIsDemo, useSetTitle } from "../routes.js";
53
53
  import { QuarantineMatButton } from "./QuarantineButton.js";
54
54
  import { CancelLoadButton } from "./CancelLoadButton.js";
@@ -487,16 +487,17 @@ function MaterialColumn({ data, region, }) {
487
487
  }
488
488
  }
489
489
  function RecentCompletedMaterial() {
490
+ const minutes = useAtomValue(minutesSinceEpochAtom);
490
491
  const matSummary = useAtomValue(last30MaterialSummary);
491
492
  const recentCompleted = useMemo(() => {
492
- const cutoff = addHours(new Date(), -5);
493
+ const cutoff = addHours(new Date(minutes * 60000), -5);
493
494
  return matSummary.matsById
494
495
  .valuesToLazySeq()
495
496
  .filter((e) => e.completed_last_proc_machining === true &&
496
497
  e.last_unload_time !== undefined &&
497
498
  e.last_unload_time >= cutoff)
498
499
  .toSortedArray({ desc: (e) => e.last_unload_time?.getTime() ?? 0 });
499
- }, [matSummary]);
500
+ }, [matSummary, minutes]);
500
501
  return (_jsx("div", { children: recentCompleted.map((m, idx) => (_jsx(MatSummary, { mat: m }, idx))) }));
501
502
  }
502
503
  const CompletedCol = memo(function CompletedCol({ fillViewPort, showMaterial, }) {
@@ -34,7 +34,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
34
  import { useRef, useCallback, useEffect, useMemo, Fragment } from "react";
35
35
  import JsBarcode from "jsbarcode";
36
36
  import { LazySeq } from "@seedtactics/immutable-collections";
37
- import { currentStatus } from "../../cell-status/current-status.js";
37
+ import { currentStatus, secondsSinceEpochAtom } from "../../cell-status/current-status.js";
38
38
  import { useAtomValue } from "jotai";
39
39
  import { useReactToPrint } from "react-to-print";
40
40
  import { Button } from "@mui/material";
@@ -60,16 +60,20 @@ function Barcode(props) {
60
60
  }, [props.text]);
61
61
  return _jsx("svg", { ref: setRef });
62
62
  }
63
+ function LabelDate() {
64
+ const seconds = useAtomValue(secondsSinceEpochAtom);
65
+ return new Date(seconds * 1000).toLocaleString(undefined, {
66
+ weekday: "long",
67
+ month: "long",
68
+ day: "numeric",
69
+ year: "numeric",
70
+ hour: "numeric",
71
+ minute: "2-digit",
72
+ second: "2-digit",
73
+ });
74
+ }
63
75
  function SinglePage(props) {
64
- return (_jsxs("div", { children: [_jsx("div", { style: { marginTop: "4em", display: "flex", justifyContent: "center" }, children: _jsx("h2", { children: "SeedTactic: FMS Insight" }) }), _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: _jsx("p", { style: { fontSize: "x-large" }, children: new Date().toLocaleString(undefined, {
65
- weekday: "long",
66
- month: "long",
67
- day: "numeric",
68
- year: "numeric",
69
- hour: "numeric",
70
- minute: "2-digit",
71
- second: "2-digit",
72
- }) }) }), _jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.materialName ?? props.partName }), props.count >= 2 ? _jsx(Barcode, { text: props.count.toString() }) : undefined] }), props.serial1 && props.serial1 !== "" ? (_jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.serial1 }), props.serial2 && props.serial2 !== "" ? (_jsx(Barcode, { text: props.serial2 || "" })) : undefined] })) : undefined, _jsx("div", { style: { marginTop: "2em", marginLeft: "4em", marginRight: "4em", marginBottom: "9em" }, children: props.uniq === null || props.uniq === "" ? (_jsx("p", { style: { fontSize: "x-large" }, children: "Not currently assigned to any jobs" })) : (_jsxs(_Fragment, { children: [_jsx("p", { style: { fontSize: "x-large" }, children: "Assigned To" }), _jsxs("div", { style: { marginTop: "1em", marginLeft: "2em", display: "flex", alignItems: "center" }, children: [_jsx(Barcode, { text: props.partName }), _jsxs("h3", { style: { marginLeft: "4em" }, children: ["assigned to ", props.uniq, props.jobCycles && props.jobCycles > 0
76
+ return (_jsxs("div", { children: [_jsx("div", { style: { marginTop: "4em", display: "flex", justifyContent: "center" }, children: _jsx("h2", { children: "SeedTactic: FMS Insight" }) }), _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: _jsx("p", { style: { fontSize: "x-large" }, children: _jsx(LabelDate, {}) }) }), _jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.materialName ?? props.partName }), props.count >= 2 ? _jsx(Barcode, { text: props.count.toString() }) : undefined] }), props.serial1 && props.serial1 !== "" ? (_jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.serial1 }), props.serial2 && props.serial2 !== "" ? (_jsx(Barcode, { text: props.serial2 || "" })) : undefined] })) : undefined, _jsx("div", { style: { marginTop: "2em", marginLeft: "4em", marginRight: "4em", marginBottom: "9em" }, children: props.uniq === null || props.uniq === "" ? (_jsx("p", { style: { fontSize: "x-large" }, children: "Not currently assigned to any jobs" })) : (_jsxs(_Fragment, { children: [_jsx("p", { style: { fontSize: "x-large" }, children: "Assigned To" }), _jsxs("div", { style: { marginTop: "1em", marginLeft: "2em", display: "flex", alignItems: "center" }, children: [_jsx(Barcode, { text: props.partName }), _jsxs("h3", { style: { marginLeft: "4em" }, children: ["assigned to ", props.uniq, props.jobCycles && props.jobCycles > 0
73
77
  ? " (" + props.jobCycles.toString() + " scheduled)"
74
78
  : ""] })] })] })) }), props.note ? (_jsx("div", { style: { marginBottom: "2em" }, children: _jsx("p", { style: { fontSize: "xxx-large", textAlign: "center" }, children: props.note }) })) : undefined, _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: props.operator ? _jsx("p", { style: { fontSize: "x-large" }, children: props.operator }) : undefined })] }));
75
79
  }
@@ -114,15 +118,7 @@ function CombinedToOnePage(props) {
114
118
  if (!props.material || props.material.length === 0) {
115
119
  return _jsx("div", {});
116
120
  }
117
- return (_jsxs("div", { children: [_jsx("div", { style: { marginTop: "4em", display: "flex", justifyContent: "center" }, children: _jsx("h2", { children: "SeedTactic: FMS Insight" }) }), _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: _jsx("p", { style: { fontSize: "x-large" }, children: new Date().toLocaleString(undefined, {
118
- weekday: "long",
119
- month: "long",
120
- day: "numeric",
121
- year: "numeric",
122
- hour: "numeric",
123
- minute: "2-digit",
124
- second: "2-digit",
125
- }) }) }), _jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.materialName ?? props.material[0].partName }), props.material.length >= 2 ? (_jsx(Barcode, { text: props.material.length.toString() })) : undefined] }), props.material[0].serial && props.material[0].serial !== "" ? (_jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.material[0].serial }), props.material.length >= 2 &&
121
+ return (_jsxs("div", { children: [_jsx("div", { style: { marginTop: "4em", display: "flex", justifyContent: "center" }, children: _jsx("h2", { children: "SeedTactic: FMS Insight" }) }), _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: _jsx("p", { style: { fontSize: "x-large" }, children: _jsx(LabelDate, {}) }) }), _jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.materialName ?? props.material[0].partName }), props.material.length >= 2 ? (_jsx(Barcode, { text: props.material.length.toString() })) : undefined] }), props.material[0].serial && props.material[0].serial !== "" ? (_jsxs("div", { style: { marginTop: "2em", display: "flex", justifyContent: "space-around" }, children: [_jsx(Barcode, { text: props.material[0].serial }), props.material.length >= 2 &&
126
122
  props.material[props.material.length - 1].serial &&
127
123
  props.material[props.material.length - 1].serial !== "" ? (_jsx(Barcode, { text: props.material[props.material.length - 1].serial || "" })) : undefined] })) : undefined, _jsx("div", { style: { marginTop: "2em", marginLeft: "4em", marginRight: "4em", marginBottom: "9em" }, children: assignments.length === 0 ? (_jsx("p", { style: { fontSize: "x-large" }, children: "Not currently assigned to any jobs" })) : props.materialName ? (_jsxs(_Fragment, { children: [_jsx("p", { style: { fontSize: "x-large" }, children: "Assigned To" }), assignments.map(([uniq, part], idx) => (_jsxs("div", { style: {
128
124
  marginTop: "1em",
@@ -36,7 +36,7 @@ import { Paper, ButtonBase, Box, Typography, Collapse, Stack, Menu, MenuItem, Li
36
36
  import { Close as CloseIcon } from "@mui/icons-material";
37
37
  import { InProcMaterial, MaterialAction, MaterialDialog, PartIdenticon } from "./Material.js";
38
38
  import { ActionType, BasketLocationEnum, LocType, PalletLocationEnum, } from "../../network/api.js";
39
- import { currentStatus, secondsSinceEpochAtom } from "../../cell-status/current-status.js";
39
+ import { currentStatus, secondsSinceEpochAtom, minutesSinceEpochAtom, } from "../../cell-status/current-status.js";
40
40
  import { LazySeq, OrderedMap } from "@seedtactics/immutable-collections";
41
41
  import { materialDialogOpen } from "../../cell-status/material-details.js";
42
42
  import { last30Jobs } from "../../cell-status/scheduled-jobs.js";
@@ -53,6 +53,7 @@ import { basketDisplayName, loadStationDisplayName } from "../../cell-status/sta
53
53
  const CollapsedIconSize = 45;
54
54
  const rowSize = CollapsedIconSize + 10; // each material row has 5px above and 5px below for padding
55
55
  function useCellOverview() {
56
+ const minutes = useAtomValue(minutesSinceEpochAtom);
56
57
  const currentSt = useAtomValue(currentStatus);
57
58
  const jobs = useAtomValue(last30Jobs);
58
59
  const fmsInfo = useAtomValue(fmsInformation);
@@ -128,7 +129,7 @@ function useCellOverview() {
128
129
  .toOrderedMap((p) => [p.pallet.palletNum, p]);
129
130
  let maxLoadNum = 1;
130
131
  let maxNumFacesOnPallet = 1;
131
- const cutoff = addDays(new Date(), -7);
132
+ const cutoff = addDays(new Date(minutes * 60000), -7);
132
133
  // now add empty locations
133
134
  // Materialize ProcessInfo so we can iterate it at two levels (proc for basket stations, paths for the rest)
134
135
  const allProcs = jobs
@@ -4,6 +4,7 @@ import type { AppProps } from "./components/App.js";
4
4
  */
5
5
  export declare function bootstrapInsight(appProps?: AppProps, rootElement?: HTMLElement): Promise<void>;
6
6
  export { customState } from "./cell-status/custom-state.js";
7
+ export { currentStatusIsLive } from "./network/websocket.js";
7
8
  export type { AppProps };
8
9
  export { defaultChooseModes } from "./components/ChooseMode.js";
9
10
  export type { ChooseModeItem } from "./components/ChooseMode.js";
package/dist/extension.js CHANGED
@@ -16,6 +16,7 @@ export async function bootstrapInsight(appProps = {}, rootElement) {
16
16
  render(appProps, root, store);
17
17
  }
18
18
  export { customState } from "./cell-status/custom-state.js";
19
+ export { currentStatusIsLive } from "./network/websocket.js";
19
20
  export { defaultChooseModes } from "./components/ChooseMode.js";
20
21
  export { RouteLocation } from "./components/routes.js";
21
22
  export { authenticatedFetch } from "./network/backend.js";
package/dist/index.html CHANGED
@@ -5,45 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
6
6
  <link rel="shortcut icon" href="/assets/favicon-gWFxiJK2.ico" />
7
7
  <title>SeedTactic FMS Insight</title>
8
- <style type="text/css">
9
- .download-loader {
10
- width: 100px;
11
- height: 100px;
12
- display: inline-block;
13
- }
14
- .download-circular {
15
- width: 100px;
16
- height: 100px;
17
- animation: rotate 2s linear infinite;
18
- }
19
- .download-path {
20
- stroke-dasharray: 1, 200;
21
- stroke-dashoffset: 0;
22
- animation: dash 1.5s ease-out infinite;
23
- stroke-linecap: round;
24
- stroke: #8d6e63;
25
- }
26
- @keyframes rotate {
27
- 100% {
28
- transform: rotate(360deg);
29
- }
30
- }
31
- @keyframes dash {
32
- 0% {
33
- stroke-dasharray: 1, 200;
34
- stroke-dashoffset: 0;
35
- }
36
- 50% {
37
- stroke-dasharray: 89, 200;
38
- stroke-dashoffset: -35px;
39
- }
40
- 100% {
41
- stroke-dasharray: 89, 200;
42
- stroke-dashoffset: -124px;
43
- }
44
- }
45
- </style>
46
- <script type="module" crossorigin src="/assets/index-kk-GRa5i.js"></script>
8
+ <style type="text/css">.download-loader{width:100px;height:100px;display:inline-block}.download-circular{width:100px;height:100px;animation:2s linear infinite rotate}.download-path{stroke-dasharray:1 200;stroke-dashoffset:0;stroke-linecap:round;stroke:#8d6e63;animation:1.5s ease-out infinite dash}@keyframes rotate{to{transform:rotate(360deg)}}@keyframes dash{0%{stroke-dasharray:1 200;stroke-dashoffset:0}50%{stroke-dasharray:89 200;stroke-dashoffset:-35px}to{stroke-dasharray:89 200;stroke-dashoffset:-124px}}</style>
9
+ <script type="module" crossorigin src="/assets/index-Dxxgr-QV.js"></script>
47
10
  <link rel="stylesheet" crossorigin href="/assets/index-DXfAwuj-.css">
48
11
  </head>
49
12
  <body>
@@ -1,4 +1,5 @@
1
1
  import { Atom } from "jotai";
2
2
  export declare const websocketReconnecting: Atom<boolean>;
3
+ export declare const currentStatusIsLive: Atom<boolean>;
3
4
  export declare const errorLoadingLast30: Atom<string | null>;
4
5
  export declare function WebsocketConnection(): null;