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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
toggleCheckedOutTable,
togglePendingTable,
toggleReturnedTable,
displaySnackbar,
} from "slices/ui/uiSlice";
import {
getUpdatedHardwareDetails,
Expand All @@ -35,6 +36,7 @@ import {
returnedOrdersSelector,
cancelOrderThunk,
cancelOrderLoadingSelector,
getTeamOrders,
} from "slices/order/orderSlice";
import {
GeneralOrderTitle,
Expand Down Expand Up @@ -236,17 +238,34 @@ 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);

const closeModal = () => {
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);
}
};
Expand Down Expand Up @@ -282,7 +301,7 @@ export const PendingTables = () => {
data-updated-time={`pending-order-time-${pendingOrder.updatedTime}`}
>
<GeneralPendingTable {...{ pendingOrder }} />
{pendingOrder.status !== "Ready for Pickup" && (
{pendingOrder.status === "Submitted" && (
<div
style={{
display: "flex",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const ChipStatus = ({
<>
<Chip
icon={<WatchLater />}
label="In progress"
label="Submitted"
className={`${styles.chipOrange} ${styles.chip}`}
/>
{overLimit && (
Expand All @@ -60,6 +60,14 @@ export const ChipStatus = ({
)}
</>
);
case "In Progress":
return (
<Chip
icon={<WatchLater />}
label="Being Packed"
className={`${styles.chipBlue} ${styles.chip}`}
/>
);
case "Error":
return (
<Chip
Expand Down
117 changes: 111 additions & 6 deletions hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import styles from "./TeamDetail.module.scss";

import TeamInfoTable from "components/teamDetail/TeamInfoTable/TeamInfoTable";
import TeamActionTable from "components/teamDetail/TeamActionTable/TeamActionTable";

import { RouteComponentProps } from "react-router-dom";
import Header from "components/general/Header/Header";
import { Grid, Divider } from "@material-ui/core";
import { Grid, Divider, TextField, IconButton } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import EditIcon from "@material-ui/icons/Edit";
import SaveIcon from "@material-ui/icons/Save";
import CloseIcon from "@material-ui/icons/Close";
import { AdminReturnedItemsTable } from "components/teamDetail/SimpleOrderTables/SimpleOrderTables";
import {
errorSelector,
Expand All @@ -22,6 +25,7 @@ import {
teamInfoErrorSelector,
teamStartingCreditsSelector,
updateParticipantIdErrorSelector,
updateTeamCredits,
} from "slices/event/teamDetailSlice";
import AlertBox from "components/general/AlertBox/AlertBox";
import TeamCheckedOutOrderTable from "components/teamDetail/TeamCheckedOutOrderTable/TeamCheckedOutOrderTable";
Expand All @@ -46,6 +50,15 @@ const TeamDetail = ({ match }: RouteComponentProps<PageParams>) => {
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"
Expand All @@ -66,6 +79,18 @@ const TeamDetail = ({ match }: RouteComponentProps<PageParams>) => {
}
}, [dispatch, hardwareIdsRequired]);

const handleSaveCredits = () => {
if (editedCredits >= 0) {
dispatch(updateTeamCredits({ teamCode, credits: editedCredits }));
setIsEditingCredits(false);
}
};

const handleCancelEdit = () => {
setEditedCredits(creditsAvailable);
setIsEditingCredits(false);
};

return (
<>
<Header />
Expand All @@ -75,10 +100,90 @@ const TeamDetail = ({ match }: RouteComponentProps<PageParams>) => {
) : (
<Grid container direction="column" spacing={6}>
<Grid item xs={12}>
<Typography variant="h1">
Team {teamCode} Overview - (💳 {creditsRemaining} Credits
Left)
</Typography>
<Typography variant="h1">Team {teamCode} Overview</Typography>
{/* Credits Editor Section */}
<div
className={styles.creditsSection}
style={{
display: "flex",
alignItems: "center",
gap: "16px",
marginTop: "12px",
padding: "12px 16px",
backgroundColor: "#f5f5f5",
borderRadius: "8px",
}}
>
{isEditingCredits ? (
<>
<TextField
label="Total Credits"
type="number"
value={editedCredits}
onChange={(e) =>
setEditedCredits(
parseInt(e.target.value) || 0
)
}
size="small"
variant="outlined"
style={{ width: "120px" }}
inputProps={{ min: 0 }}
/>
<IconButton
color="primary"
onClick={handleSaveCredits}
size="small"
title="Save"
>
<SaveIcon />
</IconButton>
<IconButton
onClick={handleCancelEdit}
size="small"
title="Cancel"
>
<CloseIcon />
</IconButton>
</>
) : (
<>
<Typography variant="body1">
💳 <strong>Total Credits:</strong>{" "}
{creditsAvailable}
</Typography>
<IconButton
color="primary"
onClick={() => setIsEditingCredits(true)}
size="small"
title="Edit Credits"
>
<EditIcon fontSize="small" />
</IconButton>
</>
)}
<Divider
orientation="vertical"
flexItem
style={{ margin: "0 8px" }}
/>
<Typography variant="body1">
📊 <strong>Used:</strong> {creditsUsed}
</Typography>
<Divider
orientation="vertical"
flexItem
style={{ margin: "0 8px" }}
/>
<Typography
variant="body1"
style={{
color: creditsRemaining < 0 ? "red" : "inherit",
}}
>
💰 <strong>Remaining:</strong> {creditsRemaining}
</Typography>
</div>
</Grid>
<Grid
item
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,52 @@ export const updateProjectDescription = createAsyncThunk<
}
);

interface UpdateTeamCreditsParams {
teamCode: string;
credits: number;
}

export const updateTeamCredits = createAsyncThunk<
Team,
UpdateTeamCreditsParams,
{ state: RootState; rejectValue: RejectValue; dispatch: AppDispatch }
>(
`${teamDetailReducerName}/updateTeamCredits`,
async ({ teamCode, credits }, { rejectWithValue, dispatch }) => {
try {
const response = await patch<Team>(`/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,
Expand Down Expand Up @@ -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";
});
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion hackathon_site/event/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion hackathon_site/event/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ class Meta:
"created_at",
"updated_at",
"profiles",
"credits",
)


Expand Down
2 changes: 1 addition & 1 deletion hackathon_site/hackathon_site/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading