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