Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions hackathon_site/dashboard/frontend/src/api/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ export const teamOrderListSerialization = (
);

if (hardwareInTableRow.length > 0 && !allItemsReturnedOrRejected) {
(order.status === "Submitted" || order.status === "Ready for Pickup"
// include "In Progress" status with pending orders so admins can see who's packing
(order.status === "Submitted" ||
order.status === "In Progress" ||
order.status === "Ready for Pickup"
? pendingOrders
: checkedOutOrders
).push({
Expand All @@ -106,6 +109,8 @@ export const teamOrderListSerialization = (
hardwareInTableRow,
createdTime: order.created_at,
updatedTime: order.updated_at,
packing_admin_id: order.packing_admin_id, // track who is packing this order
packing_admin_name: order.packing_admin_name, // display admin name in ui
});
}
}
Expand Down Expand Up @@ -205,10 +210,13 @@ export const sortCheckedOutOrders = (

export const sortPendingOrders = (orders: OrderInTable[]): OrderInTable[] => {
let ready_orders = [];
let in_progress_orders = []; // track orders currently being packed
let submitted_orders = [];
for (let order of orders) {
if (order.status === "Ready for Pickup") {
ready_orders.push(order);
} else if (order.status === "In Progress") {
in_progress_orders.push(order);
} else {
submitted_orders.push(order);
}
Expand All @@ -220,13 +228,27 @@ export const sortPendingOrders = (orders: OrderInTable[]): OrderInTable[] => {
);
});

in_progress_orders.sort((order1, order2) => {
return (
new Date(order1.updatedTime).valueOf() -
new Date(order2.updatedTime).valueOf()
);
});

submitted_orders.sort((order1, order2) => {
return (
new Date(order1.updatedTime).valueOf() -
new Date(order2.updatedTime).valueOf()
);
});

orders.splice(0, orders.length, ...submitted_orders, ...ready_orders);
// sort order: submitted first, then in progress (being packed), then ready for pickup
orders.splice(
0,
orders.length,
...submitted_orders,
...in_progress_orders,
...ready_orders
);
return orders;
};
16 changes: 14 additions & 2 deletions hackathon_site/dashboard/frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ export interface ProfileWithUser extends ProfileWithoutTeamNumber {
/** Orders API */
export type OrderStatus =
| "Submitted"
| "In Progress" // new status for tracking order packing
| "Ready for Pickup"
| "Picked Up"
| "Cancelled"
| "Returned"
| "Pending"
| "In Progress";
| "Pending";

export type PartReturnedHealth =
| "Healthy"
Expand All @@ -132,6 +132,8 @@ export interface Order {
total_credits: number;
created_at: string;
updated_at: string;
packing_admin_id?: number | null; // track which admin is packing this order
packing_admin_name?: string | null; // admin's full name for display
}

export type OrderOrdering = "" | "created_at" | "-created_at";
Expand All @@ -157,6 +159,8 @@ export interface OrderInTable {
status: OrderStatus;
createdTime: string;
updatedTime: string;
packing_admin_id?: number | null; // track which admin is packing this order
packing_admin_name?: string | null; // admin's full name for display
}

export type ReturnedItem = ItemsInOrder & { quantity: number; time: string };
Expand Down Expand Up @@ -199,3 +203,11 @@ export interface Incident {
created_at: string;
updated_at: string;
}

/** Order Lock API */
export interface OrderLockStatus {
orders_locked: boolean;
locked_by: string | null;
locked_at: string | null;
reason: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { teamSelector, teamSizeSelector } from "slices/event/teamSlice";
import { isTestUserSelector } from "slices/users/userSlice";
import { projectDescriptionSelector } from "slices/event/teamSlice";
import { getCreditsUsedSelector } from "slices/order/orderSlice";
import {
fetchLockStatus,
isLoadingSelector as isLockLoadingSelector,
ordersLockedSelector,
} from "slices/hardware/orderLockSlice";
import { AppDispatch } from "slices/store";
import { displaySnackbar } from "slices/ui/uiSlice";
import {
hardwareSignOutEndDate,
hardwareSignOutStartDate,
Expand All @@ -33,13 +40,28 @@ const CartSummary = () => {
const subtotalCredits = useSelector(subtotalCreditsSelector);
const creditsUsed = useSelector(getCreditsUsedSelector);
const creditsAvailable = useSelector(teamSelector)?.credits;
const ordersLocked = useSelector(ordersLockedSelector);
const isLockLoading = useSelector(isLockLoadingSelector);
const projectedCredits = creditsAvailable
? creditsAvailable - creditsUsed - subtotalCredits
: 0;
const teamSizeValid = teamSize >= minTeamSize && teamSize <= maxTeamSize;
const dispatch = useDispatch();
const onSubmit = () => {
const dispatch = useDispatch<AppDispatch>();
const onSubmit = async () => {
if (cartQuantity > 0) {
// Check lock status before submitting
const lockStatus = await dispatch(fetchLockStatus()).unwrap();
if (lockStatus.orders_locked) {
dispatch(
displaySnackbar({
message: "Orders are currently locked! Refreshing page...",
options: {
variant: "error",
},
})
);
return;
}
dispatch(submitOrder());
}
};
Expand Down Expand Up @@ -77,13 +99,16 @@ const CartSummary = () => {
variant="contained"
className={styles.btn}
disabled={
cartQuantity === 0 ||
cartQuantity === 0 ||
cartOrderLoading ||
isLockLoading ||
!teamSizeValid ||
!projectDescription || // Checks if projectDescription is null, undefined, or an empty string
(projectDescription &&
projectDescription.length < minProjectDescriptionLength) ||
(!isTestUser && isOutsideSignOutPeriod) ||
(!isTestUser && ordersLocked) ||
projectedCredits < 0
}
onClick={onSubmit}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,45 @@ import React, { ReactElement } from "react";
import { Alert, AlertTitle } from "@material-ui/lab";

interface ErrorBoxProps {
error?: string[] | string;
error?: string[] | string | object;
body?: ReactElement;
type?: "error" | "info" | "success" | "warning";
title?: string;
}

const AlertBox = ({ error, body, type, title, ...otherProps }: ErrorBoxProps) => {
// Helper function to convert error to displayable string
const getErrorMessage = (err: unknown): string => {
if (typeof err === "string") return err;
if (typeof err === "object" && err !== null) {
// Handle common error object shapes
if ("message" in err) return String((err as { message: unknown }).message);
if ("detail" in err) return String((err as { detail: unknown }).detail);
if ("error" in err) return String((err as { error: unknown }).error);
return JSON.stringify(err);
}
return String(err);
};

return (
<Alert
severity={type ?? "error"}
style={{ margin: "15px 0px" }}
{...otherProps}
>
{typeof error === "object" ? (
{Array.isArray(error) ? (
<>
<AlertTitle>{title ?? "An error has occurred because:"}</AlertTitle>
<ul style={{ marginLeft: "20px" }} data-testid="alert-error-list">
{error.map((err, index) => (
<li key={index}>{err}</li>
<li key={index}>{getErrorMessage(err)}</li>
))}
</ul>
</>
) : (
<>
<AlertTitle>{title ?? "An error has occurred"}</AlertTitle>
{body || error}
{body || getErrorMessage(error)}
</>
)}
</Alert>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { useSelector } from "react-redux";
import AlertBox from "components/general/AlertBox/AlertBox";
import { ordersLockedSelector } from "slices/hardware/orderLockSlice";

const OrderLockAlert = () => {
const ordersLocked = useSelector(ordersLockedSelector);

return ordersLocked ? (
<AlertBox
data-testid="order-lock-alert"
title="Order Submissions Currently Locked"
error="Hardware order submissions are currently locked by administrators. Please contact the hardware team at hardware@makeuoft.ca for assistance."
type="warning"
/>
) : null;
};

export default OrderLockAlert;
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ interface OrderProps {
time: string;
id: number;
status: string;
packingAdminName?: string | null; // display who is packing this order
}

const OrderCard = ({ teamCode, orderQuantity, time, id, status }: OrderProps) => {
const OrderCard = ({
teamCode,
orderQuantity,
time,
id,
status,
packingAdminName,
}: OrderProps) => {
const date = new Date(time);
const month = date.toLocaleString("default", { month: "short" });
const day = date.getDate();
Expand All @@ -25,6 +33,10 @@ const OrderCard = ({ teamCode, orderQuantity, time, id, status }: OrderProps) =>
{ title: "Order Qty", value: orderQuantity },
{ title: "Time", value: `${month} ${day}, ${hoursAndMinutes}` },
{ title: "ID", value: id },
// show which admin is packing this order when status is "In Progress"
...(status === "In Progress" && packingAdminName
? [{ title: "Packing Admin", value: packingAdminName }]
: []),
];

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import Button from "@material-ui/core/Button";
import LockIcon from "@material-ui/icons/Lock";
import LockOpenIcon from "@material-ui/icons/LockOpen";
import CircularProgress from "@material-ui/core/CircularProgress";
import {
lockStatusSelector,
isLoadingSelector,
toggleLock,
} from "slices/hardware/orderLockSlice";
import { displaySnackbar } from "slices/ui/uiSlice";
import { AppDispatch } from "slices/store";

const OrderLockButton = () => {
const dispatch = useDispatch<AppDispatch>();
const lockStatus = useSelector(lockStatusSelector);
const isLoading = useSelector(isLoadingSelector);

const handleToggle = async () => {
const newLockState = !lockStatus.orders_locked;
try {
const result = await dispatch(
toggleLock({
orders_locked: newLockState,
reason: "",
})
);

if (toggleLock.fulfilled.match(result)) {
dispatch(
displaySnackbar({
message: newLockState
? "Order submissions have been locked"
: "Order submissions have been unlocked",
options: { variant: "success" },
})
);
} else {
dispatch(
displaySnackbar({
message: "Failed to toggle lock status",
options: { variant: "error" },
})
);
}
} catch (error) {
dispatch(
displaySnackbar({
message: "Failed to toggle lock status",
options: { variant: "error" },
})
);
}
};

return (
<Button
variant="contained"
color={lockStatus.orders_locked ? "secondary" : "primary"}
startIcon={
isLoading ? (
<CircularProgress size={20} color="inherit" />
) : lockStatus.orders_locked ? (
<LockIcon />
) : (
<LockOpenIcon />
)
}
onClick={handleToggle}
disabled={isLoading}
data-testid="order-lock-button"
>
{lockStatus.orders_locked ? "Unlock Orders" : "Lock Orders"}
</Button>
);
};

export default OrderLockButton;
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ const OrderFilter = ({ handleReset, handleSubmit }: FormikValues) => {
status: "Submitted",
numOrders: numStatuses["Submitted"],
},
{
// new status to show orders currently being packed
status: "In Progress",
numOrders: numStatuses["In Progress"],
},
{
status: "Ready for Pickup",
numOrders: numStatuses["Ready for Pickup"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ const OrdersTable = ({ ordersData }: OrdersTableProps) => {
minWidth: 250,
renderCell: (params) => <OrderStateIcon status={params.value} />,
},
{
// show which admin is currently packing this order to prevent double-packing
field: "packing_admin_name",
headerName: "Packing Admin",
flex: 1,
minWidth: 150,
valueGetter: (params) => params.value || "-",
},
{ field: "updated_at", headerName: "Updated At" },
{ field: "request", headerName: "Request" },
];
Expand Down
Loading
Loading