Skip to content

Commit

Permalink
client: start on rebookings page
Browse files Browse the repository at this point in the history
  • Loading branch information
wuzzeb committed Oct 29, 2024
1 parent deec933 commit 7545ec3
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 2 deletions.
4 changes: 4 additions & 0 deletions client/insight/src/cell-status/loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import * as palCycles from "./pallet-cycles.js";
import * as statCycles from "./station-cycles.js";
import * as toolReplace from "./tool-replacements.js";
import * as simDayUsage from "./sim-day-usage.js";
import * as rebookings from "./rebookings.js";
import { Atom, atom } from "jotai";

export interface ServerEventAndTime {
Expand All @@ -78,6 +79,7 @@ export const onServerEvent = atom(null, (_, set, evt: ServerEventAndTime) => {
set(palCycles.updateLast30PalletCycles, evt);
set(toolReplace.updateLastToolReplacements, evt);
set(statCycles.updateLast30StationCycles, evt);
set(rebookings.updateLast30Rebookings, evt);
set(simDayUsage.updateLatestSimDayUsage, evt);

if (evt.evt.logEntry) {
Expand All @@ -93,6 +95,7 @@ export const onLoadLast30Jobs = atom(null, (get, set, historicData: Readonly<IRe
set(schJobs.setLast30Jobs, filtered);
set(names.setNamesFromLast30Jobs, filtered);
set(simDayUsage.setLatestSimDayUsage, filtered);
set(rebookings.setLast30RebookingJobs, filtered);
});

export const onLoadLast30Log = atom(null, (_, set, log: ReadonlyArray<Readonly<ILogEntry>>) => {
Expand All @@ -105,6 +108,7 @@ export const onLoadLast30Log = atom(null, (_, set, log: ReadonlyArray<Readonly<I
set(palCycles.setLast30PalletCycles, log);
set(toolReplace.setLast30ToolReplacements, log);
set(statCycles.setLast30StationCycles, log);
set(rebookings.setLast30Rebookings, log);

const newCntr = LazySeq.of(log).maxBy((x) => x.counter)?.counter ?? null;
set(lastEventCounterRW, (oldCntr) => (oldCntr === null ? newCntr : Math.max(oldCntr, newCntr ?? -1)));
Expand Down
171 changes: 171 additions & 0 deletions client/insight/src/cell-status/rebookings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/* Copyright (c) 2024, John Lenz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of John Lenz, Black Maple Software, SeedTactics,
nor the names of other contributors may be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import { Atom, atom, Setter } from "jotai";
import { ServerEventAndTime } from "./loading";
import { IJob, ILogEntry, IRebooking, IRecentHistoricData, LogType, MaterialDetails } from "../network/api";
import { LazySeq, OrderedMap, OrderedSet } from "@seedtactics/immutable-collections";
import { JobsBackend } from "../network/backend";
import { loadable } from "jotai/utils";
import { addDays } from "date-fns";

const rebookingEvts = atom(OrderedMap.empty<string, Readonly<IRebooking>>());

// unscheduled rebookings should be loaded from the last 30 events, but it is
// possible for some to be older than 30 days, so load them here. But don't
// wait for them to be loaded before rendering, so everything is wrapped in loadable.
const unschRebookings = loadable(
atom(async (_, { signal }) => {
const b = await JobsBackend.unscheduledRebookings(signal);
return OrderedMap.build(b, (b) => b.bookingId);
}),
);

export const last30Rebookings: Atom<OrderedMap<string, Readonly<IRebooking>>> = atom((get) => {
const evts = get(rebookingEvts);
const unsch = get(unschRebookings);

if (unsch.state === "hasData") {
return evts.union(unsch.data, (fromEvt, fromUnsch) => fromEvt ?? fromUnsch);
} else {
return evts;
}
});

const canceledRebookingsRW = atom(OrderedSet.empty<string>());
export const canceledRebookings: Atom<OrderedSet<string>> = canceledRebookingsRW;

type ScheduledBooking = {
readonly scheduledTime: Date;
readonly jobUnique: string;
};

const scheduledRW = atom(OrderedMap.empty<string, ScheduledBooking>());
export const last30ScheduledBookings: Atom<OrderedMap<string, ScheduledBooking>> = scheduledRW;

function convertLogToRebooking(log: Readonly<ILogEntry>): Readonly<IRebooking> {
let qty = 1;
if (log.details?.["Quantity"]) {
qty = parseInt(log.details["Quantity"], 10);
}
let mat: MaterialDetails | undefined;
if (log.material?.[0].id >= 0) {
const m = log.material[0];
mat = new MaterialDetails({
materialID: m.id,
jobUnique: m.uniq,
partName: m.part,
numProcesses: m.numproc,
workorder: m.workorder,
serial: m.serial,
});
}

const restrictedProcs = LazySeq.of(log.material ?? [])
.map((m) => m.proc)
.filter((p) => p > 0)
.toSortedArray((p) => p);

return {
bookingId: log.result,
partName: log.program,
quantity: qty,
timeUTC: log.endUTC,
priority: log.locnum,
notes: log.details?.["Notes"],
workorder: log.details?.["Workorder"] ?? log.material?.[0].workorder,
restrictedProcs: restrictedProcs as number[],
material: mat,
};
}

export const setLast30Rebookings = atom(null, (get, set, log: ReadonlyArray<Readonly<ILogEntry>>) => {
set(rebookingEvts, (old) =>
old.union(
LazySeq.of(log)
.filter((e) => e.type === LogType.Rebooking)
.toOrderedMap((e) => [e.result, convertLogToRebooking(e)]),
),
);
set(canceledRebookingsRW, (old) =>
old.union(
LazySeq.of(log)
.filter((e) => e.type === LogType.CancelRebooking)
.toOrderedSet((e) => e.result),
),
);
});

function updateJobs(set: Setter, jobs: LazySeq<Readonly<IJob>>, expire?: Date) {
set(scheduledRW, (old) => {
if (expire) {
old = old.filter((b) => b.scheduledTime >= expire);
}
return old.union(
jobs
.flatMap((j) =>
(j.bookings ?? []).map(
(b) =>
[
b,
{
scheduledTime: j.routeStartUTC,
jobUnique: j.unique,
},
] as const,
),
)
.toOrderedMap((x) => x),
);
});
}

export const setLast30RebookingJobs = atom(null, (_, set, historic: Readonly<IRecentHistoricData>) => {
updateJobs(
set,
LazySeq.ofObject(historic.jobs).map(([_, j]) => j),
);
});

export const updateLast30Rebookings = atom(null, (get, set, { evt, now, expire }: ServerEventAndTime) => {
if (evt.newJobs) {
updateJobs(set, LazySeq.of(evt.newJobs.jobs), expire ? addDays(now, -30) : undefined);
} else if (evt.logEntry) {
const e = evt.logEntry;
if (e.type === LogType.Rebooking) {
set(rebookingEvts, (old) => old.set(e.result, convertLogToRebooking(e)));
} else if (e.type === LogType.CancelRebooking) {
set(canceledRebookingsRW, (old) => old.add(e.result));
}
}
});
13 changes: 13 additions & 0 deletions client/insight/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
CallSplit,
AttachMoney as CostIcon,
Opacity,
Replay,
} from "@mui/icons-material";

import OperationDashboard from "./operations/Dashboard.js";
Expand Down Expand Up @@ -93,6 +94,7 @@ import { useAtom, useAtomValue } from "jotai";
import { SimDayUsagePage } from "./operations/SimDayUsage.js";
import { latestSimDayUsage } from "../cell-status/sim-day-usage.js";
import { CloseoutReport } from "./operations/CloseoutReport.js";
import { RebookingsPage } from "./operations/Rebookings.js";

const OperationsReportsTab = "bms-operations-reports-tab";

Expand Down Expand Up @@ -171,6 +173,12 @@ const operationsReports: ReadonlyArray<MenuNavItem> = [
route: { route: routes.RouteLocation.Operations_Production },
icon: <ProductionIcon />,
},
{
name: "Rebooking",
route: { route: routes.RouteLocation.Operations_Rebookings },
icon: <Replay />,
hidden: (info) => Boolean(info.supportsRebookings),
},
];

const analysisReports: ReadonlyArray<MenuNavItem> = [
Expand Down Expand Up @@ -542,6 +550,11 @@ const App = memo(function App(props: AppProps) {
nav1 = OperationsTabs;
menuNavItems = operationsReports;
break;
case routes.RouteLocation.Operations_Rebookings:
page = <RebookingsPage />;
nav1 = OperationsTabs;
menuNavItems = operationsReports;
break;

case routes.RouteLocation.Engineering_Cycles:
page = <RecentStationCycleChart ty="machine" />;
Expand Down
8 changes: 6 additions & 2 deletions client/insight/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ import { RouteLocation, RouteState, currentRoute, helpUrl } from "./routes";
import { fmsInformation, logout } from "../network/server-settings";
import { useAtom, useAtomValue } from "jotai";
import { QRScanButton } from "./BarcodeScanning";
import { IFMSInfo } from "../network/api";

export type MenuNavItem =
| {
readonly name: string;
readonly icon: ReactNode;
readonly route: RouteState;
readonly hidden?: (info: Readonly<IFMSInfo>) => boolean;
}
| { readonly separator: string };

Expand Down Expand Up @@ -168,6 +170,7 @@ function ToolButtons({
}

function MenuNavSelect({ menuNavs }: { menuNavs: ReadonlyArray<MenuNavItem> }) {
const fmsInfo = useAtomValue(fmsInformation);
const [curRoute, setCurrentRoute] = useAtom(currentRoute);
const [isPending, startTransition] = useTransition();
return (
Expand Down Expand Up @@ -201,7 +204,7 @@ function MenuNavSelect({ menuNavs }: { menuNavs: ReadonlyArray<MenuNavItem> }) {
{menuNavs.map((item) =>
"separator" in item ? (
<ListSubheader key={item.separator}>{item.separator}</ListSubheader>
) : (
) : item.hidden?.(fmsInfo) ? undefined : (
<MenuItem key={item.name} value={item.route.route}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.name} />
Expand Down Expand Up @@ -282,6 +285,7 @@ export function Header({
}

export function SideMenu({ menuItems }: { menuItems?: ReadonlyArray<MenuNavItem> }) {
const fmsInfo = useAtomValue(fmsInformation);
const [curRoute, setCurrentRoute] = useAtom(currentRoute);
const [isPending, startTransition] = useTransition();

Expand All @@ -304,7 +308,7 @@ export function SideMenu({ menuItems }: { menuItems?: ReadonlyArray<MenuNavItem>
{menuItems?.map((item) =>
"separator" in item ? (
<ListSubheader key={item.separator}>{item.separator}</ListSubheader>
) : (
) : item.hidden?.(fmsInfo) ? undefined : (
<ListItem key={item.name}>
<ListItemButton
selected={curRoute.route === item.route.route}
Expand Down
45 changes: 45 additions & 0 deletions client/insight/src/components/operations/Rebookings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* Copyright (c) 2024, John Lenz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of John Lenz, Black Maple Software, SeedTactics,
nor the names of other contributors may be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import { Box } from "@mui/material";
import { useSetTitle } from "../routes";
import { IRebooking } from "../../network/api";

function RebookingRow({ b }: { b: Readonly<IRebooking> }) {}

function RebookingTable() {}

export function RebookingsPage() {
useSetTitle("Rebookings");
return <Box component="main" sx={{ padding: "24px" }}></Box>;
}
3 changes: 3 additions & 0 deletions client/insight/src/components/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export enum RouteLocation {
Operations_Quality = "/operations/quality",
Operations_Inspections = "/operations/inspections",
Operations_CloseoutReport = "/operations/closeout",
Operations_Rebookings = "/analysis/rebookings",

Engineering_Cycles = "/engineering",
Engineering_Hours = "/engineering/hours",
Expand Down Expand Up @@ -136,6 +137,7 @@ export type RouteState =
| { route: RouteLocation.Operations_Quality }
| { route: RouteLocation.Operations_Inspections }
| { route: RouteLocation.Operations_CloseoutReport }
| { route: RouteLocation.Operations_Rebookings}
| { route: RouteLocation.Engineering_Cycles }
| { route: RouteLocation.Engineering_Outliers }
| { route: RouteLocation.Engineering_Hours }
Expand Down Expand Up @@ -305,6 +307,7 @@ export function helpUrl(r: RouteState): string {
case RouteLocation.Operations_CurrentWorkorders:
case RouteLocation.Operations_CloseoutReport:
case RouteLocation.Operations_Production:
case RouteLocation.Operations_Rebookings:
return "https://www.seedtactics.com/docs/fms-insight/client-operations";

case RouteLocation.Operations_MachineOutliers:
Expand Down
3 changes: 3 additions & 0 deletions client/insight/src/network/backend-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ export function registerMockBackend(
invalidatePalletCycle(): Promise<void> {
return Promise.resolve();
},
unscheduledRebookings(): Promise<ReadonlyArray<Readonly<api.IRebooking>>> {
return Promise.resolve([]);
}
};

const serialsToMatId = data.then(() =>
Expand Down
1 change: 1 addition & 0 deletions client/insight/src/network/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface JobAPI {
operName: string | null,
process: number,
): Promise<void>;
unscheduledRebookings(signal?: AbortSignal): Promise<ReadonlyArray<Readonly<api.IRebooking>>>;
}

export interface FmsAPI {
Expand Down

0 comments on commit 7545ec3

Please sign in to comment.