Skip to content

Commit

Permalink
client: more work on rebookings
Browse files Browse the repository at this point in the history
  • Loading branch information
wuzzeb committed Oct 31, 2024
1 parent c043be6 commit 962dd80
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 16 deletions.
51 changes: 45 additions & 6 deletions client/insight/src/cell-status/rebookings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ 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 { LazySeq, OrderedMap } from "@seedtactics/immutable-collections";
import { JobsBackend, LogBackend } from "../network/backend";
import { loadable } from "jotai/utils";
import { addDays } from "date-fns";
import { useCallback, useState } from "react";

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

Expand All @@ -62,8 +63,8 @@ export const last30Rebookings: Atom<OrderedMap<string, Readonly<IRebooking>>> =
}
});

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

type ScheduledBooking = {
readonly scheduledTime: Date;
Expand Down Expand Up @@ -121,7 +122,7 @@ export const setLast30Rebookings = atom(null, (get, set, log: ReadonlyArray<Read
old.union(
LazySeq.of(log)
.filter((e) => e.type === LogType.CancelRebooking)
.toOrderedSet((e) => e.result),
.toOrderedMap((e) => [e.result, e.endUTC]),
),
);
});
Expand Down Expand Up @@ -165,7 +166,45 @@ export const updateLast30Rebookings = atom(null, (get, set, { evt, now, expire }
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));
set(canceledRebookingsRW, (old) => old.set(e.result, e.endUTC));
}
}
});

export function useCancelRebooking(): [(bookingId: string) => Promise<void>, boolean] {
const [loading, setLoading] = useState(false);
const callback = useCallback((bookingId: string) => {
setLoading(true);
return LogBackend.cancelRebooking(bookingId)
.then(() => {})
.catch(console.log)
.finally(() => setLoading(false));
}, []);
return [callback, loading];
}

export type NewRebooking = {
readonly part: string;
readonly qty?: number;
readonly workorder?: string | null;
readonly restrictedProcs?: ReadonlyArray<number> | null;
readonly notes?: string;
};

export function useNewRebooking(): [(n: NewRebooking) => Promise<void>, boolean] {
const [loading, setLoading] = useState(false);
const callback = useCallback((n: NewRebooking) => {
setLoading(true);
return LogBackend.requestRebookingWithoutMaterial(
n.part,
n.qty,
n.workorder,
n.restrictedProcs as number[],
n.notes,
)
.then(() => {})
.catch(console.log)
.finally(() => setLoading(false));
}, []);
return [callback, loading];
}
8 changes: 6 additions & 2 deletions client/insight/src/components/analysis/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,13 @@ export function useTablePage(): TablePage {
);
}

