diff --git a/hackathon_site/dashboard/frontend/src/api/helpers.ts b/hackathon_site/dashboard/frontend/src/api/helpers.ts index 70f006753..ebd9d7547 100644 --- a/hackathon_site/dashboard/frontend/src/api/helpers.ts +++ b/hackathon_site/dashboard/frontend/src/api/helpers.ts @@ -40,7 +40,7 @@ export const teamOrderListSerialization = ( (hardwareRequested[hardware.id] = hardware.requested_quantity) ); order.items.forEach(({ id, hardware_id, part_returned_health }) => { - if (part_returned_health) { + if (part_returned_health && part_returned_health !== "Rejected") { const returnItemKey = `${hardware_id}-${part_returned_health}`; if (returnedItems[returnItemKey]) returnedItems[returnItemKey].quantity += 1; @@ -55,14 +55,26 @@ export const teamOrderListSerialization = ( }; } } else { - if (hardwareItems[hardware_id]) - hardwareItems[hardware_id].quantityGranted += 1; - else - hardwareItems[hardware_id] = { - id: hardware_id, - quantityGranted: 1, - quantityRequested: hardwareRequested[hardware_id], - }; + if ( + !( + part_returned_health === "Rejected" && + order.status === "Picked Up" + ) + ) { + if (hardwareItems[hardware_id]) { + if (part_returned_health !== "Rejected") + hardwareItems[hardware_id].quantityGranted += 1; + hardwareItems[hardware_id].quantityGrantedBySystem += 1; + } else { + hardwareItems[hardware_id] = { + id: hardware_id, + quantityGranted: + part_returned_health === "Rejected" ? 0 : 1, + quantityRequested: hardwareRequested[hardware_id], + quantityGrantedBySystem: 1, + }; + } + } } hardwareIdsToFetch[hardware_id] = hardware_id; }); diff --git a/hackathon_site/dashboard/frontend/src/api/types.ts b/hackathon_site/dashboard/frontend/src/api/types.ts index 5f7cfc278..7a5c94a55 100644 --- a/hackathon_site/dashboard/frontend/src/api/types.ts +++ b/hackathon_site/dashboard/frontend/src/api/types.ts @@ -103,7 +103,12 @@ export type OrderStatus = | "Picked Up" | "Cancelled" | "Returned"; -export type PartReturnedHealth = "Healthy" | "Heavily Used" | "Broken" | "Lost"; +export type PartReturnedHealth = + | "Healthy" + | "Heavily Used" + | "Broken" + | "Lost" + | "Rejected"; export type ItemsInOrder = Omit; @@ -127,6 +132,7 @@ export interface OrderFilters { ordering?: OrderOrdering; status?: OrderStatus[]; search?: string; + limit?: number; } /** Sanitized Orders */ @@ -134,6 +140,7 @@ export interface OrderItemTableRow { id: number; quantityRequested: number; quantityGranted: number; + quantityGrantedBySystem: number; } export interface OrderInTable { diff --git a/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.tsx b/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.tsx index 6d0225457..f67103b43 100644 --- a/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.tsx +++ b/hackathon_site/dashboard/frontend/src/components/orders/OrdersFilter/OrderFilter.tsx @@ -167,9 +167,11 @@ export const EnhancedOrderFilter = () => { const dispatch = useDispatch(); const handleSubmit = ({ ordering, status }: OrderFilters) => { + const limit = 1000; const filters: OrderFilters = { ordering, status, + limit, }; dispatch(setFilters(filters)); dispatch(getOrdersWithFilters()); @@ -185,6 +187,7 @@ export const EnhancedOrderFilter = () => { initialValues={{ ordering: "", status: [], + limit: 1000, }} onSubmit={handleSubmit} onReset={handleReset} diff --git a/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamActionTable/TeamActionTable.tsx b/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamActionTable/TeamActionTable.tsx index 55ef173a9..f14c56947 100644 --- a/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamActionTable/TeamActionTable.tsx +++ b/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamActionTable/TeamActionTable.tsx @@ -40,7 +40,7 @@ const TeamActionTable = () => { ]; return ( - + Actions diff --git a/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamCheckedOutOrderTable/TeamCheckedOutOrderTable.tsx b/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamCheckedOutOrderTable/TeamCheckedOutOrderTable.tsx index b79ff6295..d1469a68c 100644 --- a/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamCheckedOutOrderTable/TeamCheckedOutOrderTable.tsx +++ b/hackathon_site/dashboard/frontend/src/components/teamDetail/TeamCheckedOutOrderTable/TeamCheckedOutOrderTable.tsx @@ -11,6 +11,7 @@ import { TableContainer, TableHead, TableRow, + Typography, } from "@material-ui/core"; import React, { useState } from "react"; import Container from "@material-ui/core/Container"; @@ -398,6 +399,13 @@ export const TeamCheckedOutOrderTable = () => { spacing={1} style={{ marginTop: "10px" }} > + + + Note: participants will receive an + email every time you change the + status of their order. + + - - - + + + Note: participants will receive an email + every time you change the status of + their order. + + {pendingOrder.status === "Submitted" && ( + + + + )} + {pendingOrder.status === "Ready for Pickup" && ( + + + + )} + {pendingOrder.status === "Submitted" && ( + + + + )} + {pendingOrder.status === "Ready for Pickup" && ( + + + + + + + + )} diff --git a/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx b/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx index e3414405d..e9dc295ac 100644 --- a/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx @@ -8,10 +8,7 @@ import { RouteComponentProps } from "react-router-dom"; import Header from "components/general/Header/Header"; import { Grid, Divider } from "@material-ui/core"; import Typography from "@material-ui/core/Typography"; -import { - AdminReturnedItemsTable, - SimplePendingOrderFulfillmentTable, -} from "components/teamDetail/SimpleOrderTables/SimpleOrderTables"; +import { AdminReturnedItemsTable } from "components/teamDetail/SimpleOrderTables/SimpleOrderTables"; import { errorSelector, getAdminTeamOrders, @@ -29,6 +26,7 @@ import TeamCheckedOutOrderTable from "components/teamDetail/TeamCheckedOutOrderT import { getHardwareWithFilters, setFilters } from "slices/hardware/hardwareSlice"; import { getCategories } from "slices/hardware/categorySlice"; import ProductOverview from "components/inventory/ProductOverview/ProductOverview"; +import TeamPendingOrderTable from "components/teamDetail/TeamPendingOrderTable/TeamPendingOrderTable"; export interface PageParams { code: string; @@ -91,7 +89,8 @@ const TeamDetail = ({ match }: RouteComponentProps) => { ) : ( <> - + {/**/} + diff --git a/hackathon_site/dashboard/frontend/src/slices/order/adminOrderSlice.ts b/hackathon_site/dashboard/frontend/src/slices/order/adminOrderSlice.ts index 82fab7a4c..a3342bbd1 100644 --- a/hackathon_site/dashboard/frontend/src/slices/order/adminOrderSlice.ts +++ b/hackathon_site/dashboard/frontend/src/slices/order/adminOrderSlice.ts @@ -88,7 +88,7 @@ const adminOrderSlice = createSlice({ state: AdminOrderState, { payload }: PayloadAction ) => { - const { status, ordering, search } = { + const { status, ordering, search, limit } = { ...state.filters, ...payload, }; @@ -98,6 +98,7 @@ const adminOrderSlice = createSlice({ ...(status && { status }), ...(ordering && { ordering }), ...(search && { search }), + ...(limit && { limit }), }; }, diff --git a/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts b/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts index 2281071ea..8f9f8ee80 100644 --- a/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts +++ b/hackathon_site/dashboard/frontend/src/slices/order/teamOrderSlice.ts @@ -22,6 +22,10 @@ interface TeamOrderExtraState { export interface UpdateOrderAttributes { id: number; status: OrderStatus; + request?: { + id: number; + requested_quantity: number; + }[]; } const extraState: TeamOrderExtraState = { @@ -69,7 +73,8 @@ export const getAdminTeamOrders = createAsyncThunk< ); return rejectWithValue({ status: e.response.status, - message: e.response.message ?? e.response.data, + message: + e.response.message ?? e.response.data.status ?? e.response.data, }); } } @@ -228,12 +233,26 @@ const teamOrderSlice = createSlice({ builder.addCase(updateOrderStatus.fulfilled, (state, { payload }) => { state.isLoading = false; state.error = null; - const updateObject = { - id: payload.id, - changes: { - status: payload.status, - }, - }; + const { pendingOrders } = teamOrderListSerialization([payload]); + let updateObject; + if (pendingOrders.length > 0) { + const { hardwareInTableRow } = pendingOrders[0]; + + updateObject = { + id: payload.id, + changes: { + status: payload.status, + hardwareInTableRow, + }, + }; + } else { + updateObject = { + id: payload.id, + changes: { + status: payload.status, + }, + }; + } teamOrders.updateOne(state, updateObject); }); builder.addCase(updateOrderStatus.rejected, (state, { payload }) => { diff --git a/hackathon_site/dashboard/frontend/src/testing/mockData.tsx b/hackathon_site/dashboard/frontend/src/testing/mockData.tsx index 531cf58fe..8a3958694 100644 --- a/hackathon_site/dashboard/frontend/src/testing/mockData.tsx +++ b/hackathon_site/dashboard/frontend/src/testing/mockData.tsx @@ -839,6 +839,7 @@ export const mockPendingOrdersInTable: OrderInTable[] = [ id: 10, quantityRequested: 2, quantityGranted: 1, + quantityGrantedBySystem: 1, }, ], status: "Ready for Pickup", @@ -852,11 +853,13 @@ export const mockPendingOrdersInTable: OrderInTable[] = [ id: 1, quantityRequested: 2, quantityGranted: 2, + quantityGrantedBySystem: 2, }, { id: 4, quantityRequested: 1, quantityGranted: 1, + quantityGrantedBySystem: 1, }, ], status: "Submitted", @@ -870,11 +873,13 @@ export const mockPendingOrdersInTable: OrderInTable[] = [ id: 3, quantityRequested: 1, quantityGranted: 1, + quantityGrantedBySystem: 1, }, { id: 4, quantityRequested: 1, quantityGranted: 1, + quantityGrantedBySystem: 1, }, ], status: "Ready for Pickup", @@ -891,6 +896,7 @@ export const mockCheckedOutOrdersInTable: OrderInTable[] = [ id: 10, quantityRequested: 3, quantityGranted: 2, + quantityGrantedBySystem: 2, }, ], status: "Picked Up", @@ -904,11 +910,13 @@ export const mockCheckedOutOrdersInTable: OrderInTable[] = [ id: 1, quantityRequested: 2, quantityGranted: 1, + quantityGrantedBySystem: 1, }, { id: 2, quantityRequested: 1, quantityGranted: 1, + quantityGrantedBySystem: 1, }, ], status: "Picked Up", @@ -922,6 +930,7 @@ export const mockCheckedOutOrdersInTable: OrderInTable[] = [ id: 1, quantityRequested: 2, quantityGranted: 2, + quantityGrantedBySystem: 2, }, ], status: "Picked Up", diff --git a/hackathon_site/hackathon_site/settings/__init__.py b/hackathon_site/hackathon_site/settings/__init__.py index ebfb97519..f7bcef63a 100644 --- a/hackathon_site/hackathon_site/settings/__init__.py +++ b/hackathon_site/hackathon_site/settings/__init__.py @@ -196,7 +196,7 @@ ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], - "PAGE_SIZE": 100, + "PAGE_SIZE": 1000, "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend",], } diff --git a/hackathon_site/hardware/migrations/0012_alter_orderitem_part_returned_health.py b/hackathon_site/hardware/migrations/0012_alter_orderitem_part_returned_health.py new file mode 100644 index 000000000..543423bbd --- /dev/null +++ b/hackathon_site/hardware/migrations/0012_alter_orderitem_part_returned_health.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.11 on 2023-06-10 18:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("hardware", "0011_alter_order_team"), + ] + + operations = [ + migrations.AlterField( + model_name="orderitem", + name="part_returned_health", + field=models.CharField( + blank=True, + choices=[ + ("Healthy", "Healthy"), + ("Heavily Used", "Heavily Used"), + ("Broken", "Broken"), + ("Lost", "Lost"), + ("Rejected", "Rejected"), + ], + max_length=64, + null=True, + ), + ), + ] diff --git a/hackathon_site/hardware/models.py b/hackathon_site/hardware/models.py index 241e5ed9e..5d3c9a3f1 100644 --- a/hackathon_site/hardware/models.py +++ b/hackathon_site/hardware/models.py @@ -28,6 +28,10 @@ def get_queryset(self): "order_items", filter=( ~Q(order_items__part_returned_health="Healthy") + & ~( + Q(order_items__part_returned_health="Rejected") + & Q(order_items__order__status="Picked Up") + ) & ~Q(order_items__order__status="Cancelled") ), distinct=True, @@ -85,6 +89,7 @@ class OrderItem(models.Model): ("Heavily Used", "Heavily Used"), ("Broken", "Broken"), ("Lost", "Lost"), + ("Rejected", "Rejected"), ] order = models.ForeignKey( "Order", null=False, on_delete=models.CASCADE, related_name="items" diff --git a/hackathon_site/hardware/serializers.py b/hackathon_site/hardware/serializers.py index 130ab7468..0aef5b216 100644 --- a/hackathon_site/hardware/serializers.py +++ b/hackathon_site/hardware/serializers.py @@ -161,7 +161,7 @@ def get_team_code(obj: Order): class OrderChangeSerializer(OrderListSerializer): change_options = { "Submitted": ["Cancelled", "Ready for Pickup"], - "Ready for Pickup": ["Picked Up"], + "Ready for Pickup": ["Picked Up", "Submitted"], "Picked Up": ["Returned"], } @@ -191,6 +191,33 @@ def validate_status(self, data): ) return data + def update(self, instance: Order, validated_data): + status = validated_data.pop("status", None) + request = validated_data.pop("request", None) + + if status is not None: + instance.status = status + if request is not None: + for item in request: + items_in_order = list( + OrderItem.objects.filter(hardware=item["id"], order=instance.pk) + ) + num_items_in_order = len(items_in_order) + requested_number = item["requested_quantity"] + + if requested_number > num_items_in_order: + raise serializers.ValidationError( + f"Cannot increase the number of Hardware item number {items_in_order[0].hardware} to more than the originally ordered {requested_number} items." + ) + for idx in range(0, num_items_in_order): + if idx < num_items_in_order - requested_number: + items_in_order[idx].part_returned_health = "Rejected" + else: + items_in_order[idx].part_returned_health = None + items_in_order[idx].save() + + return serializers.ModelSerializer.update(self, instance, validated_data) + class TeamOrderChangeSerializer(OrderChangeSerializer): change_options = { diff --git a/hackathon_site/hardware/views.py b/hackathon_site/hardware/views.py index c4d1a3992..8fcaea4a9 100644 --- a/hackathon_site/hardware/views.py +++ b/hackathon_site/hardware/views.py @@ -283,6 +283,8 @@ def patch(self, request, *args, **kwargs): connection.open() try: + if response.data["status"] == "Submitted": + return response render_to_string_context = { "recipient": "Hardware Inventory Admins", "order": response.data,