From 9488df0b41358a3a5822fc4cad58c9accc40f723 Mon Sep 17 00:00:00 2001 From: nn2-minh <Nguyen12.Minh@live.uwe.ac.uk> Date: Mon, 21 Apr 2025 22:06:56 +0700 Subject: [PATCH] add ruff format --- app/backend/dummy_data.py | 6 +- app/backend/models/models.py | 4 +- app/backend/routes/cart.py | 198 +++++------ app/backend/routes/order.py | 138 ++++---- .../components/product/view_product.py | 3 +- app/frontend/components/user_orders.py | 327 ++++++++++-------- app/frontend/main.py | 31 +- 7 files changed, 367 insertions(+), 340 deletions(-) diff --git a/app/backend/dummy_data.py b/app/backend/dummy_data.py index a6035ee..cd09f64 100644 --- a/app/backend/dummy_data.py +++ b/app/backend/dummy_data.py @@ -198,17 +198,17 @@ def insert_dummy_data(session: Session): ] session.add_all(order_items) session.commit() - + if not session.query(Cart).first(): # Create example cart for the first user cart = Cart(user_id=1) session.add(cart) session.commit() - + # Add a couple of items to the cart cart_items = [ CartItem(cart_id=cart.id, product_id=3, quantity=2, price=102.99), - CartItem(cart_id=cart.id, product_id=4, quantity=1, price=103.99) + CartItem(cart_id=cart.id, product_id=4, quantity=1, price=103.99), ] session.add_all(cart_items) session.commit() diff --git a/app/backend/models/models.py b/app/backend/models/models.py index 4c1f1d9..8c343c8 100644 --- a/app/backend/models/models.py +++ b/app/backend/models/models.py @@ -109,7 +109,7 @@ class Cart(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") created_at: datetime = Field(default_factory=datetime.utcnow) - + # Relationships user: Optional["User"] = Relationship(back_populates="carts") cart_items: List["CartItem"] = Relationship(back_populates="cart") @@ -121,7 +121,7 @@ class CartItem(SQLModel, table=True): product_id: int = Field(foreign_key="product.id") quantity: int price: float - + # Relationships cart: Optional["Cart"] = Relationship(back_populates="cart_items") product: Optional["Product"] = Relationship() diff --git a/app/backend/routes/cart.py b/app/backend/routes/cart.py index b481a2d..f5bbe2d 100644 --- a/app/backend/routes/cart.py +++ b/app/backend/routes/cart.py @@ -4,8 +4,24 @@ from geopy.geocoders import Nominatim from geopy.distance import geodesic from backend.database import get_session from backend.routes.auth import get_current_user -from backend.models.models import Order, OrderItem, User, Product, Shop, Payment, Cart, CartItem -from backend.schemas.order import OrderCreate, OrderRead, OrderUpdate, CartItemCreate, CartItemRead, CartRead +from backend.models.models import ( + Order, + OrderItem, + User, + Product, + Shop, + Payment, + Cart, + CartItem, +) +from backend.schemas.order import ( + OrderCreate, + OrderRead, + OrderUpdate, + CartItemCreate, + CartItemRead, + CartRead, +) router = APIRouter() @@ -21,34 +37,31 @@ def add_to_cart( product = session.get(Product, cart_item.product_id) if not product: raise HTTPException(status_code=404, detail="Product not found") - + if product.stock < cart_item.quantity: raise HTTPException(status_code=400, detail="Not enough stock available") - + # Verify the shop exists shop = session.get(Shop, cart_item.shop_id) if not shop: raise HTTPException(status_code=404, detail="Shop not found") - + # Get or create a cart for the user - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: cart = Cart(user_id=current_user.id) session.add(cart) session.commit() session.refresh(cart) - + # Check if the item already exists in the cart existing_item = session.exec( select(CartItem).where( - CartItem.cart_id == cart.id, - CartItem.product_id == cart_item.product_id + CartItem.cart_id == cart.id, CartItem.product_id == cart_item.product_id ) ).first() - + if existing_item: # Update quantity if item already exists existing_item.quantity += cart_item.quantity @@ -59,12 +72,12 @@ def add_to_cart( cart_id=cart.id, product_id=cart_item.product_id, quantity=cart_item.quantity, - price=product.price + price=product.price, ) session.add(new_cart_item) - + session.commit() - + return {"message": "Item added to cart successfully"} @@ -75,18 +88,14 @@ def get_cart_items( ): """Get all items in the user's cart with product details""" # Get user's cart - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: return {"items": []} - + # Get cart items with product details - cart_items = session.exec( - select(CartItem).where(CartItem.cart_id == cart.id) - ).all() - + cart_items = session.exec(select(CartItem).where(CartItem.cart_id == cart.id)).all() + items_with_details = [] for item in cart_items: product = session.get(Product, item.product_id) @@ -100,16 +109,16 @@ def get_cart_items( "shop_name": product.shop.name if product.shop else "Unknown Shop", "images": product.images, "quantity": item.quantity, - "subtotal": item.quantity * product.price + "subtotal": item.quantity * product.price, } items_with_details.append(product_dict) - + total_price = sum(item.get("subtotal", 0) for item in items_with_details) - + return { "items": items_with_details, "total_price": total_price, - "item_count": len(items_with_details) + "item_count": len(items_with_details), } @@ -124,51 +133,42 @@ def checkout_cart( delivery_address = checkout_data.get("delivery_address") payment_id = checkout_data.get("payment_id") selected_items = checkout_data.get("selected_items", []) - + if not delivery_address or not payment_id: - raise HTTPException( - status_code=400, - detail="Missing required parameters: delivery_address and payment_id" - ) - - if not selected_items: raise HTTPException( status_code=400, - detail="No items selected for checkout" + detail="Missing required parameters: delivery_address and payment_id", ) - + + if not selected_items: + raise HTTPException(status_code=400, detail="No items selected for checkout") + # Validate payment method payment = session.get(Payment, payment_id) if not payment or payment.user_id != current_user.id: raise HTTPException( - status_code=400, - detail="Invalid or unauthorized payment method" + status_code=400, detail="Invalid or unauthorized payment method" ) - + # Get user's cart - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: raise HTTPException(status_code=404, detail="Cart not found") - + # Get selected cart items cart_items = [] for item_id in selected_items: item = session.exec( - select(CartItem).where( - CartItem.id == item_id, - CartItem.cart_id == cart.id - ) + select(CartItem).where(CartItem.id == item_id, CartItem.cart_id == cart.id) ).first() - + if item: cart_items.append(item) - + if not cart_items: raise HTTPException(status_code=400, detail="No valid items selected") - + # Group cart items by shop shop_items = {} for item in cart_items: @@ -177,47 +177,45 @@ def checkout_cart( raise HTTPException( status_code=400, detail=f"Product {item.product_id} is out of stock" ) - + if product.shop_id not in shop_items: shop_items[product.shop_id] = [] - + shop_items[product.shop_id].append(item) - + # Create orders for each shop orders = [] for shop_id, items in shop_items.items(): shop = session.get(Shop, shop_id) - + # Geocode the delivery address geolocator = Nominatim(user_agent="order_locator") try: delivery_location = geolocator.geocode(delivery_address) if not delivery_location: # Fall back to London coordinates if geocoding fails - delivery_location = type('obj', (object,), { - 'latitude': 51.5074, - 'longitude': -0.1278 - }) + delivery_location = type( + "obj", (object,), {"latitude": 51.5074, "longitude": -0.1278} + ) except Exception as e: print(f"Geocoding error: {e}") # Fall back to London coordinates - delivery_location = type('obj', (object,), { - 'latitude': 51.5074, - 'longitude': -0.1278 - }) - + delivery_location = type( + "obj", (object,), {"latitude": 51.5074, "longitude": -0.1278} + ) + # Calculate the distance between the shop and the delivery location shop_location = (shop.latitude, shop.longitude) delivery_coordinates = (delivery_location.latitude, delivery_location.longitude) distance_km = geodesic(shop_location, delivery_coordinates).kilometers - + # Calculate the shipping price ($1 per km) shipping_price = distance_km * 1.0 - + # Calculate the total price product_total = sum(item.price * item.quantity for item in items) total_price = shipping_price + product_total - + # Create the order new_order = Order( user_id=current_user.id, @@ -233,7 +231,7 @@ def checkout_cart( session.add(new_order) session.commit() session.refresh(new_order) - + # Create order items and update product stock for item in items: order_item = OrderItem( @@ -243,19 +241,19 @@ def checkout_cart( price=item.price, ) session.add(order_item) - + # Update product stock product = session.get(Product, item.product_id) product.stock -= item.quantity session.add(product) - + # Remove checked out item from cart session.delete(item) - + orders.append(new_order.id) - + session.commit() - + return {"message": "Orders created successfully", "order_ids": orders} @@ -267,28 +265,23 @@ def remove_from_cart( ): """Remove an item from the cart""" # Get user's cart - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: raise HTTPException(status_code=404, detail="Cart not found") - + # Find the cart item cart_item = session.exec( - select(CartItem).where( - CartItem.id == item_id, - CartItem.cart_id == cart.id - ) + select(CartItem).where(CartItem.id == item_id, CartItem.cart_id == cart.id) ).first() - + if not cart_item: raise HTTPException(status_code=404, detail="Item not found in cart") - + # Remove the item session.delete(cart_item) session.commit() - + return {"message": "Item removed from cart successfully"} @@ -301,24 +294,19 @@ def update_cart_item( ): """Update the quantity of a cart item""" # Get user's cart - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: raise HTTPException(status_code=404, detail="Cart not found") - + # Find the cart item cart_item = session.exec( - select(CartItem).where( - CartItem.id == item_id, - CartItem.cart_id == cart.id - ) + select(CartItem).where(CartItem.id == item_id, CartItem.cart_id == cart.id) ).first() - + if not cart_item: raise HTTPException(status_code=404, detail="Item not found in cart") - + # Update quantity new_quantity = update_data.get("quantity") if new_quantity is not None: @@ -330,13 +318,15 @@ def update_cart_item( product = session.get(Product, cart_item.product_id) if not product: raise HTTPException(status_code=404, detail="Product not found") - + if product.stock < new_quantity: - raise HTTPException(status_code=400, detail="Not enough stock available") - + raise HTTPException( + status_code=400, detail="Not enough stock available" + ) + cart_item.quantity = new_quantity session.add(cart_item) - + session.commit() - - return {"message": "Cart item updated successfully"} \ No newline at end of file + + return {"message": "Cart item updated successfully"} diff --git a/app/backend/routes/order.py b/app/backend/routes/order.py index 46f456f..c3783b8 100644 --- a/app/backend/routes/order.py +++ b/app/backend/routes/order.py @@ -4,7 +4,16 @@ from geopy.geocoders import Nominatim from geopy.distance import geodesic from backend.database import get_session from backend.routes.auth import get_current_user -from backend.models.models import Order, OrderItem, User, Product, Shop, Payment, Cart, CartItem +from backend.models.models import ( + Order, + OrderItem, + User, + Product, + Shop, + Payment, + Cart, + CartItem, +) from backend.schemas.order import OrderCreate, OrderRead, OrderUpdate, CartItemCreate router = APIRouter() @@ -20,34 +29,31 @@ def add_to_cart( product = session.get(Product, cart_item.product_id) if not product: raise HTTPException(status_code=404, detail="Product not found") - + if product.stock < cart_item.quantity: raise HTTPException(status_code=400, detail="Not enough stock available") - + # Verify the shop exists shop = session.get(Shop, cart_item.shop_id) if not shop: raise HTTPException(status_code=404, detail="Shop not found") - + # Get or create a cart for the user - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: cart = Cart(user_id=current_user.id) session.add(cart) session.commit() session.refresh(cart) - + # Check if the item already exists in the cart existing_item = session.exec( select(CartItem).where( - CartItem.cart_id == cart.id, - CartItem.product_id == cart_item.product_id + CartItem.cart_id == cart.id, CartItem.product_id == cart_item.product_id ) ).first() - + if existing_item: # Update quantity if item already exists existing_item.quantity += cart_item.quantity @@ -58,12 +64,12 @@ def add_to_cart( cart_id=cart.id, product_id=cart_item.product_id, quantity=cart_item.quantity, - price=product.price + price=product.price, ) session.add(new_cart_item) - + session.commit() - + return {"message": "Item added to cart successfully"} @@ -73,18 +79,14 @@ def get_cart_items( current_user: User = Depends(get_current_user), ): # Get user's cart - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: return {"items": []} - + # Get cart items with product details - cart_items = session.exec( - select(CartItem).where(CartItem.cart_id == cart.id) - ).all() - + cart_items = session.exec(select(CartItem).where(CartItem.cart_id == cart.id)).all() + items_with_details = [] for item in cart_items: product = session.get(Product, item.product_id) @@ -95,10 +97,10 @@ def get_cart_items( "price": product.price, "shop_id": product.shop_id, "images": product.images, - "quantity": item.quantity + "quantity": item.quantity, } items_with_details.append(product_dict) - + return {"items": items_with_details} @@ -111,33 +113,32 @@ def checkout_cart( # Extract parameters from request body delivery_address = checkout_data.get("delivery_address") payment_id = checkout_data.get("payment_id") - + if not delivery_address or not payment_id: - raise HTTPException(status_code=400, detail="Missing required parameters: delivery_address and payment_id") - + raise HTTPException( + status_code=400, + detail="Missing required parameters: delivery_address and payment_id", + ) + # Get user's cart - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: raise HTTPException(status_code=404, detail="Cart not found") - + # Get cart items - cart_items = session.exec( - select(CartItem).where(CartItem.cart_id == cart.id) - ).all() - + cart_items = session.exec(select(CartItem).where(CartItem.cart_id == cart.id)).all() + if not cart_items: raise HTTPException(status_code=400, detail="Cart is empty") - + # Validate payment method payment = session.get(Payment, payment_id) if not payment or payment.user_id != current_user.id: raise HTTPException( status_code=400, detail="Invalid or unauthorized payment method" ) - + # Group cart items by shop shop_items = {} for item in cart_items: @@ -146,47 +147,45 @@ def checkout_cart( raise HTTPException( status_code=400, detail=f"Product {item.product_id} is out of stock" ) - + if product.shop_id not in shop_items: shop_items[product.shop_id] = [] - + shop_items[product.shop_id].append(item) - + # Create orders for each shop orders = [] for shop_id, items in shop_items.items(): shop = session.get(Shop, shop_id) - + # Geocode the delivery address geolocator = Nominatim(user_agent="order_locator") try: delivery_location = geolocator.geocode(delivery_address) if not delivery_location: # Fall back to London coordinates if geocoding fails - delivery_location = type('obj', (object,), { - 'latitude': 51.5074, - 'longitude': -0.1278 - }) + delivery_location = type( + "obj", (object,), {"latitude": 51.5074, "longitude": -0.1278} + ) except Exception as e: print(f"Geocoding error: {e}") # Fall back to London coordinates - delivery_location = type('obj', (object,), { - 'latitude': 51.5074, - 'longitude': -0.1278 - }) - + delivery_location = type( + "obj", (object,), {"latitude": 51.5074, "longitude": -0.1278} + ) + # Calculate the distance between the shop and the delivery location shop_location = (shop.latitude, shop.longitude) delivery_coordinates = (delivery_location.latitude, delivery_location.longitude) distance_km = geodesic(shop_location, delivery_coordinates).kilometers - + # Calculate the shipping price ($1 per km) shipping_price = distance_km * 1.0 - + # Calculate the total price product_total = sum(item.price * item.quantity for item in items) total_price = shipping_price + product_total - + # Create the order new_order = Order( user_id=current_user.id, @@ -202,7 +201,7 @@ def checkout_cart( session.add(new_order) session.commit() session.refresh(new_order) - + # Create order items and update product stock for item in items: order_item = OrderItem( @@ -212,18 +211,18 @@ def checkout_cart( price=item.price, ) session.add(order_item) - + # Update product stock product = session.get(Product, item.product_id) product.stock -= item.quantity session.add(product) - + orders.append(new_order.id) - + # Clear the cart after checkout session.exec(delete(CartItem).where(CartItem.cart_id == cart.id)) session.commit() - + return {"message": "Orders created successfully", "order_ids": orders} @@ -234,28 +233,25 @@ def remove_from_cart( current_user: User = Depends(get_current_user), ): # Get user's cart - cart = session.exec( - select(Cart).where(Cart.user_id == current_user.id) - ).first() - + cart = session.exec(select(Cart).where(Cart.user_id == current_user.id)).first() + if not cart: raise HTTPException(status_code=404, detail="Cart not found") - + # Find the cart item cart_item = session.exec( select(CartItem).where( - CartItem.cart_id == cart.id, - CartItem.product_id == product_id + CartItem.cart_id == cart.id, CartItem.product_id == product_id ) ).first() - + if not cart_item: raise HTTPException(status_code=404, detail="Item not found in cart") - + # Remove the item session.delete(cart_item) session.commit() - + return {"message": "Item removed from cart successfully"} diff --git a/app/frontend/components/product/view_product.py b/app/frontend/components/product/view_product.py index f7af0b4..780ed9f 100644 --- a/app/frontend/components/product/view_product.py +++ b/app/frontend/components/product/view_product.py @@ -483,8 +483,7 @@ def view_product_frame( messagebox.showinfo("Success", "Product added to cart successfully!") # Ask if user wants to view cart view_cart = messagebox.askyesno( - "View Cart", - "Would you like to view your cart?" + "View Cart", "Would you like to view your cart?" ) if view_cart: switch_func("user_orders") diff --git a/app/frontend/components/user_orders.py b/app/frontend/components/user_orders.py index 6ce2816..4696431 100644 --- a/app/frontend/components/user_orders.py +++ b/app/frontend/components/user_orders.py @@ -119,19 +119,19 @@ def user_orders_frame(parent, switch_func, API_URL, token): # Tab control for Cart and Order History tab_view = ctk.CTkTabview(content_frame, fg_color="transparent") tab_view.pack(fill="both", expand=True, padx=10, pady=10) - + # Create tabs cart_tab = tab_view.add("My Cart") orders_tab = tab_view.add("Order History") - + # CART TAB cart_frame = ctk.CTkScrollableFrame(cart_tab, fg_color="transparent") cart_frame.pack(fill="both", expand=True, padx=20, pady=20) - + # Cart title with refresh button title_frame = ctk.CTkFrame(cart_frame, fg_color="transparent") title_frame.pack(fill="x", pady=(0, 20)) - + cart_title = ctk.CTkLabel( title_frame, text="Your Shopping Cart", @@ -139,11 +139,11 @@ def user_orders_frame(parent, switch_func, API_URL, token): text_color=SHOPPING, ) cart_title.pack(side="left", anchor="w") - + def refresh_cart(): """Function to reload cart items""" load_cart_items() - + refresh_cart_btn = ctk.CTkButton( title_frame, text="🔄 Refresh", @@ -152,35 +152,35 @@ def user_orders_frame(parent, switch_func, API_URL, token): hover_color="#5A5A5A", width=100, height=28, - command=refresh_cart + command=refresh_cart, ) refresh_cart_btn.pack(side="right", padx=5) - + cart_items_frame = ctk.CTkFrame(cart_frame, fg_color="transparent") cart_items_frame.pack(fill="both", expand=True) - + # Checkout frame (initially empty) checkout_frame = ctk.CTkFrame(cart_frame, fg_color="transparent") checkout_frame.pack(fill="x", pady=10) - + def load_cart_items(): # Clear existing items for widget in cart_items_frame.winfo_children(): widget.destroy() - + # Clear checkout frame for widget in checkout_frame.winfo_children(): widget.destroy() - + try: headers = {"Authorization": f"Bearer {frame.token}"} response = requests.get(f"{API_URL}/cart/items", headers=headers) - + if response.status_code == 200: cart_data = response.json() cart_items = cart_data.get("items", []) total_price = cart_data.get("total_price", 0) - + if not cart_items: empty_label = ctk.CTkLabel( cart_items_frame, @@ -189,106 +189,118 @@ def user_orders_frame(parent, switch_func, API_URL, token): ) empty_label.pack(pady=20) return - + # Header row header_frame = ctk.CTkFrame(cart_items_frame, fg_color="#333333") header_frame.pack(fill="x", pady=(0, 10)) - + # Configure the same grid layout as the items - header_frame.grid_columnconfigure(0, weight=0, minsize=40) # Checkbox column - header_frame.grid_columnconfigure(1, weight=1, minsize=300) # Product column (expandable) - header_frame.grid_columnconfigure(2, weight=0, minsize=100) # Unit price column - header_frame.grid_columnconfigure(3, weight=0, minsize=100) # Quantity column - header_frame.grid_columnconfigure(4, weight=0, minsize=100) # Subtotal column - header_frame.grid_columnconfigure(5, weight=0, minsize=90) # Actions column - + header_frame.grid_columnconfigure( + 0, weight=0, minsize=40 + ) # Checkbox column + header_frame.grid_columnconfigure( + 1, weight=1, minsize=300 + ) # Product column (expandable) + header_frame.grid_columnconfigure( + 2, weight=0, minsize=100 + ) # Unit price column + header_frame.grid_columnconfigure( + 3, weight=0, minsize=100 + ) # Quantity column + header_frame.grid_columnconfigure( + 4, weight=0, minsize=100 + ) # Subtotal column + header_frame.grid_columnconfigure( + 5, weight=0, minsize=90 + ) # Actions column + # Checkbox header (select all) select_all_var = ctk.BooleanVar(value=False) selected_items = {} # Dictionary to track selected items {item_id: bool} - + def toggle_select_all(): for item_id, checkbox_var in selected_items.items(): checkbox_var.set(select_all_var.get()) - + select_all_cb = ctk.CTkCheckBox( - header_frame, - text="", + header_frame, + text="", variable=select_all_var, command=toggle_select_all, - width=30 + width=30, ) select_all_cb.grid(row=0, column=0, padx=5, pady=10, sticky="w") - + ctk.CTkLabel( header_frame, text="Product", font=("Helvetica", 12, "bold"), ).grid(row=0, column=1, padx=5, pady=10, sticky="w") - + ctk.CTkLabel( header_frame, text="Unit Price", font=("Helvetica", 12, "bold"), ).grid(row=0, column=2, padx=5, pady=10) - + ctk.CTkLabel( header_frame, text="Quantity", font=("Helvetica", 12, "bold"), ).grid(row=0, column=3, padx=5, pady=10) - + ctk.CTkLabel( header_frame, text="Subtotal", font=("Helvetica", 12, "bold"), ).grid(row=0, column=4, padx=5, pady=10) - + ctk.CTkLabel( header_frame, text="Actions", font=("Helvetica", 12, "bold"), ).grid(row=0, column=5, padx=5, pady=10) - + # Create items list for item in cart_items: item_id = item.get("id") # Create checkbox variable for this item selected_items[item_id] = ctk.BooleanVar(value=False) - + item_frame = create_cart_item_frame(item, selected_items[item_id]) item_frame.pack(fill="x", pady=5) - + # Add checkout section divider = ctk.CTkFrame(checkout_frame, fg_color="#444444", height=1) divider.pack(fill="x", pady=10) - + # Selected items summary def update_checkout_summary(): # Calculate selected items total selected_total = 0 selected_count = 0 - + for item in cart_items: item_id = item.get("id") if item_id in selected_items and selected_items[item_id].get(): selected_total += item.get("subtotal", 0) selected_count += 1 - + # Update summary label summary_label.configure( text=f"Selected Items: {selected_count} | Total: ₫{selected_total:,.0f}" ) - + # Update checkout button state if selected_count > 0: checkout_button.configure(state="normal") else: checkout_button.configure(state="disabled") - + # Create a frame for the summary bottom_frame = ctk.CTkFrame(checkout_frame, fg_color="transparent") bottom_frame.pack(fill="x", pady=15) - + # Create the summary label summary_label = ctk.CTkLabel( bottom_frame, @@ -297,39 +309,41 @@ def user_orders_frame(parent, switch_func, API_URL, token): text_color=SHOPPING, ) summary_label.pack(side="left", padx=10) - + # Function to update selection count (bound to each checkbox) def on_checkbox_change(): update_checkout_summary() - + # Check if all are selected all_selected = all(var.get() for var in selected_items.values()) select_all_var.set(all_selected) - + # Bind the change function to all checkboxes for var in selected_items.values(): var.trace_add("write", lambda *args: on_checkbox_change()) - + # Delivery address address_frame = ctk.CTkFrame(checkout_frame, fg_color="transparent") address_frame.pack(fill="x", pady=10) - + address_label = ctk.CTkLabel( address_frame, text="Delivery Address:", font=("Helvetica", 14), - text_color="#AAAAAA", + text_color="#AAAAAA", ) address_label.pack(side="left", padx=10) - + address_entry = ctk.CTkEntry(address_frame, width=300) address_entry.pack(side="left", padx=10) - address_entry.insert(0, "10 Downing Street, London, UK") # Default address - + address_entry.insert( + 0, "10 Downing Street, London, UK" + ) # Default address + # Payment selection - payment_frame = ctk.CTkFrame(checkout_frame, fg_color="transparent") + payment_frame = ctk.CTkFrame(checkout_frame, fg_color="transparent") payment_frame.pack(fill="x", pady=10) - + payment_label = ctk.CTkLabel( payment_frame, text="Payment Method:", @@ -337,40 +351,44 @@ def user_orders_frame(parent, switch_func, API_URL, token): text_color="#AAAAAA", ) payment_label.pack(side="left", padx=10) - + # Function to load payment methods def load_payment_methods(): payment_options.clear() payment_ids.clear() - + # Get payment methods try: - payments_response = requests.get(f"{API_URL}/payment/user", headers=headers) - + payments_response = requests.get( + f"{API_URL}/payment/user", headers=headers + ) + if payments_response.status_code == 200: payments = payments_response.json() for payment in payments: payment_text = f"{payment.get('payment_method')} ending in {payment.get('card_number')[-4:]}" payment_options.append(payment_text) - payment_ids[payment_text] = payment.get('id') - + payment_ids[payment_text] = payment.get("id") + if not payment_options: payment_options.append("No payment methods available") - + # Update dropdown payment_dropdown.configure(values=payment_options) payment_var.set(payment_options[0] if payment_options else "") except Exception as e: print(f"Error loading payment methods: {e}") - + # Get payment methods payment_options = [] payment_ids = {} - + # Create a frame to hold the dropdown and refresh button - payment_dropdown_frame = ctk.CTkFrame(payment_frame, fg_color="transparent") + payment_dropdown_frame = ctk.CTkFrame( + payment_frame, fg_color="transparent" + ) payment_dropdown_frame.pack(side="left", fill="x", expand=True) - + payment_var = ctk.StringVar(value="") payment_dropdown = ctk.CTkOptionMenu( payment_dropdown_frame, @@ -379,7 +397,7 @@ def user_orders_frame(parent, switch_func, API_URL, token): width=250, ) payment_dropdown.pack(side="left", padx=10) - + # Refresh button refresh_btn = ctk.CTkButton( payment_dropdown_frame, @@ -391,14 +409,14 @@ def user_orders_frame(parent, switch_func, API_URL, token): hover_color="#5A5A5A", ) refresh_btn.pack(side="left", padx=5) - + # Load payment methods initially load_payment_methods() - + def add_payment_method(): """Function to redirect to payment methods page""" switch_func("user_payments") - + add_payment_btn = ctk.CTkButton( payment_frame, text="Add Payment Method", @@ -409,64 +427,73 @@ def user_orders_frame(parent, switch_func, API_URL, token): width=150, ) add_payment_btn.pack(side="left", padx=10) - + # Checkout button def handle_checkout(): # Get selected item IDs selected_item_ids = [ item_id for item_id, var in selected_items.items() if var.get() ] - + if not selected_item_ids: messagebox.showerror("Error", "Please select at least one item") return - - if not payment_options or payment_options[0] == "No payment methods available": + + if ( + not payment_options + or payment_options[0] == "No payment methods available" + ): result = messagebox.askyesno( - "No Payment Method", - "You don't have any payment methods. Would you like to add one now?" + "No Payment Method", + "You don't have any payment methods. Would you like to add one now?", ) if result: switch_func("user_payments") return - + delivery_addr = address_entry.get().strip() if not delivery_addr: messagebox.showerror("Error", "Please enter a delivery address") return - + selected_payment = payment_var.get() payment_id = payment_ids.get(selected_payment) - + if not payment_id: messagebox.showerror("Error", "Invalid payment method selected") return - + try: checkout_response = requests.post( f"{API_URL}/cart/checkout", json={ - "delivery_address": delivery_addr, + "delivery_address": delivery_addr, "payment_id": payment_id, - "selected_items": selected_item_ids + "selected_items": selected_item_ids, }, - headers=headers + headers=headers, ) - + if checkout_response.status_code == 200: - messagebox.showinfo("Success", "Your order has been placed successfully!") + messagebox.showinfo( + "Success", "Your order has been placed successfully!" + ) # Refresh order history and clear cart view load_order_history() load_cart_items() # Switch to order history tab tab_view.set("Order History") else: - error_msg = checkout_response.json().get("detail", "Unknown error") - messagebox.showerror("Checkout Failed", f"Error: {error_msg}") - + error_msg = checkout_response.json().get( + "detail", "Unknown error" + ) + messagebox.showerror( + "Checkout Failed", f"Error: {error_msg}" + ) + except Exception as e: messagebox.showerror("Error", f"Checkout failed: {str(e)}") - + checkout_button = ctk.CTkButton( bottom_frame, text="Checkout Selected Items", @@ -476,53 +503,52 @@ def user_orders_frame(parent, switch_func, API_URL, token): command=handle_checkout, state="disabled", # Initially disabled until items are selected width=200, - height=40 + height=40, ) checkout_button.pack(side="right", padx=15) - + else: error_msg = response.json().get("detail", "Unknown error") messagebox.showerror("Error", f"Failed to load cart: {error_msg}") - + except Exception as e: messagebox.showerror("Error", f"Failed to load cart: {str(e)}") - + def create_cart_item_frame(item, checkbox_var): item_frame = ctk.CTkFrame(cart_items_frame, fg_color="#2b2b2b", corner_radius=5) - + # Create content frame - use grid layout for better alignment content = ctk.CTkFrame(item_frame, fg_color="transparent") content.pack(fill="x", padx=5, pady=10) - + # Configure grid columns with consistent widths - content.grid_columnconfigure(0, weight=0, minsize=40) # Checkbox column - content.grid_columnconfigure(1, weight=1, minsize=300) # Product column (expandable) - content.grid_columnconfigure(2, weight=0, minsize=100) # Unit price column - content.grid_columnconfigure(3, weight=0, minsize=100) # Quantity column - content.grid_columnconfigure(4, weight=0, minsize=100) # Subtotal column - content.grid_columnconfigure(5, weight=0, minsize=90) # Actions column - + content.grid_columnconfigure(0, weight=0, minsize=40) # Checkbox column + content.grid_columnconfigure( + 1, weight=1, minsize=300 + ) # Product column (expandable) + content.grid_columnconfigure(2, weight=0, minsize=100) # Unit price column + content.grid_columnconfigure(3, weight=0, minsize=100) # Quantity column + content.grid_columnconfigure(4, weight=0, minsize=100) # Subtotal column + content.grid_columnconfigure(5, weight=0, minsize=90) # Actions column + # Checkbox - column 0 item_checkbox = ctk.CTkCheckBox( - content, - text="", - variable=checkbox_var, - width=30 + content, text="", variable=checkbox_var, width=30 ) item_checkbox.grid(row=0, column=0, padx=5, sticky="w") - + # Product image and name - column 1 product_frame = ctk.CTkFrame(content, fg_color="transparent") product_frame.grid(row=0, column=1, padx=5, sticky="w") - + # Create horizontal layout for image and text img_frame = ctk.CTkFrame(product_frame, fg_color="#333333", width=60, height=60) img_frame.pack(side="left", padx=5, pady=5) img_frame.pack_propagate(False) - + img_label = ctk.CTkLabel(img_frame, text="") img_label.place(relx=0.5, rely=0.5, anchor="center") - + # Try to load product image images = item.get("images", []) if images: @@ -538,11 +564,11 @@ def user_orders_frame(parent, switch_func, API_URL, token): img_label.image = tk_img except Exception as e: print(f"Failed to load product image: {e}") - + # Product details details_frame = ctk.CTkFrame(product_frame, fg_color="transparent") details_frame.pack(side="left", fill="both", expand=True, padx=10) - + # Product name product_name = ctk.CTkLabel( details_frame, @@ -552,7 +578,7 @@ def user_orders_frame(parent, switch_func, API_URL, token): anchor="w", ) product_name.pack(anchor="w") - + # Shop name shop_name = ctk.CTkLabel( details_frame, @@ -563,7 +589,7 @@ def user_orders_frame(parent, switch_func, API_URL, token): anchor="w", ) shop_name.pack(anchor="w") - + # Unit price - column 2 unit_price = item.get("price", 0) price_label = ctk.CTkLabel( @@ -572,55 +598,59 @@ def user_orders_frame(parent, switch_func, API_URL, token): font=("Helvetica", 12), ) price_label.grid(row=0, column=2, padx=5) - + # Quantity adjustment - column 3 quantity_frame = ctk.CTkFrame(content, fg_color="#1f1f1f") quantity_frame.grid(row=0, column=3, padx=5) - + quantity = item.get("quantity", 1) - + def update_quantity(delta): nonlocal quantity item_id = item.get("id") new_quantity = quantity + delta - + if 1 <= new_quantity <= 99: try: headers = {"Authorization": f"Bearer {frame.token}"} response = requests.put( f"{API_URL}/cart/update/{item_id}", json={"quantity": new_quantity}, - headers=headers + headers=headers, ) - + if response.status_code == 200: # Update display quantity = new_quantity quantity_label.configure(text=str(quantity)) - + # Update subtotal subtotal = unit_price * quantity subtotal_label.configure(text=f"₫{subtotal:,.0f}") - + # Reload cart to refresh totals load_cart_items() else: error_msg = response.json().get("detail", "Unknown error") - messagebox.showerror("Error", f"Failed to update quantity: {error_msg}") - + messagebox.showerror( + "Error", f"Failed to update quantity: {error_msg}" + ) + except Exception as e: - messagebox.showerror("Error", f"Failed to update quantity: {str(e)}") - + messagebox.showerror( + "Error", f"Failed to update quantity: {str(e)}" + ) + minus_btn = ctk.CTkButton( - quantity_frame, - text="-", + quantity_frame, + text="-", width=25, height=25, font=("Helvetica", 12), - command=lambda: update_quantity(-1) + command=lambda: update_quantity(-1), ) minus_btn.pack(side="left", padx=2, pady=2) - + quantity_label = ctk.CTkLabel( quantity_frame, text=str(quantity), @@ -628,17 +658,17 @@ def user_orders_frame(parent, switch_func, API_URL, token): font=("Helvetica", 12), ) quantity_label.pack(side="left") - + plus_btn = ctk.CTkButton( - quantity_frame, - text="+", + quantity_frame, + text="+", width=25, height=25, font=("Helvetica", 12), - command=lambda: update_quantity(1) + command=lambda: update_quantity(1), ) plus_btn.pack(side="left", padx=2, pady=2) - + # Subtotal - column 4 subtotal = item.get("subtotal", 0) subtotal_label = ctk.CTkLabel( @@ -648,25 +678,24 @@ def user_orders_frame(parent, switch_func, API_URL, token): text_color=SHOPPING, ) subtotal_label.grid(row=0, column=4, padx=5) - + # Actions - column 5 def remove_item(): try: headers = {"Authorization": f"Bearer {frame.token}"} response = requests.delete( - f"{API_URL}/cart/remove/{item.get('id')}", - headers=headers + f"{API_URL}/cart/remove/{item.get('id')}", headers=headers ) - + if response.status_code == 200: load_cart_items() # Refresh cart else: error_msg = response.json().get("detail", "Unknown error") messagebox.showerror("Error", f"Failed to remove item: {error_msg}") - + except Exception as e: messagebox.showerror("Error", f"Failed to remove item: {str(e)}") - + remove_btn = ctk.CTkButton( content, text="Remove", @@ -677,7 +706,7 @@ def user_orders_frame(parent, switch_func, API_URL, token): command=remove_item, ) remove_btn.grid(row=0, column=5, padx=5) - + return item_frame # ORDER HISTORY TAB @@ -790,30 +819,32 @@ def user_orders_frame(parent, switch_func, API_URL, token): font=("Helvetica", 12), text_color="white", ).pack(side="left") - + # Order items items_frame = ctk.CTkFrame(details_frame, fg_color="transparent") items_frame.pack(fill="x", pady=5) - + ctk.CTkLabel( items_frame, text="Ordered Items:", font=("Helvetica", 12, "bold"), text_color="#AAAAAA", ).pack(anchor="w", padx=(10, 5), pady=(5, 0)) - + # Create a frame for each order item order_items = order.get("order_items", []) for item in order_items: item_frame = ctk.CTkFrame(items_frame, fg_color="#1e1e1e", corner_radius=5) item_frame.pack(fill="x", padx=10, pady=2) - + # Get product details try: headers = {"Authorization": f"Bearer {frame.token}"} product_id = item.get("product_id") - response = requests.get(f"{API_URL}/product/get/{product_id}", headers=headers) - + response = requests.get( + f"{API_URL}/product/get/{product_id}", headers=headers + ) + if response.status_code == 200: product = response.json() product_name = product.get("name", "Unknown Product") @@ -821,7 +852,7 @@ def user_orders_frame(parent, switch_func, API_URL, token): product_name = f"Product #{product_id}" except: product_name = f"Product #{product_id}" - + # Product info product_info = ctk.CTkLabel( item_frame, @@ -830,7 +861,7 @@ def user_orders_frame(parent, switch_func, API_URL, token): text_color="white", ) product_info.pack(side="left", padx=10, pady=5) - + # Item price item_price = item.get("price", 0) * item.get("quantity", 0) price_label = ctk.CTkLabel( diff --git a/app/frontend/main.py b/app/frontend/main.py index 7a2c8a8..9e483ad 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -29,17 +29,22 @@ def switch_frame(frame_name, *args): Optionally, additional data (like shop_data) can be passed as extra arguments. """ global access_token - + # Special handling for dashboard after login if frame_name == "dashboard" and args and isinstance(args[0], str): # This is coming from login with a new token access_token = args[0] print(f"New login detected. Setting token: {access_token[:10]}...") - + # Recreate all frames that need the token to ensure they start with the correct token initialize_authenticated_frames(access_token) - # For other frames, update token if provided - elif args and args[0] and isinstance(args[0], str) and frame_name not in ["login", "register"]: + # For other frames, update token if provided + elif ( + args + and args[0] + and isinstance(args[0], str) + and frame_name not in ["login", "register"] + ): access_token = args[0] print(f"Token updated in switch_frame: {access_token[:10]}...") # Update token for all frames that need it @@ -58,13 +63,17 @@ def switch_frame(frame_name, *args): frame.refresh_data(*args) # Make sure we have a valid token for authenticated pages - if frame_name not in ["login", "register"] and (access_token is None or access_token == ""): + if frame_name not in ["login", "register"] and ( + access_token is None or access_token == "" + ): print("No valid token, redirecting to login") frames["login"].tkraise() return - + if frame_name not in ["login", "register"]: - print(f"Switching to {frame_name} with token: {access_token[:10] if access_token else 'None'}") + print( + f"Switching to {frame_name} with token: {access_token[:10] if access_token else 'None'}" + ) frame.tkraise() @@ -72,18 +81,20 @@ def switch_frame(frame_name, *args): def initialize_authenticated_frames(token): """Initialize or reinitialize all frames that require authentication""" global frames - + # Recreate authenticated frames with the new token frames["create_shop"] = create_shop_frame(root, switch_frame, API_URL, token) frames["view_shop"] = view_shop_frame(root, switch_frame, API_URL, token, None) frames["create_product"] = create_product_frame(root, switch_frame, API_URL, token) - frames["view_product"] = view_product_frame(root, switch_frame, API_URL, token, None) + frames["view_product"] = view_product_frame( + root, switch_frame, API_URL, token, None + ) frames["category"] = category_frame(root, switch_frame, API_URL, token) frames["dashboard"] = dashboard_frame(root, switch_frame, API_URL, token) frames["user_details"] = user_details_frame(root, switch_frame, API_URL, token) frames["user_orders"] = user_orders_frame(root, switch_frame, API_URL, token) frames["user_payments"] = user_payments_frame(root, switch_frame, API_URL, token) - + # Place all authenticated frames for key, frame in frames.items(): if key not in ["login", "register"]: -- GitLab