diff --git a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx
index b27ceae..eb6bad6 100644
--- a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx
+++ b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx
@@ -22,6 +22,7 @@ import {
toggleCheckedOutTable,
togglePendingTable,
toggleReturnedTable,
+ displaySnackbar,
} from "slices/ui/uiSlice";
import {
getUpdatedHardwareDetails,
@@ -35,6 +36,7 @@ import {
returnedOrdersSelector,
cancelOrderThunk,
cancelOrderLoadingSelector,
+ getTeamOrders,
} from "slices/order/orderSlice";
import {
GeneralOrderTitle,
@@ -236,7 +238,6 @@ export const PendingTables = () => {
const isVisible = useSelector(isPendingTableVisibleSelector);
const isCancelOrderLoading = useSelector(cancelOrderLoadingSelector);
const toggleVisibility = () => dispatch(togglePendingTable());
- const cancelOrder = (orderId: number) => dispatch(cancelOrderThunk(orderId));
const [showCancelOrderModal, setShowCancelOrderModal] = useState(false);
const [orderId, setorderId] = useState(null);
@@ -244,9 +245,27 @@ export const PendingTables = () => {
setShowCancelOrderModal(false);
};
- const submitCancelOrderModal = (cancelOrderId: number | null) => {
+ const submitCancelOrderModal = async (cancelOrderId: number | null) => {
if (cancelOrderId != null) {
- cancelOrder(cancelOrderId); // Perform Cancellation
+ // Refresh orders first to get latest status
+ await dispatch(getTeamOrders());
+
+ // Find the order with the latest status from state
+ const currentOrder = unsorted_orders.find((o) => o.id === cancelOrderId);
+
+ // Only proceed if order is still Submitted
+ if (currentOrder && currentOrder.status === "Submitted") {
+ dispatch(cancelOrderThunk(cancelOrderId));
+ } else {
+ // Order status changed, show error message via snackbar
+ dispatch(
+ displaySnackbar({
+ message:
+ "Cannot cancel order - it is no longer in Submitted status. The order may be being packed.",
+ options: { variant: "error" },
+ })
+ );
+ }
setShowCancelOrderModal(false);
}
};
@@ -282,7 +301,7 @@ export const PendingTables = () => {
data-updated-time={`pending-order-time-${pendingOrder.updatedTime}`}
>
- {pendingOrder.status !== "Ready for Pickup" && (
+ {pendingOrder.status === "Submitted" && (
}
- label="In progress"
+ label="Submitted"
className={`${styles.chipOrange} ${styles.chip}`}
/>
{overLimit && (
@@ -60,6 +60,14 @@ export const ChipStatus = ({
)}
>
);
+ case "In Progress":
+ return (
+
}
+ label="Being Packed"
+ className={`${styles.chipBlue} ${styles.chip}`}
+ />
+ );
case "Error":
return (
) => {
const creditsUsed = useSelector(getCreditsUsedSelector);
const creditsRemaining = creditsAvailable ? creditsAvailable - creditsUsed : 0;
+ // Credits editing state
+ const [isEditingCredits, setIsEditingCredits] = useState(false);
+ const [editedCredits, setEditedCredits] = useState(creditsAvailable);
+
+ // Update editedCredits when creditsAvailable changes
+ useEffect(() => {
+ setEditedCredits(creditsAvailable);
+ }, [creditsAvailable]);
+
const updateParticipantIdError = useSelector(updateParticipantIdErrorSelector);
if (
updateParticipantIdError === "Could not update participant id status: Error 404"
@@ -66,6 +79,18 @@ const TeamDetail = ({ match }: RouteComponentProps) => {
}
}, [dispatch, hardwareIdsRequired]);
+ const handleSaveCredits = () => {
+ if (editedCredits >= 0) {
+ dispatch(updateTeamCredits({ teamCode, credits: editedCredits }));
+ setIsEditingCredits(false);
+ }
+ };
+
+ const handleCancelEdit = () => {
+ setEditedCredits(creditsAvailable);
+ setIsEditingCredits(false);
+ };
+
return (
<>
@@ -75,10 +100,90 @@ const TeamDetail = ({ match }: RouteComponentProps) => {
) : (
-
- Team {teamCode} Overview - (💳 {creditsRemaining} Credits
- Left)
-
+ Team {teamCode} Overview
+ {/* Credits Editor Section */}
+
+ {isEditingCredits ? (
+ <>
+
+ setEditedCredits(
+ parseInt(e.target.value) || 0
+ )
+ }
+ size="small"
+ variant="outlined"
+ style={{ width: "120px" }}
+ inputProps={{ min: 0 }}
+ />
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+ 💳 Total Credits:{" "}
+ {creditsAvailable}
+
+ setIsEditingCredits(true)}
+ size="small"
+ title="Edit Credits"
+ >
+
+
+ >
+ )}
+
+
+ 📊 Used: {creditsUsed}
+
+
+
+ 💰 Remaining: {creditsRemaining}
+
+
(
+ `${teamDetailReducerName}/updateTeamCredits`,
+ async ({ teamCode, credits }, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await patch(`/api/event/teams/${teamCode}/`, {
+ credits,
+ });
+ dispatch(
+ displaySnackbar({
+ message: `Team credits updated to ${credits}.`,
+ options: {
+ variant: "success",
+ },
+ })
+ );
+ return response.data;
+ } catch (e: any) {
+ const message =
+ e.response.statusText === "Not Found"
+ ? `Could not update team credits: Error ${e.response.status}`
+ : `Something went wrong: Error ${e.response.status}`;
+ dispatch(
+ displaySnackbar({
+ message,
+ options: {
+ variant: "error",
+ },
+ })
+ );
+ return rejectWithValue({
+ status: e.response.status,
+ message,
+ });
+ }
+ }
+);
+
const teamDetailSlice = createSlice({
name: teamDetailReducerName,
initialState,
@@ -203,6 +249,19 @@ const teamDetailSlice = createSlice({
state.teamInfoError = payload?.message ?? "Something went wrong";
state.projectDescription = null;
});
+ builder.addCase(updateTeamCredits.pending, (state) => {
+ state.isTeamInfoLoading = true;
+ state.teamInfoError = null;
+ });
+ builder.addCase(updateTeamCredits.fulfilled, (state, { payload }) => {
+ state.isTeamInfoLoading = false;
+ state.teamInfoError = null;
+ state.credits = payload.credits;
+ });
+ builder.addCase(updateTeamCredits.rejected, (state, { payload }) => {
+ state.isTeamInfoLoading = false;
+ state.teamInfoError = payload?.message ?? "Something went wrong";
+ });
},
});
diff --git a/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts b/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts
index 7b8e5b4..36cacde 100644
--- a/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts
+++ b/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts
@@ -131,7 +131,7 @@ export const returnItems = createAsyncThunk<
},
})
);
- // dispatch(getAdminTeamOrders(response.data.team_code));
+ dispatch(getAdminTeamOrders(response.data.team_code));
return response.data;
} catch (e: any) {
const message =
@@ -380,6 +380,12 @@ const teamOrderSlice = createSlice({
};
}
teamOrders.updateOne(state, updateObject);
+
+ // Update creditsUsed when order is cancelled
+ if (payload.status === "Cancelled") {
+ state.creditsUsed -= payload.total_credits || 0;
+ if (state.creditsUsed < 0) state.creditsUsed = 0;
+ }
});
builder.addCase(updateOrderStatus.rejected, (state, { payload, meta }) => {
state.isLoading = false;
diff --git a/hackathon_site/event/api_views.py b/hackathon_site/event/api_views.py
index ace4170..1384b93 100644
--- a/hackathon_site/event/api_views.py
+++ b/hackathon_site/event/api_views.py
@@ -310,12 +310,17 @@ def patch(self, request, *args, **kwargs):
data = request.data
if not isinstance(data, dict):
raise ValueError("Invalid request data format")
- valid_fields = {"project_description"}
+ valid_fields = {"project_description", "credits"}
for field in data:
if field not in valid_fields:
raise ValueError(f'"{field}" is not a valid field for update')
if field == "project_description" and not isinstance(data[field], str):
raise ValueError("project_description must be a string")
+ if field == "credits":
+ if not isinstance(data[field], int):
+ raise ValueError("credits must be an integer")
+ if data[field] < 0:
+ raise ValueError("credits cannot be negative")
return self.partial_update(request, *args, **kwargs)
except ValueError as e:
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
diff --git a/hackathon_site/event/serializers.py b/hackathon_site/event/serializers.py
index 75a5456..c9ef8b4 100644
--- a/hackathon_site/event/serializers.py
+++ b/hackathon_site/event/serializers.py
@@ -183,7 +183,6 @@ class Meta:
"created_at",
"updated_at",
"profiles",
- "credits",
)
diff --git a/hackathon_site/hackathon_site/settings/__init__.py b/hackathon_site/hackathon_site/settings/__init__.py
index d433526..ecbd261 100644
--- a/hackathon_site/hackathon_site/settings/__init__.py
+++ b/hackathon_site/hackathon_site/settings/__init__.py
@@ -322,7 +322,7 @@
REGISTRATION_CLOSE_DATE = datetime(2026, 1, 28, 23, 59, 0, tzinfo=TZ_INFO)
EVENT_START_DATE = datetime(2026, 2, 14, 8, 0, 0, tzinfo=TZ_INFO)
EVENT_END_DATE = datetime(2026, 2, 15, 17, 0, 0, tzinfo=TZ_INFO)
-HARDWARE_SIGN_OUT_START_DATE = datetime(2026, 2, 14, 6, 0, tzinfo=TZ_INFO)
+HARDWARE_SIGN_OUT_START_DATE = datetime(2026, 1, 14, 6, 0, tzinfo=TZ_INFO)
HARDWARE_SIGN_OUT_END_DATE = datetime(2026, 2, 15, 11, 0, tzinfo=TZ_INFO)
# Registration user requirements