export function useColSort<Id, Row>(defSortCol: Id, cols: ReadonlyArray<Column<Id, Row>>): ColSort<Id, Row> {
export function useColSort<Id, Row>(
defSortCol: Id,
cols: ReadonlyArray<Column<Id, Row>>,
defOrder?: "asc" | "desc" | undefined,
): ColSort<Id, Row> {
const [orderBy, setOrderBy] = useState(defSortCol);
const [order, setOrder] = useState<"asc" | "desc">("asc");
const [order, setOrder] = useState<"asc" | "desc">(defOrder ?? "asc");

return useMemo(() => {
function handleRequestSort(property: Id) {
Expand Down
279 changes: 272 additions & 7 deletions client/insight/src/components/operations/Rebookings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,280 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
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";
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Fab,
Stack,
Table,
Typography,
} from "@mui/material";
import { useAtomValue } from "jotai";
import { fmsInformation } from "../../network/server-settings.js";
import {
Column,
DataTableActions,
DataTableBody,
DataTableHead,
useColSort,
useTablePage,
} from "../analysis/DataTable.js";
import { useSetTitle } from "../routes.js";
import { IRebooking } from "../../network/api.js";
import { memo, useMemo, useState } from "react";
import {
canceledRebookings,
last30Rebookings,
last30ScheduledBookings,
useCancelRebooking,
} from "../../cell-status/rebookings.js";
import { OrderedMap } from "@seedtactics/immutable-collections";
import { Add } from "@mui/icons-material";

function RebookingRow({ b }: { b: Readonly<IRebooking> }) {}
enum ColumnId {
BookingId,
Part,
Quantity,
RequestTime,
Priority,
Workorder,
Serial,
Canceled,
Job,
SchTime,
// Procs
// Notes
}

type Row = Readonly<IRebooking> & {
readonly job?: string;
readonly schTime?: Date;
readonly canceled?: Date;
};

type Col = Column<ColumnId, Row>;

const columns: ReadonlyArray<Col> = [
{
id: ColumnId.BookingId,
numeric: false,
label: "ID",
getDisplay: (r) => r.bookingId,
Cell: ({ row }: { row: Row }) =>
row.canceled ? (
<Typography sx={{ textDecoration: "line-through" }}>{row.bookingId}</Typography>
) : (
row.bookingId
),
},
{
id: ColumnId.Part,
numeric: false,
label: "Part",
getDisplay: (r) => r.partName,
},
{
id: ColumnId.Quantity,
numeric: true,
label: "Quantity",
getDisplay: (r) => r.quantity.toString(),
getForSort: (r) => r.quantity,
},
{
id: ColumnId.RequestTime,
numeric: false,
label: "Request Time",
getDisplay: (r) => r.timeUTC.toLocaleString(),
getForSort: (r) => r.timeUTC.getTime(),
},
{
id: ColumnId.Priority,
numeric: false,
label: "Priority",
getDisplay: (r) => (r.priority ? r.priority.toString() : ""),
getForSort: (r) => r.priority ?? 0,
},
{
id: ColumnId.Workorder,
numeric: false,
label: "Workorder",
getDisplay: (r) => r.workorder ?? "",
},
{
id: ColumnId.Serial,
numeric: false,
label: "Serial",
getDisplay: (r) => r.material?.serial ?? "",
},
{
id: ColumnId.Canceled,
numeric: false,
label: "Canceled",
getDisplay: (r) => r.canceled?.toLocaleString() ?? "",
getForSort: (r) => r.canceled?.getTime() ?? 0,
},
{
id: ColumnId.Job,
numeric: false,
label: "Scheduled Job",
getDisplay: (r) => r.job ?? "",
},
{
id: ColumnId.SchTime,
numeric: false,
label: "Scheduled Time",
getDisplay: (r) => (r.schTime ? r.schTime.toLocaleString() : ""),
getForSort: (r) => (r.schTime ? r.schTime.getTime() : 0),
},
];

const BookingTable = memo(function BookingTable({
setRebookingToShow,
}: {
setRebookingToShow: (r: Row) => void;
}) {
const sort = useColSort(ColumnId.RequestTime, columns, "desc");
const tpage = useTablePage();
const rebookings: OrderedMap<string, Row> = useAtomValue(last30Rebookings);
const canceled = useAtomValue(canceledRebookings);
const scheduled = useAtomValue(last30ScheduledBookings);

const rows = useMemo(
() =>
rebookings
.adjust(scheduled, (r, sch) =>
r
? {
...r,
job: sch?.jobUnique,
schTime: sch?.scheduledTime,
}
: undefined,
)
.adjust(canceled, (r, t) => (r ? { ...r, canceled: t } : undefined))
.valuesToAscLazySeq()
.toSortedArray(sort.sortOn),
[sort.sortOn, rebookings, canceled, scheduled],
);

return (
<div>
<Table>
<DataTableHead columns={columns} sort={sort} showDetailsCol copyToClipboardRows={rows} />
<DataTableBody
columns={columns}
pageData={rows}
rowsPerPage={tpage.rowsPerPage}
onClickDetails={(_, row) => setRebookingToShow(row)}
/>
</Table>
<DataTableActions tpage={tpage} count={rows.length} />
</div>
);
});

function RebookingTable() {}
const RebookingDialog = memo(function RebookingDialog({
rebooking,
close,
}: {
rebooking: Row | undefined;
close: (r: undefined) => void;
}) {
const [sendCancel, canceling] = useCancelRebooking();

function cancel() {
if (rebooking) {
sendCancel(rebooking.bookingId)
.then(() => close(undefined))
.catch(console.log);
}
}

return (
<Dialog open={rebooking !== undefined} onClose={() => close(undefined)}>
<DialogTitle>
{rebooking?.canceled ? (
<Typography sx={{ textDecoration: "line-through" }}>{rebooking?.bookingId}</Typography>
) : (
rebooking?.bookingId
)}
</DialogTitle>
<DialogContent>
<Stack direction="row" spacing={1}>
<Typography>Part: {rebooking?.partName}</Typography>
<Typography>Quantity: {rebooking?.quantity}</Typography>
<Typography>Request Time: {rebooking?.timeUTC.toLocaleString()}</Typography>
<Typography>Priority: {rebooking?.priority}</Typography>
<Typography>Workorder: {rebooking?.workorder}</Typography>
<Typography>Serial: {rebooking?.material?.serial}</Typography>
<Typography>Note: {rebooking?.notes}</Typography>
</Stack>
{rebooking?.job && (
<>
<Divider />
<Stack direction="row" spacing={1}>
<Typography>Scheduled Job: {rebooking?.job}</Typography>
<Typography>Scheduled Time: {rebooking?.schTime?.toLocaleString()}</Typography>
</Stack>
</>
)}
{rebooking?.canceled && <Typography>Canceled: {rebooking.canceled.toLocaleString()}</Typography>}
</DialogContent>
<DialogActions>
{!rebooking?.canceled && !rebooking?.job && (
<Button onClick={cancel} color="secondary" disabled={canceling}>
{canceling ? <CircularProgress size={24} /> : undefined}
Cancel Rebooking Request
</Button>
)}
<Button onClick={() => close(undefined)}>Close</Button>
</DialogActions>
</Dialog>
);
});

const NewRebookingDialog = memo(function NewRebookingDialog() {
const [open, setOpen] = useState(false);

function create() {
// TODO
}

return (
<>
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>Create New</DialogTitle>
<DialogContent></DialogContent>
<DialogActions>
<Button color="secondary" onClick={create}>
Create
</Button>
<Button onClick={() => setOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Fab onClick={() => setOpen(true)} sx={{ position: "fixed", bottom: "24px", right: "24px" }}>
<Add />
</Fab>
</>
);
});

export function RebookingsPage() {
useSetTitle("Rebookings");
return <Box component="main" sx={{ padding: "24px" }}></Box>;
const fmsInfo = useAtomValue(fmsInformation);
useSetTitle(fmsInfo.supportsRebookings ?? "Rebookings");
const [rebookingToShow, setRebookingToShow] = useState<Row | undefined>(undefined);

return (
<Box component="main" sx={{ padding: "24px" }}>
<BookingTable setRebookingToShow={setRebookingToShow} />
<RebookingDialog rebooking={rebookingToShow} close={setRebookingToShow} />
<NewRebookingDialog />
</Box>
);
}
Loading

0 comments on commit 962dd80

Please sign in to comment.