From 85f3f4761ad9d9c4bd422a46213e38322c92950b Mon Sep 17 00:00:00 2001 From: Winston Hsiao <96440583+Winston-Hsiao@users.noreply.github.com> Date: Wed, 16 Oct 2024 19:11:13 -0400 Subject: [PATCH] Edit order delivery address (#482) --- .../components/modals/EditAddressModal.tsx | 174 ++++++++++++++++++ frontend/src/components/orders/OrderCard.tsx | 49 ++++- frontend/src/gen/api.ts | 69 +++++++ store/app/crud/orders.py | 29 +++ store/app/routers/orders.py | 26 +++ 5 files changed, 343 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/modals/EditAddressModal.tsx diff --git a/frontend/src/components/modals/EditAddressModal.tsx b/frontend/src/components/modals/EditAddressModal.tsx new file mode 100644 index 00000000..83a6be60 --- /dev/null +++ b/frontend/src/components/modals/EditAddressModal.tsx @@ -0,0 +1,174 @@ +import React, { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { paths } from "@/gen/api"; +import { useAlertQueue } from "@/hooks/useAlertQueue"; +import { useAuthentication } from "@/hooks/useAuth"; + +type Order = + paths["/orders/get_user_orders"]["get"]["responses"][200]["content"]["application/json"][0]; + +interface EditAddressModalProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + order: Order; + onOrderUpdate: (updatedOrder: Order) => void; +} + +const EditAddressModal: React.FC = ({ + isOpen, + onOpenChange, + order, + onOrderUpdate, +}) => { + const { client } = useAuthentication(); + const { addAlert, addErrorAlert } = useAlertQueue(); + const [address, setAddress] = useState({ + shipping_name: order.shipping_name || "", + shipping_address_line1: order.shipping_address_line1 || "", + shipping_address_line2: order.shipping_address_line2 || "", + shipping_city: order.shipping_city || "", + shipping_state: order.shipping_state || "", + shipping_postal_code: order.shipping_postal_code || "", + shipping_country: order.shipping_country || "", + }); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setAddress((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const { data, error } = await client.PUT( + "/orders/update_order_address/{order_id}", + { + params: { path: { order_id: order.id } }, + body: address, + }, + ); + + if (error) { + addErrorAlert("Failed to update delivery address"); + console.error("Error updating address:", error); + } else { + addAlert("Delivery address updated", "success"); + onOrderUpdate(data); + } + + onOpenChange(false); + } catch (error) { + console.error("Error updating address:", error); + } + }; + + return ( + + + + Edit Delivery Address + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ ); +}; + +export default EditAddressModal; diff --git a/frontend/src/components/orders/OrderCard.tsx b/frontend/src/components/orders/OrderCard.tsx index 7039119c..0a178103 100644 --- a/frontend/src/components/orders/OrderCard.tsx +++ b/frontend/src/components/orders/OrderCard.tsx @@ -1,5 +1,12 @@ -import React from "react"; +import React, { useState } from "react"; +import EditAddressModal from "@/components/modals/EditAddressModal"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import type { paths } from "@/gen/api"; import { formatPrice } from "@/lib/utils/formatNumber"; import { normalizeStatus } from "@/lib/utils/formatString"; @@ -7,6 +14,9 @@ import { normalizeStatus } from "@/lib/utils/formatString"; type OrderWithProduct = paths["/orders/get_user_orders_with_products"]["get"]["responses"][200]["content"]["application/json"][0]; +type Order = + paths["/orders/get_user_orders"]["get"]["responses"][200]["content"]["application/json"][0]; + const orderStatuses = [ "processing", "in_development", @@ -27,9 +37,14 @@ const activeStatuses = [ const redStatuses = ["cancelled", "refunded", "failed"]; const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({ - orderWithProduct, + orderWithProduct: initialOrderWithProduct, }) => { + const [orderWithProduct, setOrderWithProduct] = useState( + initialOrderWithProduct, + ); const { order, product } = orderWithProduct; + const [isEditAddressModalOpen, setIsEditAddressModalOpen] = useState(false); + const currentStatusIndex = orderStatuses.indexOf(order.status); const isRedStatus = redStatuses.includes(order.status); const showStatusBar = activeStatuses.includes(order.status); @@ -50,6 +65,10 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({ const unitPrice = order.amount / order.quantity; + const handleOrderUpdate = (updatedOrder: Order) => { + setOrderWithProduct((prev) => ({ ...prev, order: updatedOrder })); + }; + return (

{product.name}

@@ -59,9 +78,24 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({ {normalizeStatus(order.status)}

-
+

Order ID: {order.id}

-

Quantity: {order.quantity}

+
+ + + Manage order + + + setIsEditAddressModalOpen(true)} + className="cursor-pointer hover:bg-gray-100" + > + Change delivery address + + + +
+

Quantity: {order.quantity}x

{formatPrice(order.amount)}{" "} @@ -161,6 +195,13 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({ className="w-full h-64 object-cover rounded-md mt-2" /> )} + +

); }; diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index ec1351b7..4ece8b1b 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -973,6 +973,23 @@ export interface paths { patch?: never; trace?: never; }; + "/orders/update_order_address/{order_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update Order Address */ + put: operations["update_order_address_orders_update_order_address__order_id__put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -1443,6 +1460,23 @@ export interface components { /** Tags */ tags?: string[] | null; }; + /** UpdateOrderAddressRequest */ + UpdateOrderAddressRequest: { + /** Shipping Name */ + shipping_name: string; + /** Shipping Address Line1 */ + shipping_address_line1: string; + /** Shipping Address Line2 */ + shipping_address_line2: string | null; + /** Shipping City */ + shipping_city: string; + /** Shipping State */ + shipping_state: string; + /** Shipping Postal Code */ + shipping_postal_code: string; + /** Shipping Country */ + shipping_country: string; + }; /** UpdateUserRequest */ UpdateUserRequest: { /** Email */ @@ -3276,4 +3310,39 @@ export interface operations { }; }; }; + update_order_address_orders_update_order_address__order_id__put: { + parameters: { + query?: never; + header?: never; + path: { + order_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateOrderAddressRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Order"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; } diff --git a/store/app/crud/orders.py b/store/app/crud/orders.py index bb224bef..e5fe172e 100644 --- a/store/app/crud/orders.py +++ b/store/app/crud/orders.py @@ -1,5 +1,7 @@ """This module provides CRUD operations for orders.""" +from pydantic import ValidationError + from store.app.crud.base import BaseCrud, ItemNotFoundError from store.app.model import Order @@ -24,3 +26,30 @@ async def get_order(self, order_id: str) -> Order | None: async def get_order_by_session_id(self, session_id: str) -> Order | None: orders = await self._get_items_from_secondary_index("stripe_checkout_session_id", session_id, Order) return orders[0] if orders else None + + async def update_order(self, order_id: str, update_data: dict) -> Order: + order = await self.get_order(order_id) + if not order: + raise ItemNotFoundError("Order not found") + + # Create a dict of current order data + current_data = order.model_dump() + + # Update with new data + current_data.update(update_data) + + try: + # Validate the updated data + Order(**current_data) + except ValidationError as e: + # If validation fails, raise an error + raise ValueError(f"Invalid update data: {str(e)}") + + # If validation passes, update the order + await self._update_item(order_id, Order, update_data) + + # Fetch and return the updated order + updated_order = await self.get_order(order_id) + if not updated_order: + raise ItemNotFoundError("Updated order not found") + return updated_order diff --git a/store/app/routers/orders.py b/store/app/routers/orders.py index 8b488f84..5fb10dcb 100644 --- a/store/app/routers/orders.py +++ b/store/app/routers/orders.py @@ -78,3 +78,29 @@ async def get_user_orders_with_products( return orders_with_products except ItemNotFoundError: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No orders found for this user") + + +class UpdateOrderAddressRequest(BaseModel): + shipping_name: str + shipping_address_line1: str + shipping_address_line2: str | None + shipping_city: str + shipping_state: str + shipping_postal_code: str + shipping_country: str + + +@orders_router.put("/update_order_address/{order_id}", response_model=Order) +async def update_order_address( + order_id: str, + address_update: UpdateOrderAddressRequest, + user: User = Depends(get_session_user_with_read_permission), + crud: Crud = Depends(Crud.get), +) -> Order: + order = await crud.get_order(order_id) + if order is None or order.user_id != user.id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found") + + # Update the order with the new address + updated_order = await crud.update_order(order_id, address_update.dict()) + return updated_order