diff --git a/README.md b/README.md index d45c9661804c16d0cf01e851fb27386d29230cd6..25ea2637d431530ed6138346d5cd67ca19f22bef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 1: Setup -create venv \ +create venv `pip install -r requirements.txt` # 2: Database @@ -14,8 +14,7 @@ create `.env` file based on `.env.example` in the main directory # 4: Run Backend API on your local machine -- open terminal and use \ - `uvicorn app.backend.main:app --reload` +- open terminal and use `uvicorn app.backend.main:app --reload` - open `127.0.0.1:8000/docs` on your browser # 5: Admin Dashboard @@ -34,6 +33,7 @@ The application includes an admin dashboard with the following features: The default admin user is automatically created on first run of the application. To run the full application (backend + frontend): + ``` python run_app.py ``` diff --git a/app/backend/dummy_data.py b/app/backend/dummy_data.py index 0cfe6e282477c65528fe19ebac5087f385ab7d61..69eecf3206fc25eb2e3ac9a3dcf1290ef72e324f 100644 --- a/app/backend/dummy_data.py +++ b/app/backend/dummy_data.py @@ -18,9 +18,9 @@ def check_dummy_data_exists(session: Session) -> bool: """Check if any dummy data already exists in the database""" checks = [ session.exec(select(User).where(User.email == "user@example.com")).first(), - session.exec(select(Shop).where(Shop.name == "Google HQ")).first(), - session.exec(select(Category).where(Category.name == "Category1")).first(), - session.exec(select(Product).where(Product.name == "Product1")).first(), + session.exec(select(Shop).where(Shop.name == "Adidas")).first(), + session.exec(select(Category).where(Category.name == "Clothing")).first(), + session.exec(select(Product).where(Product.name == "Adidas T-Shirt")).first(), ] return any(checks) @@ -91,160 +91,243 @@ def insert_dummy_data(session: Session): session.add_all(payments) session.commit() + # Create categories + if not session.exec(select(Category)).first(): + categories = [ + Category(name="Clothing"), + Category(name="Shoes"), + Category(name="Games"), + Category(name="Toys"), + ] + session.add_all(categories) + session.commit() + print("Categories created successfully!") + # Create shops with valid owner_id if not session.exec(select(Shop)).first() and "owner" in users: shops = [ Shop( owner_id=users["owner"].id, - name="Google HQ", - description="Google Headquarters", - image_url="app/static/default/default_shop.png", - address="1600 Amphitheatre Parkway, Mountain View, CA", - latitude=37.4220656, - longitude=-122.0840897, - ), - Shop( - owner_id=2, - name="Apple HQ", - description="Apple Headquarters", - image_url="app/static/default/default_shop.png", - address="1 Infinite Loop, Cupertino, CA", - latitude=37.33182, - longitude=-122.03118, + name="Adidas", + description="Adidas Official Store - Sportswear and Athletic Equipment", + image_url="app/static/default/Adidas/Adidas_shop_logo/adidas_logo.jpg", + address="1 Adidas Drive, Portland, OR 97201", + latitude=45.5155, + longitude=-122.6789, ), Shop( - owner_id=2, - name="Empire State Building", - description="Famous skyscraper in New York", - image_url="app/static/default/default_shop.png", - address="350 Fifth Avenue, New York, NY", - latitude=40.748817, - longitude=-73.985428, - ), - Shop( - owner_id=2, - name="Sherlock's Home", - description="Fictional detective's residence", - image_url="app/static/default/default_shop.png", - address="221B Baker Street, London, UK", - latitude=51.523767, - longitude=-0.1585557, - ), - Shop( - owner_id=2, - name="famous Eiffel Tower", - description="Iconic landmark in Paris", - image_url="app/static/default/default_shop.png", - address="New York, USA", - latitude=48.8588443, - longitude=2.2943506, - ), - Shop( - owner_id=2, - name="white house", - description="Iconic landmark in Paris", - image_url="app/static/default/default_shop.png", - address="Pennsylvania Avenue NW, Washington, DC", - latitude=48.8588443, - longitude=2.2943506, + owner_id=users["owner"].id, + name="Nike", + description="Nike Official Store - Just Do It", + image_url="app/static/default/Nike/Nike_shop_logo/nike_logo.jpg", + address="1 Bowerman Drive, Beaverton, OR 97005", + latitude=45.5152, + longitude=-122.6784, ), Shop( - owner_id=2, - name="another Eiffel Tower", - description="Iconic landmark in Paris", - image_url="app/static/default/default_shop.png", - address="Hanoi, Vietnam", - latitude=48.8588443, - longitude=2.2943506, + owner_id=users["owner"].id, + name="GameStop", + description="Your Ultimate Gaming Destination", + image_url="app/static/default/GameStop/GameStop_logo/gamestop_logo.jpg", + address="625 Westport Parkway, Grapevine, TX 76051", + latitude=32.9343, + longitude=-97.0787, ), Shop( - owner_id=2, - name="asdf", - description="Iconic landmark in Paris", - image_url="app/static/default/default_shop.png", - address="Hangzhou, China", - latitude=48.8588443, - longitude=2.2943506, + owner_id=users["owner"].id, + name="Lego", + description="Build Your Imagination with LEGO", + image_url="app/static/default/Lego/Lego_shop_logo/lego_logo.jpg", + address="1 LEGO Drive, Billund, Denmark 7190", + latitude=55.7348, + longitude=9.1158, ), ] session.add_all(shops) session.commit() print("Dummy shops created successfully!") - if not session.exec(select(Category)).first(): - categories = [Category(name=f"Category{i}") for i in range(1, 6)] - session.add_all(categories) - session.commit() + # Get category IDs + clothing_category = session.exec(select(Category).where(Category.name == "Clothing")).first() + shoes_category = session.exec(select(Category).where(Category.name == "Shoes")).first() + games_category = session.exec(select(Category).where(Category.name == "Games")).first() + toys_category = session.exec(select(Category).where(Category.name == "Toys")).first() + + # Get shop IDs + adidas_shop = session.exec(select(Shop).where(Shop.name == "Adidas")).first() + nike_shop = session.exec(select(Shop).where(Shop.name == "Nike")).first() + gamestop_shop = session.exec(select(Shop).where(Shop.name == "GameStop")).first() + lego_shop = session.exec(select(Shop).where(Shop.name == "Lego")).first() + # Create products if not session.exec(select(Product)).first(): products = [ + # Adidas products Product( - shop_id=i, - category_id=i, - name=f"Product{i}", - description=f"Description for Product {i}", - price=19.99 + (i * 10), - stock=10 * i, - ) - for i in range(1, 6) + shop_id=adidas_shop.id, + category_id=clothing_category.id, + name="Adidas T-Shirt", + description="Classic Adidas T-Shirt with Three Stripes", + price=29.99, + stock=100, + ), + Product( + shop_id=adidas_shop.id, + category_id=shoes_category.id, + name="Adidas Ultraboost", + description="Comfortable running shoes with Boost technology", + price=179.99, + stock=50, + ), + # Nike products + Product( + shop_id=nike_shop.id, + category_id=clothing_category.id, + name="Nike Dri-FIT Shirt", + description="Moisture-wicking performance shirt", + price=34.99, + stock=100, + ), + Product( + shop_id=nike_shop.id, + category_id=shoes_category.id, + name="Nike Air Max", + description="Classic Air Max sneakers with visible air unit", + price=159.99, + stock=50, + ), + # GameStop products + Product( + shop_id=gamestop_shop.id, + category_id=games_category.id, + name="PlayStation 5", + description="Next-gen gaming console", + price=499.99, + stock=20, + ), + Product( + shop_id=gamestop_shop.id, + category_id=games_category.id, + name="Xbox Series X", + description="Powerful gaming console", + price=499.99, + stock=20, + ), + # Lego products + Product( + shop_id=lego_shop.id, + category_id=toys_category.id, + name="LEGO Star Wars Millennium Falcon", + description="Iconic Star Wars spaceship building set", + price=159.99, + stock=30, + ), + Product( + shop_id=lego_shop.id, + category_id=toys_category.id, + name="LEGO City Police Station", + description="Police station building set with vehicles", + price=89.99, + stock=40, + ), ] session.add_all(products) session.commit() + print("Products created successfully!") + # Create product images if not session.exec(select(ProductImage)).first(): images = [ + # Adidas product images + ProductImage( + product_id=1, + image_url="app/static/default/Adidas/Product_image/jacket.jpg" + ), + ProductImage( + product_id=2, + image_url="app/static/default/Adidas/Product_image/shoes_adidas.jpg" + ), + # Nike product images + ProductImage( + product_id=3, + image_url="app/static/default/Nike/Product_nike_image/sweater.jpg" + ), + ProductImage( + product_id=4, + image_url="app/static/default/Nike/Product_nike_image/shoes.jpg" + ), + # GameStop product images + ProductImage( + product_id=5, + image_url="app/static/default/GameStop/Product_gamestop_imagee/sony_playstation_ps5_slim_digital.jpg" + ), + ProductImage( + product_id=6, + image_url="app/static/default/GameStop/Product_gamestop_imagee/Call_of_duty_modern_warfare_ps5.jpg" + ), + # Lego product images ProductImage( - product_id=i, image_url="app/static/default/default_product.png" - ) - for i in range(1, 6) + product_id=7, + image_url="app/static/default/Lego/Product_lego_image/lego_city.jpg" + ), + ProductImage( + product_id=8, + image_url="app/static/default/Lego/Product_lego_image/lego_city_special_edition.jpg" + ), ] session.add_all(images) session.commit() + print("Product images created successfully!") + # Create sample orders if not session.exec(select(Order)).first(): orders = [ Order( - user_id=1, - shop_id=1, + user_id=users["customer"].id, + shop_id=adidas_shop.id, payment_id=1, - total_price=65.96, + total_price=209.98, shipping_price=5.99, status="pending", - delivery_address="1600 Amphitheatre Parkway, Mountain View, CA", - delivery_latitude=37.4220656, - delivery_longitude=-122.0840897, + delivery_address="123 Customer St, Portland, OR", + delivery_latitude=45.5155, + delivery_longitude=-122.6789, ), Order( - user_id=1, - shop_id=2, + user_id=users["customer"].id, + shop_id=nike_shop.id, payment_id=2, - total_price=99.96, - shipping_price=9.99, + total_price=194.98, + shipping_price=5.99, status="shipped", - delivery_address="1 Infinite Loop, Cupertino, CA", - delivery_latitude=37.33182, - delivery_longitude=-122.03118, + delivery_address="123 Customer St, Portland, OR", + delivery_latitude=45.5155, + delivery_longitude=-122.6789, ), ] session.add_all(orders) session.commit() + # Create order items if not session.exec(select(OrderItem)).first(): order_items = [ - OrderItem(order_id=1, product_id=1, quantity=3, price=19.99), - OrderItem(order_id=2, product_id=2, quantity=3, price=29.99), + OrderItem(order_id=1, product_id=1, quantity=1, price=29.99), + OrderItem(order_id=1, product_id=2, quantity=1, price=179.99), + OrderItem(order_id=2, product_id=3, quantity=1, price=34.99), + OrderItem(order_id=2, product_id=4, quantity=1, price=159.99), ] session.add_all(order_items) session.commit() + # Create cart and cart items if not session.exec(select(Cart)).first(): - cart = Cart(user_id=1) + cart = Cart(user_id=users["customer"].id) session.add(cart) session.commit() cart_items = [ - CartItem(cart_id=cart.id, product_id=3, quantity=2, price=39.99), - CartItem(cart_id=cart.id, product_id=4, quantity=1, price=49.99), + CartItem(cart_id=cart.id, product_id=5, quantity=1, price=499.99), + CartItem(cart_id=cart.id, product_id=7, quantity=1, price=159.99), ] session.add_all(cart_items) session.commit() diff --git a/app/backend/models/models.py b/app/backend/models/models.py index d968823d42a2ff0e582695ef7f6e580065e96122..40e4d3b5dfc18f3f8779e5d6f0647fa4ed6ee759 100644 --- a/app/backend/models/models.py +++ b/app/backend/models/models.py @@ -94,6 +94,7 @@ class OrderItem(SQLModel, table=True): price: float order: Order = Relationship(back_populates="order_items") + product: Optional["Product"] = Relationship() class Payment(SQLModel, table=True): diff --git a/app/backend/routes/admin.py b/app/backend/routes/admin.py index 54e5a3dfdff7157ab0b0fdf639a438c0b4f1fb35..21adfbeda40437c747a7304b01e68841942879e9 100644 --- a/app/backend/routes/admin.py +++ b/app/backend/routes/admin.py @@ -220,7 +220,10 @@ def get_user_statistics( # Update counts for day_data in user_counts: - day_str = day_data[0].strftime("%Y-%m-%d") + if isinstance(day, str): + day_str = day # Already correct + else: + day_str = day.strftime("%Y-%m-%d") users_by_day[day_str] = day_data[1] # Convert to list of dictionaries diff --git a/app/backend/routes/order.py b/app/backend/routes/order.py index 5dd91facd8cb8c6dcafe53a0ef855be582f70644..697d81bccb91b2faffcca55283a0761e392e5bd2 100644 --- a/app/backend/routes/order.py +++ b/app/backend/routes/order.py @@ -13,6 +13,7 @@ from app.backend.models.models import ( Payment, Cart, CartItem, + Category, ) from app.backend.schemas.order import ( OrderCreate, @@ -20,7 +21,7 @@ from app.backend.schemas.order import ( OrderUpdate, CartItemCreate, ) -from typing import Optional +from typing import Optional, Dict, Any router = APIRouter() @@ -368,13 +369,54 @@ def list_orders( session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): - query = select(Order).where(Order.user_id == current_user.id) + query = ( + select(Order, User) + .join(User, Order.user_id == User.id) + .where(Order.user_id == current_user.id) + ) # Filter by status if provided if status: query = query.where(Order.status == status) - orders = session.exec(query).all() + results = session.exec(query).all() + orders = [result[0] for result in results] + + # Set the user attribute on each order + for i, order in enumerate(orders): + order.user = results[i][1] + + # Fetch order items with product and category information + for order in orders: + order_items_query = ( + select(OrderItem, Product, Category) + .join(Product, OrderItem.product_id == Product.id) + .join(Category, Product.category_id == Category.id, isouter=True) + .where(OrderItem.order_id == order.id) + ) + order_items_results = session.exec(order_items_query).all() + + # Create a dictionary to store product and category information + product_info: Dict[int, Dict[str, Any]] = {} + + # Store product and category information + for item_result in order_items_results: + order_item, product, category = item_result + product_info[order_item.product_id] = { + "product": product, + "category": category, + } + + # Update order items with product and category information + for order_item in order.order_items: + if order_item.product_id in product_info: + info = product_info[order_item.product_id] + # Set the product attribute on the order item + setattr(order_item, "product", info["product"]) + # Set the category attribute on the product + if info["category"]: + setattr(info["product"], "category", info["category"]) + return orders @@ -393,14 +435,55 @@ def get_shop_orders( status_code=403, detail="Unauthorized to access this shop's orders" ) - # Query orders for this shop - query = select(Order).where(Order.shop_id == shop_id) + # Query orders for this shop with user information + query = ( + select(Order, User) + .join(User, Order.user_id == User.id) + .where(Order.shop_id == shop_id) + ) # Filter by status if provided if status: query = query.where(Order.status == status) - orders = session.exec(query).all() + results = session.exec(query).all() + orders = [result[0] for result in results] + + # Set the user attribute on each order + for i, order in enumerate(orders): + order.user = results[i][1] + + # Fetch order items with product and category information + for order in orders: + order_items_query = ( + select(OrderItem, Product, Category) + .join(Product, OrderItem.product_id == Product.id) + .join(Category, Product.category_id == Category.id, isouter=True) + .where(OrderItem.order_id == order.id) + ) + order_items_results = session.exec(order_items_query).all() + + # Create a dictionary to store product and category information + product_info: Dict[int, Dict[str, Any]] = {} + + # Store product and category information + for item_result in order_items_results: + order_item, product, category = item_result + product_info[order_item.product_id] = { + "product": product, + "category": category, + } + + # Update order items with product and category information + for order_item in order.order_items: + if order_item.product_id in product_info: + info = product_info[order_item.product_id] + # Set the product attribute on the order item + setattr(order_item, "product", info["product"]) + # Set the category attribute on the product + if info["category"]: + setattr(info["product"], "category", info["category"]) + return orders @@ -411,10 +494,18 @@ def get_order( session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): - order = session.get(Order, order_id) - if not order: + query = ( + select(Order, User) + .join(User, Order.user_id == User.id) + .where(Order.id == order_id) + ) + result = session.exec(query).first() + + if not result: raise HTTPException(status_code=404, detail="Order not found") + order, user = result + # Allow access if user is the buyer or the shop owner shop = session.get(Shop, order.shop_id) if order.user_id != current_user.id and ( @@ -422,6 +513,36 @@ def get_order( ): raise HTTPException(status_code=403, detail="Unauthorized to access this order") + # Set the user attribute on the order + order.user = user + + # Fetch order items with product and category information + order_items_query = ( + select(OrderItem, Product, Category) + .join(Product, OrderItem.product_id == Product.id) + .join(Category, Product.category_id == Category.id, isouter=True) + .where(OrderItem.order_id == order.id) + ) + order_items_results = session.exec(order_items_query).all() + + # Create a dictionary to store product and category information + product_info: Dict[int, Dict[str, Any]] = {} + + # Store product and category information + for item_result in order_items_results: + order_item, product, category = item_result + product_info[order_item.product_id] = {"product": product, "category": category} + + # Update order items with product and category information + for order_item in order.order_items: + if order_item.product_id in product_info: + info = product_info[order_item.product_id] + # Set the product attribute on the order item + setattr(order_item, "product", info["product"]) + # Set the category attribute on the product + if info["category"]: + setattr(info["product"], "category", info["category"]) + return order diff --git a/app/backend/schemas/order.py b/app/backend/schemas/order.py index 53ff02b05a4871a26ee877f9205b9e11668efa42..8a6a3d84b92c50373d60489eb99e0420d1a5e245 100644 --- a/app/backend/schemas/order.py +++ b/app/backend/schemas/order.py @@ -1,6 +1,8 @@ from pydantic import BaseModel, ConfigDict from typing import List, Optional from datetime import datetime +from app.backend.schemas.user import UserRead +from app.backend.schemas.product import ProductRead class OrderItemCreate(BaseModel): @@ -17,9 +19,10 @@ class CartItemCreate(BaseModel): class CartItemRead(BaseModel): id: int + cart_id: int product_id: int quantity: int - price: float + price: Optional[float] = None model_config = ConfigDict(from_attributes=True) @@ -45,6 +48,7 @@ class OrderItemRead(BaseModel): product_id: int quantity: int price: float + product: Optional[ProductRead] = None model_config = ConfigDict(from_attributes=True) @@ -57,7 +61,9 @@ class OrderRead(BaseModel): shipping_price: float status: str created_at: datetime + delivery_address: str order_items: List[OrderItemRead] + user: UserRead model_config = ConfigDict(from_attributes=True) diff --git a/app/frontend/components/admin/category_management.py b/app/frontend/components/admin/category_management.py index c3db1642ffb70f62acd05c4c10ad1a02301b060d..228a72fa1814760691727b37ac98aac4a262f52e 100644 --- a/app/frontend/components/admin/category_management.py +++ b/app/frontend/components/admin/category_management.py @@ -112,89 +112,174 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): ) delete_button.pack(side="left") - # Data table frame + # Data table frame - using grid layout similar to owner_dashboard table_frame = ctk.CTkFrame(main_container, corner_radius=10) table_frame.pack(fill="both", expand=True) - # Configure Treeview style - style = ttk.Style() - style.theme_use("clam") # Use clam theme as base - - # Configure the Treeview colors to match app theme - style.configure( - "Treeview", - background="#2b2b2b", - foreground="#ffffff", - fieldbackground="#2b2b2b", - borderwidth=0, - rowheight=40, + # Create scrollable frame to contain the category table + categories_scrollable = ctk.CTkScrollableFrame( + table_frame, fg_color="#2b2b2b", corner_radius=5 ) + categories_scrollable.pack(fill="both", expand=True, padx=5, pady=5) - # Configure the headings - style.configure( - "Treeview.Heading", - background="#2e8b57", - foreground="#ffffff", - borderwidth=0, - font=("Helvetica", 12, "bold"), - ) + # Table headers + headers_frame = ctk.CTkFrame(categories_scrollable, fg_color="#9c27b0", height=30) + headers_frame.pack(fill="x", pady=(0, 1)) - # Selection color - style.map("Treeview", background=[("selected", "#2e8b57")]) - - # Create scrollbar - scrollbar = ttk.Scrollbar(table_frame) - scrollbar.pack(side="right", fill="y") - - # Create Treeview - columns = ( - "id", - "name", - "description", - "product_count", - "created_at", - "parent_category", - ) - category_tree = ttk.Treeview( - table_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set - ) + # Create the headers - Adjusted to match the screenshot + header_texts = ["ID", "Category Name", "Products"] + header_widths = [0.1, 0.7, 0.2] # Adjusted proportional widths - # Configure scrollbar - scrollbar.config(command=category_tree.yview) + for i, text in enumerate(header_texts): + header_cell = ctk.CTkFrame(headers_frame, fg_color="transparent") + header_cell.place( + relx=sum(header_widths[:i]), rely=0, relwidth=header_widths[i], relheight=1 + ) - # Define column headings - category_tree.heading("id", text="ID") - category_tree.heading("name", text="Category Name") - category_tree.heading("description", text="Description") - category_tree.heading("product_count", text="Products") - category_tree.heading("created_at", text="Created At") - category_tree.heading("parent_category", text="Parent Category") + # All headers centered + header_label = ctk.CTkLabel( + header_cell, + text=text, + font=("Helvetica", 14, "bold"), + text_color="white" + ) + header_label.place(relx=0.5, rely=0.5, anchor="center") - # Define column widths and alignment - category_tree.column("id", width=60, anchor="center") - category_tree.column("name", width=150) - category_tree.column("description", width=200) - category_tree.column("product_count", width=80, anchor="center") - category_tree.column("created_at", width=150, anchor="center") - category_tree.column("parent_category", width=150) + # Container for category rows + category_rows_frame = ctk.CTkFrame(categories_scrollable, fg_color="transparent") + category_rows_frame.pack(fill="both", expand=True) - category_tree.pack(fill="both", expand=True, padx=5, pady=5) + # Category rows list to track and update + category_rows = [] - # Footer with back button - footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) - footer_frame.pack(fill="x", pady=(15, 0)) + # Add mouse wheel scroll binding for better scrolling + def _bound_to_mousewheel(event): + categories_scrollable._parent_canvas.bind_all("<MouseWheel>", _on_mousewheel) - back_button = ctk.CTkButton( - footer_frame, - text="Back to Admin Dashboard", - command=lambda: switch_func("admin_dashboard"), - font=("Helvetica", 12, "bold"), - height=40, - corner_radius=8, - fg_color="#555555", - hover_color="#444444", - ) - back_button.pack(side="left") + def _unbound_to_mousewheel(event): + categories_scrollable._parent_canvas.unbind_all("<MouseWheel>") + + def _on_mousewheel(event): + categories_scrollable._parent_canvas.yview_scroll( + int(-1 * (event.delta / 120)), "units" + ) + + # Bind these functions to the frame + categories_scrollable.bind("<Enter>", _bound_to_mousewheel) + categories_scrollable.bind("<Leave>", _unbound_to_mousewheel) + + # Helper function to create a row for a category + def create_category_row(category, row_count): + row = ctk.CTkFrame( + category_rows_frame, + fg_color="#252525" if row_count % 2 == 0 else "#2b2b2b", + height=35 + ) + row.pack(fill="x", pady=0) + row.pack_propagate(False) + + # ID cell + id_cell = ctk.CTkFrame(row, fg_color="transparent") + id_cell.place(relx=0, rely=0, relwidth=header_widths[0], relheight=1) + + id_label = ctk.CTkLabel( + id_cell, + text=str(category["id"]), + font=("Helvetica", 12), + text_color="#4caf50" # Green color for ID + ) + id_label.place(relx=0.5, rely=0.5, anchor="center") + + # Name cell - centered to match image + name_cell = ctk.CTkFrame(row, fg_color="transparent") + name_cell.place( + relx=header_widths[0], rely=0, relwidth=header_widths[1], relheight=1 + ) + + name_label = ctk.CTkLabel( + name_cell, + text=category.get("name", "Unnamed Category"), + font=("Helvetica", 12), + text_color="white" + ) + # Center the category name + name_label.place(relx=0.5, rely=0.5, anchor="center") + + # Product count cell + count_cell = ctk.CTkFrame(row, fg_color="transparent") + count_cell.place( + relx=sum(header_widths[:2]), rely=0, relwidth=header_widths[2], relheight=1 + ) + + count_label = ctk.CTkLabel( + count_cell, + text=str(category.get("product_count", 0)), + font=("Helvetica", 12), + text_color="#2196f3" # Blue color for product count + ) + count_label.place(relx=0.5, rely=0.5, anchor="center") + + # Context menu for actions + def show_context_menu(event): + context_menu = ctk.CTkFrame(row, fg_color="#333333", corner_radius=5, border_width=1, border_color="#555555") + + # Position the context menu near the cursor + x, y, _, _ = row.bbox("all") + context_menu.place(x=event.x_root - row.winfo_rootx(), y=event.y_root - row.winfo_rooty()) + + # Edit option + edit_btn = ctk.CTkButton( + context_menu, + text="Edit Category", + font=("Helvetica", 11), + height=30, + width=150, + corner_radius=0, + fg_color="transparent", + hover_color="#3a7ebf", + command=lambda: [context_menu.destroy(), edit_category_dialog(category)] + ) + edit_btn.pack(fill="x", pady=(0, 1)) + + # Delete option + delete_btn = ctk.CTkButton( + context_menu, + text="Delete Category", + font=("Helvetica", 11), + height=30, + width=150, + corner_radius=0, + fg_color="transparent", + hover_color="#f44336", + command=lambda: [context_menu.destroy(), confirm_delete_category(category)] + ) + delete_btn.pack(fill="x") + + # Close menu when clicking elsewhere + def close_menu(e): + if e.widget != context_menu and not isinstance(e.widget, ctk.CTkButton): + context_menu.destroy() + row.unbind("<Button-1>", bind_id) + + bind_id = row.bind_all("<Button-1>", close_menu, add="+") + + # Bind the row for right-click context menu + row.bind("<Button-3>", show_context_menu) + + # Make the entire row clickable + row.bind("<Button-1>", lambda e: edit_category_dialog(category)) + + # Change cursor to hand when hovering over the row + row.bind("<Enter>", lambda e: row.configure(cursor="hand2")) + row.bind("<Leave>", lambda e: row.configure(cursor="")) + + return row + + def clear_category_rows(): + """Clear all category rows from the table""" + for row in category_rows: + row.destroy() + category_rows.clear() # Function to fetch categories def fetch_categories(search_term=None): @@ -212,70 +297,43 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): if response.status_code == 200: # Clear current items - for item in category_tree.get_children(): - category_tree.delete(item) + clear_category_rows() categories = response.json() - - for category in categories: - # Format date - created_at = "N/A" - if "created_at" in category: - try: - dt = datetime.fromisoformat( - category["created_at"].replace("Z", "+00:00") - ) - created_at = dt.strftime("%Y-%m-%d %H:%M") - except: - pass - - # Handle parent category - parent_category = category.get("parent_name", "None") - - # Insert into tree - category_tree.insert( - "", - "end", - values=( - category["id"], - category.get("name", "Unnamed Category"), - category.get("description", ""), - category.get("product_count", 0), - created_at, - parent_category, - ), + + if not categories: + # No categories found + no_results = ctk.CTkLabel( + category_rows_frame, + text="No categories found", + font=("Helvetica", 14), + text_color="gray" ) + no_results.pack(pady=20) + category_rows.append(no_results) + return + + # Add each category to the table + for i, category in enumerate(categories): + row = create_category_row(category, i) + category_rows.append(row) else: - pass + CTkMessagebox( + title="Error", + message=response.json().get("detail", "Failed to fetch categories"), + icon="cancel", + ) except Exception as e: CTkMessagebox( title="Error", message=f"An error occurred: {str(e)}", icon="cancel" ) - # Function to handle search - def on_search(): - search_term = search_entry.get() if search_entry.get() else None - fetch_categories(search_term) - - # Function to delete a category - def delete_category(): - selected_item = category_tree.selection() - if not selected_item: - CTkMessagebox( - title="Warning", - message="Please select a category to delete", - icon="warning", - ) - return - - # Get selected category details - category_id = category_tree.item(selected_item[0])["values"][0] - category_name = category_tree.item(selected_item[0])["values"][1] - + # Function to confirm deletion + def confirm_delete_category(category): # Confirm deletion confirm = CTkMessagebox( title="Confirm Deletion", - message=f"Are you sure you want to delete category '{category_name}'?\nThis action cannot be undone.", + message=f"Are you sure you want to delete category '{category['name']}'?\nThis action cannot be undone.", icon="question", option_1="Cancel", option_2="Delete", @@ -286,14 +344,14 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): try: response = requests.delete( - f"{API_URL}/category/delete/{category_id}", + f"{API_URL}/category/delete/{category['id']}", headers={"Authorization": f"Bearer {access_token}"}, ) if response.status_code == 200: CTkMessagebox( title="Success", - message=f"Category '{category_name}' deleted successfully!", + message=f"Category '{category['name']}' deleted successfully!", icon="check", ) fetch_categories() @@ -326,38 +384,19 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): title="Error", message=f"An error occurred: {str(e)}", icon="cancel" ) + # Function to edit a specific category + def edit_category_dialog(category): + # Display the category dialog for editing + show_category_dialog("Edit Category", category) + # Function to add a new category def add_category(): show_category_dialog("Add Category", None) - # Function to edit a category - def edit_category(): - selected_items = category_tree.selection() - if not selected_items: - CTkMessagebox( - title="Warning", - message="Please select a category to edit", - icon="warning", - ) - return - - category_id = category_tree.item(selected_items[0])["values"][0] - headers = {"Authorization": f"Bearer {access_token}"} - - try: - response = requests.get( - f"{API_URL}/admin/categories/{category_id}", headers=headers - ) - - if response.status_code == 200: - category = response.json() - show_category_dialog("Edit Category", category) - else: - pass - except Exception as e: - CTkMessagebox( - title="Error", message=f"An error occurred: {str(e)}", icon="cancel" - ) + # Function to handle search + def on_search(): + search_term = search_entry.get() if search_entry.get() else None + fetch_categories(search_term) def show_category_dialog(title, category=None): """Show dialog for adding/editing a category""" @@ -536,8 +575,40 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): # Connect UI elements to functions search_button.configure(command=on_search) add_button.configure(command=add_category) - edit_button.configure(command=edit_category) - delete_button.configure(command=delete_category) + edit_button.configure(command=lambda: edit_selected_category()) + delete_button.configure(command=lambda: delete_selected_category()) + + def edit_selected_category(): + """Prompt for editing based on selection""" + CTkMessagebox( + title="Info", + message="Please click the Edit button next to a specific category to edit it.", + icon="info", + ) + + def delete_selected_category(): + """Prompt for deletion based on selection""" + CTkMessagebox( + title="Info", + message="Please click the Delete button next to a specific category to delete it.", + icon="info", + ) + + # Footer with back button + footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) + footer_frame.pack(fill="x", pady=(15, 0)) + + back_button = ctk.CTkButton( + footer_frame, + text="Back to Admin Dashboard", + command=lambda: switch_func("admin_dashboard"), + font=("Helvetica", 12, "bold"), + height=40, + corner_radius=8, + fg_color="#555555", + hover_color="#444444", + ) + back_button.pack(side="left") # Initial data fetch def on_frame_shown(): diff --git a/app/frontend/components/admin/product_management.py b/app/frontend/components/admin/product_management.py deleted file mode 100644 index cc9ba172dbd84487ed7a0d8f5863baaaa29fd717..0000000000000000000000000000000000000000 --- a/app/frontend/components/admin/product_management.py +++ /dev/null @@ -1,744 +0,0 @@ -import customtkinter as ctk -from tkinter import ttk, messagebox -import requests -from datetime import datetime -import os -import base64 -from PIL import Image, ImageTk -from io import BytesIO -from CTkMessagebox import CTkMessagebox - - -def admin_product_management_frame(parent, switch_func, API_URL, access_token): - """ - Admin dashboard component for managing products in the marketplace - """ - frame = ctk.CTkFrame(parent) - - # Store the token for later use - def update_token(new_token): - nonlocal access_token - access_token = new_token - - frame.update_token = update_token - - # Create main container with padding - main_container = ctk.CTkFrame(frame, fg_color="transparent") - main_container.pack(padx=40, pady=30, fill="both", expand=True) - - # Header section - header_frame = ctk.CTkFrame(main_container, corner_radius=15, fg_color="#2e8b57") - header_frame.pack(fill="x", pady=(0, 20)) - - # Title and description - title_frame = ctk.CTkFrame(header_frame, fg_color="transparent") - title_frame.pack(padx=20, pady=15) - - ctk.CTkLabel( - title_frame, - text="Product Management", - font=("Helvetica", 24, "bold"), - text_color="#ffffff", - ).pack(anchor="w") - - ctk.CTkLabel( - title_frame, - text="Manage marketplace products", - font=("Helvetica", 14), - text_color="#e0e0e0", - ).pack(anchor="w") - - # Search and filter panel - filter_panel = ctk.CTkFrame(main_container, corner_radius=10, height=80) - filter_panel.pack(fill="x", pady=(0, 15)) - - # Left side - search - search_frame = ctk.CTkFrame(filter_panel, fg_color="transparent") - search_frame.pack(side="left", padx=20, pady=15, fill="y") - - ctk.CTkLabel(search_frame, text="Search:", font=("Helvetica", 12, "bold")).pack( - side="left", padx=(0, 10) - ) - - search_entry = ctk.CTkEntry( - search_frame, width=250, height=35, placeholder_text="Search products..." - ) - search_entry.pack(side="left", padx=(0, 10)) - - search_button = ctk.CTkButton( - search_frame, - text="Search", - font=("Helvetica", 12, "bold"), - height=35, - corner_radius=8, - fg_color="#2e8b57", - hover_color="#1f6e42", - ) - search_button.pack(side="left") - - # Right side - actions - actions_frame = ctk.CTkFrame(filter_panel, fg_color="transparent") - actions_frame.pack(side="right", padx=20, pady=15, fill="y") - - filter_menu = ctk.CTkOptionMenu( - actions_frame, - values=["All Products", "Approved", "Pending", "Rejected"], - font=("Helvetica", 12), - width=150, - height=35, - dropdown_font=("Helvetica", 12), - ) - filter_menu.pack(side="left", padx=(0, 10)) - filter_menu.set("All Products") - - sort_menu = ctk.CTkOptionMenu( - actions_frame, - values=[ - "Newest First", - "Oldest First", - "Price: High to Low", - "Price: Low to High", - ], - font=("Helvetica", 12), - width=150, - height=35, - dropdown_font=("Helvetica", 12), - ) - sort_menu.pack(side="left") - sort_menu.set("Newest First") - - # Action buttons panel - action_buttons = ctk.CTkFrame(main_container, fg_color="transparent") - action_buttons.pack(fill="x", pady=(0, 15)) - - approve_button = ctk.CTkButton( - action_buttons, - text="Approve", - font=("Helvetica", 12, "bold"), - height=35, - width=100, - corner_radius=8, - fg_color="#4caf50", - hover_color="#388e3c", - ) - approve_button.pack(side="left", padx=(0, 10)) - - reject_button = ctk.CTkButton( - action_buttons, - text="Reject", - font=("Helvetica", 12, "bold"), - height=35, - width=100, - corner_radius=8, - fg_color="#f44336", - hover_color="#d32f2f", - ) - reject_button.pack(side="left", padx=(0, 10)) - - view_button = ctk.CTkButton( - action_buttons, - text="View Details", - font=("Helvetica", 12, "bold"), - height=35, - width=120, - corner_radius=8, - fg_color="#3a7ebf", - hover_color="#2a6da9", - ) - view_button.pack(side="left") - - # Data table frame - table_frame = ctk.CTkFrame(main_container, corner_radius=10) - table_frame.pack(fill="both", expand=True) - - # Configure Treeview style - style = ttk.Style() - style.theme_use("clam") # Use clam theme as base - - # Configure the Treeview colors to match app theme - style.configure( - "Treeview", - background="#2b2b2b", - foreground="#ffffff", - fieldbackground="#2b2b2b", - borderwidth=0, - rowheight=40, - ) - - # Configure the headings - style.configure( - "Treeview.Heading", - background="#2e8b57", - foreground="#ffffff", - borderwidth=0, - font=("Helvetica", 12, "bold"), - ) - - # Selection color - style.map("Treeview", background=[("selected", "#2e8b57")]) - - # Create scrollbar - scrollbar = ttk.Scrollbar(table_frame) - scrollbar.pack(side="right", fill="y") - - # Create Treeview - Reordered columns - columns = ( - "id", - "name", - "category", - "price", - "owner", - "status", - "created_at", - "image", - ) - product_tree = ttk.Treeview( - table_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set - ) - - # Configure scrollbar - scrollbar.config(command=product_tree.yview) - - # Define column headings - Reordered - product_tree.heading("id", text="ID") - product_tree.heading("name", text="Product Name") - product_tree.heading("category", text="Category") - product_tree.heading("price", text="Price") - product_tree.heading("owner", text="Shop Owner") - product_tree.heading("status", text="Status") - product_tree.heading("created_at", text="Created At") - product_tree.heading("image", text="Image") - - # Define column widths and alignment - Reordered - product_tree.column("id", width=60, anchor="center") - product_tree.column("name", width=200) - product_tree.column("category", width=150) - product_tree.column("price", width=80, anchor="center") - product_tree.column("owner", width=150) - product_tree.column("status", width=100, anchor="center") - product_tree.column("created_at", width=150, anchor="center") - product_tree.column("image", width=80, anchor="center") - - # Add tag configurations for different status types - product_tree.tag_configure("approved", background="#1a472a") # Dark green - product_tree.tag_configure("pending", background="#5d4037") # Brown - product_tree.tag_configure("rejected", background="#b71c1c") # Dark red - - product_tree.pack(fill="both", expand=True, padx=5, pady=5) - - # Store images to prevent garbage collection - image_refs = [] - - # Footer with back button - footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) - footer_frame.pack(fill="x", pady=(15, 0)) - - back_button = ctk.CTkButton( - footer_frame, - text="Back to Admin Dashboard", - command=lambda: switch_func("admin_dashboard"), - font=("Helvetica", 12, "bold"), - height=40, - corner_radius=8, - fg_color="#555555", - hover_color="#444444", - ) - back_button.pack(side="left") - - # Function to fetch products - def fetch_products(status_filter=None, sort_option=None, search_term=None): - """ - Fetch products from backend with filters - """ - headers = {"Authorization": f"Bearer {access_token}"} - - # Build query params - params = {} - if status_filter and status_filter != "All Products": - params["status"] = status_filter.lower() - if sort_option: - if sort_option == "Newest First": - params["sort"] = "created_at_desc" - elif sort_option == "Oldest First": - params["sort"] = "created_at_asc" - elif sort_option == "Price: High to Low": - params["sort"] = "price_desc" - elif sort_option == "Price: Low to High": - params["sort"] = "price_asc" - if search_term: - params["search"] = search_term - - try: - response = requests.get( - f"{API_URL}/admin/products", headers=headers, params=params - ) - - if response.status_code == 200: - # Clear current items - for item in product_tree.get_children(): - product_tree.delete(item) - - # Clear image references - image_refs.clear() - - products = response.json() - - for product in products: - # Format price - price = f"${product.get('price', 0):.2f}" - - # Format date - created_at = "N/A" - if "created_at" in product: - try: - dt = datetime.fromisoformat( - product["created_at"].replace("Z", "+00:00") - ) - created_at = dt.strftime("%Y-%m-%d %H:%M") - except: - pass - - # Status for styling - status = product.get("status", "pending").lower() - - # Get image if available - img_text = "No Image" - if product.get("images") and len(product["images"]) > 0: - img_text = "Has Image" - - # Get category - category = product.get("category", {}).get("name", "Uncategorized") - - # Get owner name - owner = product.get("shop", {}).get("name", "Unknown") - - # Insert into tree - Reordered values to match new column order - item_id = product_tree.insert( - "", - "end", - values=( - product["id"], - product.get("name", "Unnamed Product"), - category, - price, - owner, - status.capitalize(), - created_at, - img_text, - ), - tags=(status,), - ) - - # Try to load the image - if product.get("images") and len(product["images"]) > 0: - try: - image_url = product["images"][0].get("image_url") - if image_url: - response = requests.get(image_url) - if response.status_code == 200: - img_data = response.content - img = Image.open(BytesIO(img_data)) - img = img.resize((30, 30)) - photo_img = ImageTk.PhotoImage(img) - - # Store reference to prevent garbage collection - image_refs.append(photo_img) - - # Use item id to identify which item to update - product_tree.item( - item_id, - values=( - product["id"], - product.get("name", "Unnamed Product"), - category, - price, - owner, - status.capitalize(), - created_at, - "", # Will be replaced by image - ), - ) - - # Create image for the item - product_tree.item(item_id, image=photo_img) - except Exception as e: - print(f"[DEBUG] Error loading product image: {e}") - else: - CTkMessagebox( - title="Error", message="Failed to fetch products", icon="cancel" - ) - except Exception as e: - CTkMessagebox( - title="Error", message=f"An error occurred: {str(e)}", icon="cancel" - ) - - # Function to handle filter changes - def on_filter_change(*args): - status_filter = filter_menu.get() - sort_option = sort_menu.get() - search_term = search_entry.get() if search_entry.get() else None - fetch_products(status_filter, sort_option, search_term) - - # Function to handle search - def on_search(): - status_filter = filter_menu.get() - sort_option = sort_menu.get() - search_term = search_entry.get() if search_entry.get() else None - fetch_products(status_filter, sort_option, search_term) - - # Function to approve a product - def approve_product(): - selected_items = product_tree.selection() - if not selected_items: - CTkMessagebox( - title="Warning", - message="Please select a product to approve", - icon="warning", - ) - return - - product_id = product_tree.item(selected_items[0])["values"][0] - headers = {"Authorization": f"Bearer {access_token}"} - - try: - response = requests.put( - f"{API_URL}/admin/products/{product_id}/approve", headers=headers - ) - - if response.status_code == 200: - CTkMessagebox( - title="Success", - message="Product approved successfully", - icon="check", - ) - on_filter_change() # Refresh the list - else: - CTkMessagebox( - title="Error", message="Failed to approve product", icon="cancel" - ) - except Exception as e: - CTkMessagebox( - title="Error", message=f"An error occurred: {str(e)}", icon="cancel" - ) - - # Function to reject a product - def reject_product(): - selected_items = product_tree.selection() - if not selected_items: - CTkMessagebox( - title="Warning", - message="Please select a product to reject", - icon="warning", - ) - return - - product_id = product_tree.item(selected_items[0])["values"][0] - product_name = product_tree.item(selected_items[0])["values"][1] - - # Create rejection dialog - reject_dialog = ctk.CTkToplevel(frame) - reject_dialog.title("Reject Product") - reject_dialog.geometry("500x300") - reject_dialog.resizable(False, False) - reject_dialog.transient(frame) - reject_dialog.grab_set() - - # Dialog content - ctk.CTkLabel( - reject_dialog, - text=f"Reject Product: {product_name}", - font=("Helvetica", 18, "bold"), - ).pack(pady=(20, 10)) - - ctk.CTkLabel( - reject_dialog, - text="Please provide a reason for rejection:", - font=("Helvetica", 14), - ).pack(pady=(0, 10)) - - reason_textbox = ctk.CTkTextbox(reject_dialog, width=400, height=100) - reason_textbox.pack(padx=20, pady=10) - - # Button frame - button_frame = ctk.CTkFrame(reject_dialog, fg_color="transparent") - button_frame.pack(pady=20) - - def on_cancel(): - reject_dialog.destroy() - - def on_submit(): - rejection_reason = reason_textbox.get("1.0", "end-1c").strip() - if not rejection_reason: - CTkMessagebox( - title="Warning", - message="Please provide a reason for rejection", - icon="warning", - parent=reject_dialog, - ) - return - - headers = {"Authorization": f"Bearer {access_token}"} - data = {"reason": rejection_reason} - - try: - response = requests.put( - f"{API_URL}/admin/products/{product_id}/reject", - headers=headers, - json=data, - ) - - if response.status_code == 200: - reject_dialog.destroy() - CTkMessagebox( - title="Success", - message="Product rejected successfully", - icon="check", - ) - on_filter_change() # Refresh the list - else: - CTkMessagebox( - title="Error", - message="Failed to reject product", - icon="cancel", - parent=reject_dialog, - ) - except Exception as e: - CTkMessagebox( - title="Error", - message=f"An error occurred: {str(e)}", - icon="cancel", - parent=reject_dialog, - ) - - ctk.CTkButton( - button_frame, - text="Cancel", - command=on_cancel, - font=("Helvetica", 12, "bold"), - fg_color="#9e9e9e", - hover_color="#757575", - width=120, - height=35, - ).pack(side="left", padx=10) - - ctk.CTkButton( - button_frame, - text="Reject Product", - command=on_submit, - font=("Helvetica", 12, "bold"), - fg_color="#f44336", - hover_color="#d32f2f", - width=150, - height=35, - ).pack(side="left", padx=10) - - # Function to view product details - def view_product_details(): - selected_items = product_tree.selection() - if not selected_items: - CTkMessagebox( - title="Warning", - message="Please select a product to view", - icon="warning", - ) - return - - product_id = product_tree.item(selected_items[0])["values"][0] - headers = {"Authorization": f"Bearer {access_token}"} - - try: - response = requests.get( - f"{API_URL}/admin/products/{product_id}", headers=headers - ) - - if response.status_code == 200: - product = response.json() - show_product_details(product) - else: - CTkMessagebox( - title="Error", - message="Failed to fetch product details", - icon="cancel", - ) - except Exception as e: - CTkMessagebox( - title="Error", message=f"An error occurred: {str(e)}", icon="cancel" - ) - - # Function to show product details dialog - def show_product_details(product): - detail_dialog = ctk.CTkToplevel(frame) - detail_dialog.title("Product Details") - detail_dialog.geometry("800x600") - detail_dialog.resizable(True, True) - detail_dialog.transient(frame) - detail_dialog.grab_set() - - # Create scrollable frame to handle large content - main_scroll = ctk.CTkScrollableFrame(detail_dialog) - main_scroll.pack(fill="both", expand=True, padx=20, pady=20) - - # Title - ctk.CTkLabel( - main_scroll, - text=product.get("name", "Unnamed Product"), - font=("Helvetica", 24, "bold"), - ).pack(anchor="w", pady=(0, 20)) - - # Top section: Images and Basic Info - top_frame = ctk.CTkFrame(main_scroll, fg_color="transparent") - top_frame.pack(fill="x", pady=(0, 20)) - - # Left side - Images - image_frame = ctk.CTkFrame(top_frame, width=300, height=300, fg_color="#333") - image_frame.pack(side="left", padx=(0, 20)) - image_frame.pack_propagate(False) # Force dimensions - - # If has images, display the first one - if product.get("images") and len(product["images"]) > 0: - try: - image_url = product["images"][0].get("image_url") - if image_url: - response = requests.get(image_url) - if response.status_code == 200: - img_data = response.content - img = Image.open(BytesIO(img_data)) - - # Maintain aspect ratio - img.thumbnail((280, 280)) - - photo_img = ImageTk.PhotoImage(img) - - img_label = ctk.CTkLabel(image_frame, text="", image=photo_img) - img_label.image = photo_img # Keep reference - img_label.place(relx=0.5, rely=0.5, anchor="center") - else: - ctk.CTkLabel(image_frame, text="Image not available").place( - relx=0.5, rely=0.5, anchor="center" - ) - except: - ctk.CTkLabel(image_frame, text="Error loading image").place( - relx=0.5, rely=0.5, anchor="center" - ) - else: - ctk.CTkLabel(image_frame, text="No Image Available").place( - relx=0.5, rely=0.5, anchor="center" - ) - - # Right side - Basic info in a structured form - info_frame = ctk.CTkFrame(top_frame, fg_color="transparent") - info_frame.pack(side="left", fill="both", expand=True) - - # Fields grid - info_grid = ctk.CTkFrame(info_frame, fg_color="transparent") - info_grid.pack(fill="both", expand=True) - - def add_field(label, value, row): - label_widget = ctk.CTkLabel( - info_grid, - text=f"{label}:", - font=("Helvetica", 14, "bold"), - width=150, - anchor="e", - ) - label_widget.grid(row=row, column=0, sticky="e", padx=(0, 20), pady=8) - - value_widget = ctk.CTkLabel( - info_grid, text=str(value), font=("Helvetica", 14), anchor="w" - ) - value_widget.grid(row=row, column=1, sticky="w", pady=8) - - # Basic details - add_field("Product ID", product["id"], 0) - add_field("Product Name", product.get("name", "Unnamed Product"), 1) - add_field("Price", f"${product.get('price', 0):.2f}", 2) - add_field("Status", product.get("status", "Pending").capitalize(), 3) - - # Shop information - shop = product.get("shop", {}) - add_field("Shop", shop.get("name", "Unknown"), 4) - add_field("Shop Owner", shop.get("owner_name", "Unknown"), 5) - - # Category - category = product.get("category", {}) - add_field("Category", category.get("name", "Uncategorized"), 6) - - # Created date - created_at = "N/A" - if "created_at" in product: - try: - dt = datetime.fromisoformat( - product["created_at"].replace("Z", "+00:00") - ) - created_at = dt.strftime("%Y-%m-%d %H:%M") - except: - pass - add_field("Created At", created_at, 7) - - # Description section - desc_frame = ctk.CTkFrame(main_scroll, fg_color="transparent") - desc_frame.pack(fill="x", pady=(0, 20)) - - ctk.CTkLabel( - desc_frame, text="Description", font=("Helvetica", 18, "bold") - ).pack(anchor="w", pady=(0, 10)) - - desc_box = ctk.CTkTextbox(desc_frame, height=100) - desc_box.pack(fill="x") - desc_box.insert("1.0", product.get("description", "No description provided")) - desc_box.configure(state="disabled") # Make it read-only - - # Action buttons - buttons_frame = ctk.CTkFrame(main_scroll, fg_color="transparent") - buttons_frame.pack(fill="x", pady=(20, 0)) - - close_button = ctk.CTkButton( - buttons_frame, - text="Close", - command=detail_dialog.destroy, - height=35, - width=100, - ) - close_button.pack(side="right", padx=(10, 0)) - - # Add Approve/Reject buttons if product is pending - if product.get("status", "").lower() == "pending": - reject_button = ctk.CTkButton( - buttons_frame, - text="Reject", - command=lambda: detail_dialog.destroy() or reject_product(), - fg_color="#f44336", - hover_color="#d32f2f", - height=35, - width=100, - ) - reject_button.pack(side="right", padx=(10, 0)) - - approve_button = ctk.CTkButton( - buttons_frame, - text="Approve", - command=lambda: detail_dialog.destroy() or approve_product(), - fg_color="#4caf50", - hover_color="#388e3c", - height=35, - width=100, - ) - approve_button.pack(side="right", padx=(10, 0)) - - # Connect UI elements to functions - filter_menu.configure(command=on_filter_change) - sort_menu.configure(command=on_filter_change) - search_button.configure(command=on_search) - - # Connect action buttons - approve_button.configure(command=approve_product) - reject_button.configure(command=reject_product) - view_button.configure(command=view_product_details) - - # Initial data fetch - def on_frame_shown(): - fetch_products() - - frame.after(100, on_frame_shown) - - return frame diff --git a/app/frontend/components/admin/shop_owner_management.py b/app/frontend/components/admin/shop_owner_management.py index ed416da2d97150c1e5fabb71781f38986b8fea6d..585371f7cad5e8b3bd0471a7b322a662ee8def19 100644 --- a/app/frontend/components/admin/shop_owner_management.py +++ b/app/frontend/components/admin/shop_owner_management.py @@ -79,18 +79,6 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token actions_frame = ctk.CTkFrame(control_panel, fg_color="transparent") actions_frame.pack(side="right", padx=20, pady=15, fill="y") - view_shops_button = ctk.CTkButton( - actions_frame, - text="View Shops", - font=("Helvetica", 12, "bold"), - height=35, - width=120, - corner_radius=8, - fg_color="#3a7ebf", - hover_color="#2a6da9", - ) - view_shops_button.pack(side="left", padx=(0, 10)) - refresh_button = ctk.CTkButton( actions_frame, text="Refresh", @@ -101,177 +89,193 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token fg_color="#4caf50", hover_color="#388e3c", ) - refresh_button.pack(side="left", padx=(0, 10)) - - delete_button = ctk.CTkButton( - actions_frame, - text="Delete Owner", - font=("Helvetica", 12, "bold"), - height=35, - width=120, - corner_radius=8, - fg_color="#f44336", - hover_color="#d32f2f", - ) - delete_button.pack(side="left") + refresh_button.pack(side="left") - # Data table frame + # Data table frame - using grid layout similar to owner_dashboard table_frame = ctk.CTkFrame(main_container, corner_radius=10) table_frame.pack(fill="both", expand=True, pady=(15, 0)) - # Configure Treeview style - style = ttk.Style() - style.theme_use("clam") # Use clam theme as base - - # Configure the Treeview colors to match app theme - style.configure( - "Treeview", - background="#2b2b2b", - foreground="#ffffff", - fieldbackground="#2b2b2b", - borderwidth=0, - rowheight=35, + # Create scrollable frame to contain the owner table + owners_scrollable = ctk.CTkScrollableFrame( + table_frame, fg_color="#2b2b2b", corner_radius=5 ) + owners_scrollable.pack(fill="both", expand=True, padx=5, pady=5) - # Configure the headings - style.configure( - "Treeview.Heading", - background="#2e8b57", - foreground="#ffffff", - borderwidth=0, - font=("Helvetica", 12, "bold"), - ) + # Table headers + headers_frame = ctk.CTkFrame(owners_scrollable, fg_color="#1a1a1a", height=40) + headers_frame.pack(fill="x", pady=(0, 2)) - # Selection color - style.map("Treeview", background=[("selected", "#2e8b57")]) + # Create the headers + header_texts = ["ID", "Username", "Email", "Phone Number", "Role", "Actions"] + header_widths = [0.1, 0.2, 0.25, 0.2, 0.1, 0.15] # Proportional widths - # Create scrollbar - scrollbar = ttk.Scrollbar(table_frame) - scrollbar.pack(side="right", fill="y") + for i, text in enumerate(header_texts): + header_cell = ctk.CTkFrame(headers_frame, fg_color="transparent") + header_cell.place( + relx=sum(header_widths[:i]), rely=0, relwidth=header_widths[i], relheight=1 + ) - # Create Treeview with styled tags - columns = ("id", "username", "email", "phone", "role") - owner_tree = ttk.Treeview( - table_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set - ) + header_label = ctk.CTkLabel( + header_cell, text=text, font=("Helvetica", 14, "bold"), text_color="white" + ) + header_label.place(relx=0.5, rely=0.5, anchor="center") - # Configure scrollbar - scrollbar.config(command=owner_tree.yview) + # Container for owner rows + owner_rows_frame = ctk.CTkFrame(owners_scrollable, fg_color="transparent") + owner_rows_frame.pack(fill="both", expand=True) - # Define column headings - owner_tree.heading("id", text="ID") - owner_tree.heading("username", text="Username") - owner_tree.heading("email", text="Email") - owner_tree.heading("phone", text="Phone Number") - owner_tree.heading("role", text="Role") + # Owner rows list to track and update + owner_rows = [] - # Define column widths - owner_tree.column("id", width=60, anchor="center") - owner_tree.column("username", width=150) - owner_tree.column("email", width=200) - owner_tree.column("phone", width=150) - owner_tree.column("role", width=100, anchor="center") + # Add mouse wheel scroll binding for better scrolling + def _bound_to_mousewheel(event): + owners_scrollable._parent_canvas.bind_all("<MouseWheel>", _on_mousewheel) - # Add tag configurations - owner_tree.tag_configure("owner", background="#252525") - owner_tree.tag_configure("highlight", background="#1a4731") - owner_tree.tag_configure("match", background="#1f3d5a") + def _unbound_to_mousewheel(event): + owners_scrollable._parent_canvas.unbind_all("<MouseWheel>") - owner_tree.pack(fill="both", expand=True, padx=5, pady=5) + def _on_mousewheel(event): + owners_scrollable._parent_canvas.yview_scroll( + int(-1 * (event.delta / 120)), "units" + ) - # Footer with back button - footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) - footer_frame.pack(fill="x", pady=(15, 0)) + # Bind these functions to the frame + owners_scrollable.bind("<Enter>", _bound_to_mousewheel) + owners_scrollable.bind("<Leave>", _unbound_to_mousewheel) + + # Helper function to create a row for a shop owner + def create_owner_row(owner, row_count): + # Highlight owners with shops + has_shops = len(owner.get("shops", [])) > 0 + + row = ctk.CTkFrame( + owner_rows_frame, + fg_color="#1a4731" if has_shops else ("#252525" if row_count % 2 == 0 else "#2b2b2b"), + height=50 + ) + row.pack(fill="x", pady=1) + row.pack_propagate(False) + + # ID cell + id_cell = ctk.CTkFrame(row, fg_color="transparent") + id_cell.place(relx=0, rely=0, relwidth=header_widths[0], relheight=1) + + id_label = ctk.CTkLabel( + id_cell, + text=str(owner["id"]), + font=("Helvetica", 12), + text_color="white" + ) + id_label.place(relx=0.5, rely=0.5, anchor="center") - back_button = ctk.CTkButton( - footer_frame, - text="Back to Admin Dashboard", - command=lambda: switch_func("admin_dashboard"), - font=("Helvetica", 12, "bold"), - height=40, - corner_radius=8, - fg_color="#555555", - hover_color="#444444", - ) - back_button.pack(side="left") + # Username cell + username_cell = ctk.CTkFrame(row, fg_color="transparent") + username_cell.place( + relx=header_widths[0], rely=0, relwidth=header_widths[1], relheight=1 + ) - # Add endpoints to the backend for these actions - def fetch_shop_owners(): - """Fetch all shop owners from the backend""" - headers = {"Authorization": f"Bearer {access_token}"} + username_label = ctk.CTkLabel( + username_cell, + text=owner.get("username", "Unknown"), + font=("Helvetica", 12), + text_color="white" + ) + username_label.place(relx=0.5, rely=0.5, anchor="center") - try: - response = requests.get(f"{API_URL}/admin/owners", headers=headers) + # Email cell + email_cell = ctk.CTkFrame(row, fg_color="transparent") + email_cell.place( + relx=sum(header_widths[:2]), rely=0, relwidth=header_widths[2], relheight=1 + ) - if response.status_code == 200: - # Clear existing items - for item in owner_tree.get_children(): - owner_tree.delete(item) + email_label = ctk.CTkLabel( + email_cell, + text=owner.get("email", "N/A"), + font=("Helvetica", 12), + text_color="white" + ) + email_label.place(relx=0.5, rely=0.5, anchor="center") - shop_owners = response.json() + # Phone cell + phone_cell = ctk.CTkFrame(row, fg_color="transparent") + phone_cell.place( + relx=sum(header_widths[:3]), rely=0, relwidth=header_widths[3], relheight=1 + ) - # Filter users by role (only shop owners) - row_count = 0 - for owner in shop_owners: - # Format date - created_at = "N/A" - if "created_at" in owner: - try: - dt = datetime.fromisoformat( - owner["created_at"].replace("Z", "+00:00") - ) - created_at = dt.strftime("%Y-%m-%d %H:%M") - except: - pass - - # Get number of shops if available - shop_count = len(owner.get("shops", [])) - - # Add with alternating tags for zebra striping - tag = "owner" if row_count % 2 == 0 else "" - if shop_count > 0: - tag = "highlight" # Highlight owners with shops - - owner_tree.insert( - "", - "end", - values=( - owner["id"], - owner.get("username", "Unknown"), - owner.get("email", ""), - owner.get("phone_number", ""), - owner.get("role", "shop_owner"), - ), - tags=(tag,), - ) - row_count += 1 - else: - pass - except Exception as e: - CTkMessagebox( - title="Connection Error", - message=f"Failed to connect to server: {e}", - icon="cancel", - ) + phone_label = ctk.CTkLabel( + phone_cell, + text=owner.get("phone_number", "N/A"), + font=("Helvetica", 12), + text_color="white" + ) + phone_label.place(relx=0.5, rely=0.5, anchor="center") - def delete_shop_owner(): - """Delete (ban) the selected shop owner""" - selected_item = owner_tree.selection() - if not selected_item: - CTkMessagebox( - title="Warning", - message="Please select a shop owner to delete", - icon="warning", - ) - return + # Role cell + role_cell = ctk.CTkFrame(row, fg_color="transparent") + role_cell.place( + relx=sum(header_widths[:4]), rely=0, relwidth=header_widths[4], relheight=1 + ) + + role_label = ctk.CTkLabel( + role_cell, + text=owner.get("role", "shop_owner"), + font=("Helvetica", 12), + text_color="white" + ) + role_label.place(relx=0.5, rely=0.5, anchor="center") + + # Actions cell + actions_cell = ctk.CTkFrame(row, fg_color="transparent") + actions_cell.place( + relx=sum(header_widths[:5]), rely=0, relwidth=header_widths[5], relheight=1 + ) - owner_id = owner_tree.item(selected_item[0])["values"][0] - username = owner_tree.item(selected_item[0])["values"][1] + # Action buttons container (for multiple buttons) + buttons_frame = ctk.CTkFrame(actions_cell, fg_color="transparent") + buttons_frame.place(relx=0.5, rely=0.5, anchor="center") + + # View shops button + view_button = ctk.CTkButton( + buttons_frame, + text="View", + font=("Helvetica", 10, "bold"), + height=25, + width=55, + corner_radius=6, + fg_color="#3a7ebf", + hover_color="#2a6da9", + command=lambda o=owner: view_owner_shops(o) + ) + view_button.pack(side="left", padx=(0, 5)) + + # Delete button + delete_action = ctk.CTkButton( + buttons_frame, + text="Delete", + font=("Helvetica", 10, "bold"), + height=25, + width=55, + corner_radius=6, + fg_color="#f44336", + hover_color="#d32f2f", + command=lambda o=owner: confirm_delete_owner(o) + ) + delete_action.pack(side="left") + return row + + def clear_owner_rows(): + """Clear all owner rows from the table""" + for row in owner_rows: + row.destroy() + owner_rows.clear() + + # Function to confirm deletion of an owner + def confirm_delete_owner(owner): # Confirm deletion confirm = CTkMessagebox( title="Confirm Deletion", - message=f"Are you sure you want to delete shop owner '{username}'?\n\nThis will also delete all their shops and products.\n\nThis action cannot be undone!", + message=f"Are you sure you want to delete shop owner '{owner['username']}'?\n\nThis will also delete all their shops and products.\n\nThis action cannot be undone!", icon="question", option_1="Cancel", option_2="Delete", @@ -283,7 +287,7 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token try: headers = {"Authorization": f"Bearer {access_token}"} response = requests.delete( - f"{API_URL}/admin/owners/{owner_id}", headers=headers + f"{API_URL}/admin/owners/{owner['id']}", headers=headers ) if response.status_code == 200: @@ -308,24 +312,112 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token icon="cancel", ) - def view_shops(): - """View shops owned by the selected shop owner""" - selected_item = owner_tree.selection() - if not selected_item: - CTkMessagebox( - title="Warning", - message="Please select a shop owner first", - icon="warning", - ) - return - + # View shops for a specific owner + def view_owner_shops(owner): # Get selected owner ID and username - owner_id = owner_tree.item(selected_item[0])["values"][0] - username = owner_tree.item(selected_item[0])["values"][1] + owner_id = owner["id"] + username = owner.get("username", "Unknown") # Create a popup to display shops show_shops_dialog(owner_id, username) + # Fetch shop owners + def fetch_shop_owners(): + """Fetch all shop owners from the backend""" + headers = {"Authorization": f"Bearer {access_token}"} + + try: + response = requests.get(f"{API_URL}/admin/owners", headers=headers) + + if response.status_code == 200: + # Clear existing items + clear_owner_rows() + + shop_owners = response.json() + + # Add each shop owner to the table + row_count = 0 + for owner in shop_owners: + row = create_owner_row(owner, row_count) + owner_rows.append(row) + row_count += 1 + + if row_count == 0: + # No results found + no_results = ctk.CTkLabel( + owner_rows_frame, + text="No shop owners found", + font=("Helvetica", 14), + text_color="gray" + ) + no_results.pack(pady=20) + owner_rows.append(no_results) + else: + CTkMessagebox( + title="Error", + message=response.json().get("detail", "Failed to fetch shop owners"), + icon="cancel", + ) + except Exception as e: + CTkMessagebox( + title="Connection Error", + message=f"Failed to connect to server: {e}", + icon="cancel", + ) + + def search_owners(): + """Search owners based on the search term""" + search_term = search_entry.get().lower() + if not search_term: + fetch_shop_owners() + return + + headers = {"Authorization": f"Bearer {access_token}"} + + try: + # We'll fetch all owners and filter on the client side for simplicity + response = requests.get(f"{API_URL}/admin/owners", headers=headers) + + if response.status_code == 200: + # Clear existing items + clear_owner_rows() + + shop_owners = response.json() + + # Filter owners by search term + row_count = 0 + for owner in shop_owners: + if ( + search_term in str(owner.get("username", "")).lower() or + search_term in str(owner.get("email", "")).lower() + ): + row = create_owner_row(owner, row_count) + owner_rows.append(row) + row_count += 1 + + if row_count == 0: + # No results found + no_results = ctk.CTkLabel( + owner_rows_frame, + text="No shop owners found matching your search", + font=("Helvetica", 14), + text_color="gray" + ) + no_results.pack(pady=20) + owner_rows.append(no_results) + else: + CTkMessagebox( + title="Error", + message=response.json().get("detail", "Failed to fetch shop owners"), + icon="cancel", + ) + except Exception as e: + CTkMessagebox( + title="Connection Error", + message=f"Failed to connect to server: {e}", + icon="cancel", + ) + def show_shops_dialog(owner_id, username): # Fetch shops for this owner shops = fetch_owner_shops(owner_id) @@ -444,30 +536,25 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token ) return [] - def search_owners(): - """Search owners based on the search term""" - search_term = search_entry.get().lower() - if not search_term: - fetch_shop_owners() - return - - # Filter the treeview based on the search term - for item in owner_tree.get_children(): - values = owner_tree.item(item)["values"] - # Check if search term is in username or email - if ( - search_term in str(values[1]).lower() - or search_term in str(values[2]).lower() - ): - owner_tree.item(item, tags=("match",)) - else: - owner_tree.detach(item) # Hide non-matching items - # Connect functions to buttons search_button.configure(command=search_owners) refresh_button.configure(command=fetch_shop_owners) - delete_button.configure(command=delete_shop_owner) - view_shops_button.configure(command=view_shops) + + # Footer with back button + footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) + footer_frame.pack(fill="x", pady=(15, 0)) + + back_button = ctk.CTkButton( + footer_frame, + text="Back to Admin Dashboard", + command=lambda: switch_func("admin_dashboard"), + font=("Helvetica", 12, "bold"), + height=40, + corner_radius=8, + fg_color="#555555", + hover_color="#444444", + ) + back_button.pack(side="left") # Fetch shop owners when the frame is shown def on_frame_shown(): diff --git a/app/frontend/components/admin/user_management.py b/app/frontend/components/admin/user_management.py index 8d9a3f425e103ebb47e4dcc5a9a5c30cf777bc11..2fe64433d8fbab7b3f88f289631ec0872a12489d 100644 --- a/app/frontend/components/admin/user_management.py +++ b/app/frontend/components/admin/user_management.py @@ -89,164 +89,171 @@ def admin_user_management_frame(parent, switch_func, API_URL, access_token): fg_color="#4caf50", hover_color="#388e3c", ) - refresh_button.pack(side="left", padx=(0, 10)) + refresh_button.pack(side="left") - delete_button = ctk.CTkButton( - actions_frame, - text="Delete User", - font=("Helvetica", 12, "bold"), - height=35, - width=120, - corner_radius=8, - fg_color="#f44336", - hover_color="#d32f2f", - ) - delete_button.pack(side="left") - - # Data table frame + # Data table frame - using grid layout similar to owner_dashboard table_frame = ctk.CTkFrame(main_container, corner_radius=10) table_frame.pack(fill="both", expand=True, pady=(15, 0)) - # Configure Treeview style - style = ttk.Style() - style.theme_use("clam") # Use clam theme as base - - # Configure the Treeview colors to match app theme - style.configure( - "Treeview", - background="#2b2b2b", - foreground="#ffffff", - fieldbackground="#2b2b2b", - borderwidth=0, - rowheight=35, + # Create scrollable frame to contain the user table + users_scrollable = ctk.CTkScrollableFrame( + table_frame, fg_color="#2b2b2b", corner_radius=5 ) + users_scrollable.pack(fill="both", expand=True, padx=5, pady=5) - # Configure the headings - style.configure( - "Treeview.Heading", - background="#1f538d", - foreground="#ffffff", - borderwidth=0, - font=("Helvetica", 12, "bold"), - ) + # Table headers + headers_frame = ctk.CTkFrame(users_scrollable, fg_color="#1a1a1a", height=40) + headers_frame.pack(fill="x", pady=(0, 2)) - # Selection color - style.map("Treeview", background=[("selected", "#3a7ebf")]) + # Create the headers + header_texts = ["ID", "Username", "Email", "Phone Number", "Role", "Actions"] + header_widths = [0.1, 0.2, 0.25, 0.2, 0.1, 0.15] # Proportional widths - # Create scrollbar - scrollbar = ttk.Scrollbar(table_frame) - scrollbar.pack(side="right", fill="y") + for i, text in enumerate(header_texts): + header_cell = ctk.CTkFrame(headers_frame, fg_color="transparent") + header_cell.place( + relx=sum(header_widths[:i]), rely=0, relwidth=header_widths[i], relheight=1 + ) - # Create Treeview with styled tags - columns = ("id", "username", "email", "phone", "role") - user_tree = ttk.Treeview( - table_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set - ) + header_label = ctk.CTkLabel( + header_cell, text=text, font=("Helvetica", 14, "bold"), text_color="white" + ) + header_label.place(relx=0.5, rely=0.5, anchor="center") - # Configure scrollbar - scrollbar.config(command=user_tree.yview) + # Container for user rows + user_rows_frame = ctk.CTkFrame(users_scrollable, fg_color="transparent") + user_rows_frame.pack(fill="both", expand=True) - # Define column headings - user_tree.heading("id", text="ID") - user_tree.heading("username", text="Username") - user_tree.heading("email", text="Email") - user_tree.heading("phone", text="Phone Number") - user_tree.heading("role", text="Role") + # User rows list to track and update + user_rows = [] - # Define column widths - user_tree.column("id", width=60, anchor="center") - user_tree.column("username", width=150) - user_tree.column("email", width=200) - user_tree.column("phone", width=150) - user_tree.column("role", width=100, anchor="center") + # Add mouse wheel scroll binding for better scrolling + def _bound_to_mousewheel(event): + users_scrollable._parent_canvas.bind_all("<MouseWheel>", _on_mousewheel) - # Add tag configurations - user_tree.tag_configure("user", background="#252525") - user_tree.tag_configure("match", background="#1f3d5a") + def _unbound_to_mousewheel(event): + users_scrollable._parent_canvas.unbind_all("<MouseWheel>") - user_tree.pack(fill="both", expand=True, padx=5, pady=5) + def _on_mousewheel(event): + users_scrollable._parent_canvas.yview_scroll( + int(-1 * (event.delta / 120)), "units" + ) - # Footer with back button - footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) - footer_frame.pack(fill="x", pady=(15, 0)) + # Bind these functions to the frame + users_scrollable.bind("<Enter>", _bound_to_mousewheel) + users_scrollable.bind("<Leave>", _unbound_to_mousewheel) - back_button = ctk.CTkButton( - footer_frame, - text="Back to Admin Dashboard", - command=lambda: switch_func("admin_dashboard"), - font=("Helvetica", 12, "bold"), - height=40, - corner_radius=8, - fg_color="#555555", - hover_color="#444444", - ) - back_button.pack(side="left") + # Helper function to create a row for a user + def create_user_row(user, row_count): + row = ctk.CTkFrame( + user_rows_frame, + fg_color="#252525" if row_count % 2 == 0 else "#2b2b2b", + height=50 + ) + row.pack(fill="x", pady=1) + row.pack_propagate(False) + + # ID cell + id_cell = ctk.CTkFrame(row, fg_color="transparent") + id_cell.place(relx=0, rely=0, relwidth=header_widths[0], relheight=1) + + id_label = ctk.CTkLabel( + id_cell, + text=str(user["id"]), + font=("Helvetica", 12), + text_color="white" + ) + id_label.place(relx=0.5, rely=0.5, anchor="center") - # Add endpoints to the backend for these actions - def fetch_users(): - """Fetch all regular users from the backend""" - headers = {"Authorization": f"Bearer {access_token}"} + # Username cell + username_cell = ctk.CTkFrame(row, fg_color="transparent") + username_cell.place( + relx=header_widths[0], rely=0, relwidth=header_widths[1], relheight=1 + ) - try: - response = requests.get(f"{API_URL}/admin/users", headers=headers) + username_label = ctk.CTkLabel( + username_cell, + text=user.get("username", "N/A"), + font=("Helvetica", 12), + text_color="white" + ) + username_label.place(relx=0.5, rely=0.5, anchor="center") - if response.status_code == 200: - # Clear existing items - for item in user_tree.get_children(): - user_tree.delete(item) + # Email cell + email_cell = ctk.CTkFrame(row, fg_color="transparent") + email_cell.place( + relx=sum(header_widths[:2]), rely=0, relwidth=header_widths[2], relheight=1 + ) - users = response.json() + email_label = ctk.CTkLabel( + email_cell, + text=user.get("email", "N/A"), + font=("Helvetica", 12), + text_color="white" + ) + email_label.place(relx=0.5, rely=0.5, anchor="center") - # Filter users by role (only regular users, not admins) - row_count = 0 - for user in users: - if user["role"] == "buyer": - # Add row with alternating tags for zebra striping - tag = "user" if row_count % 2 == 0 else "" - user_tree.insert( - "", - "end", - values=( - user["id"], - user["username"], - user["email"], - user.get("phone_number", "N/A"), - user["role"], - ), - tags=(tag,), - ) - row_count += 1 - else: - CTkMessagebox( - title="Error", - message=response.json().get("detail", "Failed to fetch users"), - icon="cancel", - ) - except requests.exceptions.RequestException as e: - CTkMessagebox( - title="Connection Error", - message=f"Failed to connect to server: {e}", - icon="cancel", - ) + # Phone cell + phone_cell = ctk.CTkFrame(row, fg_color="transparent") + phone_cell.place( + relx=sum(header_widths[:3]), rely=0, relwidth=header_widths[3], relheight=1 + ) - def delete_user(): - """Delete (ban) the selected user""" - selected_item = user_tree.selection() - if not selected_item: - CTkMessagebox( - title="Warning", - message="Please select a user to delete", - icon="warning", - ) - return + phone_label = ctk.CTkLabel( + phone_cell, + text=user.get("phone_number", "N/A"), + font=("Helvetica", 12), + text_color="white" + ) + phone_label.place(relx=0.5, rely=0.5, anchor="center") + + # Role cell + role_cell = ctk.CTkFrame(row, fg_color="transparent") + role_cell.place( + relx=sum(header_widths[:4]), rely=0, relwidth=header_widths[4], relheight=1 + ) + + role_label = ctk.CTkLabel( + role_cell, + text=user.get("role", "N/A"), + font=("Helvetica", 12), + text_color="white" + ) + role_label.place(relx=0.5, rely=0.5, anchor="center") + + # Actions cell + actions_cell = ctk.CTkFrame(row, fg_color="transparent") + actions_cell.place( + relx=sum(header_widths[:5]), rely=0, relwidth=header_widths[5], relheight=1 + ) - user_id = user_tree.item(selected_item[0])["values"][0] - username = user_tree.item(selected_item[0])["values"][1] + delete_action = ctk.CTkButton( + actions_cell, + text="Delete", + font=("Helvetica", 11, "bold"), + height=30, + width=80, + corner_radius=8, + fg_color="#f44336", + hover_color="#d32f2f", + command=lambda u=user: confirm_delete_user(u) + ) + delete_action.place(relx=0.5, rely=0.5, anchor="center") + return row + + def clear_user_rows(): + """Clear all user rows from the table""" + for row in user_rows: + row.destroy() + user_rows.clear() + + # Function to confirm deletion + def confirm_delete_user(user): # Confirm deletion confirm = CTkMessagebox( title="Confirm Deletion", - message=f"Are you sure you want to delete user '{username}'?", + message=f"Are you sure you want to delete user '{user['username']}'?", icon="question", option_1="Cancel", option_2="Delete", @@ -260,7 +267,7 @@ def admin_user_management_frame(parent, switch_func, API_URL, access_token): try: response = requests.delete( - f"{API_URL}/admin/users/{user_id}", headers=headers + f"{API_URL}/admin/users/{user['id']}", headers=headers ) if response.status_code == 200: @@ -281,6 +288,40 @@ def admin_user_management_frame(parent, switch_func, API_URL, access_token): icon="cancel", ) + # Add endpoints to the backend for these actions + def fetch_users(): + """Fetch all regular users from the backend""" + headers = {"Authorization": f"Bearer {access_token}"} + + try: + response = requests.get(f"{API_URL}/admin/users", headers=headers) + + if response.status_code == 200: + # Clear existing items + clear_user_rows() + + users = response.json() + + # Filter users by role (only regular users, not admins) + row_count = 0 + for user in users: + if user["role"] == "buyer": + row = create_user_row(user, row_count) + user_rows.append(row) + row_count += 1 + else: + CTkMessagebox( + title="Error", + message=response.json().get("detail", "Failed to fetch users"), + icon="cancel", + ) + except requests.exceptions.RequestException as e: + CTkMessagebox( + title="Connection Error", + message=f"Failed to connect to server: {e}", + icon="cancel", + ) + def search_users(): """Search users based on the search term""" search_term = search_entry.get().lower() @@ -288,22 +329,71 @@ def admin_user_management_frame(parent, switch_func, API_URL, access_token): fetch_users() return - # Filter the treeview based on the search term - for item in user_tree.get_children(): - values = user_tree.item(item)["values"] - # Check if search term is in username or email - if ( - search_term in str(values[1]).lower() - or search_term in str(values[2]).lower() - ): - user_tree.item(item, tags=("match",)) + headers = {"Authorization": f"Bearer {access_token}"} + + try: + # We'll fetch all users and filter on the client side for simplicity + response = requests.get(f"{API_URL}/admin/users", headers=headers) + + if response.status_code == 200: + # Clear existing items + clear_user_rows() + + users = response.json() + + # Filter users by search term and role + row_count = 0 + for user in users: + if user["role"] == "buyer" and ( + search_term in str(user.get("username", "")).lower() or + search_term in str(user.get("email", "")).lower() + ): + row = create_user_row(user, row_count) + user_rows.append(row) + row_count += 1 + + if row_count == 0: + # No results found + no_results = ctk.CTkLabel( + user_rows_frame, + text="No users found matching your search", + font=("Helvetica", 14), + text_color="gray" + ) + no_results.pack(pady=20) + user_rows.append(no_results) else: - user_tree.detach(item) # Hide non-matching items + CTkMessagebox( + title="Error", + message=response.json().get("detail", "Failed to fetch users"), + icon="cancel", + ) + except requests.exceptions.RequestException as e: + CTkMessagebox( + title="Connection Error", + message=f"Failed to connect to server: {e}", + icon="cancel", + ) # Connect functions to buttons search_button.configure(command=search_users) refresh_button.configure(command=fetch_users) - delete_button.configure(command=delete_user) + + # Footer with back button + footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) + footer_frame.pack(fill="x", pady=(15, 0)) + + back_button = ctk.CTkButton( + footer_frame, + text="Back to Admin Dashboard", + command=lambda: switch_func("admin_dashboard"), + font=("Helvetica", 12, "bold"), + height=40, + corner_radius=8, + fg_color="#555555", + hover_color="#444444", + ) + back_button.pack(side="left") # Fetch users when the frame is shown def on_frame_shown(): diff --git a/app/frontend/components/owner/owner_orders.py b/app/frontend/components/owner/owner_orders.py index 5ed371d286de0634fc583046738e7d9c5e2630d4..ff5c59552cab566ff3eaea7eb4a40e98881da926 100644 --- a/app/frontend/components/owner/owner_orders.py +++ b/app/frontend/components/owner/owner_orders.py @@ -230,17 +230,26 @@ def owner_orders_frame(parent, switch_func, API_URL, token): ) date_label.pack(anchor="w", pady=(0, 10)) - # Customer info - customer = order_data.get("buyer", {}) - customer_name = customer.get("username", "Unknown") + # Delivery address + address_frame = ctk.CTkFrame(info_frame, fg_color="transparent") + address_frame.pack(fill="x", pady=(0, 10)) + + address_label = ctk.CTkLabel( + address_frame, + text="Delivery Address:", + font=("Helvetica", 12, "bold"), + text_color="#AAAAAA", + ) + address_label.pack(side="left", padx=(0, 5)) - customer_label = ctk.CTkLabel( - info_frame, - text=f"Customer: {customer_name}", - font=("Helvetica", 14), + delivery_address = order_data.get("delivery_address", "No address provided") + address_value = ctk.CTkLabel( + address_frame, + text=delivery_address, + font=("Helvetica", 12), text_color="white", ) - customer_label.pack(anchor="w", pady=(0, 5)) + address_value.pack(side="left") # Total amount total_amount = sum( diff --git a/app/frontend/components/user_orders.py b/app/frontend/components/user_orders.py index 283316c93d81f0bc9e1ce380c08cbcb61998cc3b..4eb08fb9e91ae44daa5774e4bc0d4b02ddbb820f 100644 --- a/app/frontend/components/user_orders.py +++ b/app/frontend/components/user_orders.py @@ -894,9 +894,11 @@ def user_orders_frame(parent, switch_func, API_URL, token): text_color="#AAAAAA", ).pack(side="left", padx=(10, 5)) + # Get delivery address from order + delivery_address = order.get("delivery_address", "No address provided") ctk.CTkLabel( address_frame, - text=order.get("delivery_address", "No address provided"), + text=delivery_address, font=("Helvetica", 12), text_color="white", ).pack(side="left") @@ -1093,31 +1095,11 @@ def user_orders_frame(parent, switch_func, API_URL, token): text_color="#AAAAAA", ).pack(side="left", padx=(10, 5)) + # Get delivery address from order + delivery_address = order.get("delivery_address", "No address provided") ctk.CTkLabel( address_frame, - text=order.get("delivery_address", "No address provided"), - font=("Helvetica", 12), - text_color="white", - ).pack(side="left") - - # Expected delivery date (estimated) - shipping_date = order.get("updated_at", "Unknown date") - if isinstance(shipping_date, str) and len(shipping_date) > 10: - shipping_date = shipping_date[:10] # Just get the date part - - estimated_frame = ctk.CTkFrame(details_frame, fg_color="transparent") - estimated_frame.pack(fill="x", pady=5) - - ctk.CTkLabel( - estimated_frame, - text="Shipped Date:", - font=("Helvetica", 12, "bold"), - text_color="#AAAAAA", - ).pack(side="left", padx=(10, 5)) - - ctk.CTkLabel( - estimated_frame, - text=shipping_date, + text=delivery_address, font=("Helvetica", 12), text_color="white", ).pack(side="left") diff --git a/app/static/default/Adidas/Adidas_shop_logo/adidas_logo.jpg b/app/static/default/Adidas/Adidas_shop_logo/adidas_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4cb5680c81aa7200b045f6e4efe166832a394a6d Binary files /dev/null and b/app/static/default/Adidas/Adidas_shop_logo/adidas_logo.jpg differ diff --git a/app/static/default/Adidas/Product_image/jacket.jpg b/app/static/default/Adidas/Product_image/jacket.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a2995d48ba5061cdaed4e155fdac496b6da2b21 Binary files /dev/null and b/app/static/default/Adidas/Product_image/jacket.jpg differ diff --git a/app/static/default/Adidas/Product_image/shoes_adidas.jpg b/app/static/default/Adidas/Product_image/shoes_adidas.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d0e3a48e2b26c50b05780a5ce96441d429db91fd Binary files /dev/null and b/app/static/default/Adidas/Product_image/shoes_adidas.jpg differ diff --git a/app/static/default/GameStop/GameStop_logo/gamestop_logo.jpg b/app/static/default/GameStop/GameStop_logo/gamestop_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..33110a6ebfee1f6b4171dc4198be2fc08ec28ce9 Binary files /dev/null and b/app/static/default/GameStop/GameStop_logo/gamestop_logo.jpg differ diff --git a/app/static/default/GameStop/Product_gamestop_imagee/Call_of_duty_modern_warfare_ps5.jpg b/app/static/default/GameStop/Product_gamestop_imagee/Call_of_duty_modern_warfare_ps5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..86c8bdffac22edc790e3d1a6aef474232d0e290b Binary files /dev/null and b/app/static/default/GameStop/Product_gamestop_imagee/Call_of_duty_modern_warfare_ps5.jpg differ diff --git a/app/static/default/GameStop/Product_gamestop_imagee/sony_playstation_ps5_slim_digital.jpg b/app/static/default/GameStop/Product_gamestop_imagee/sony_playstation_ps5_slim_digital.jpg new file mode 100644 index 0000000000000000000000000000000000000000..044fdfac6019200185d2ac64790ac1d0cc0184c1 Binary files /dev/null and b/app/static/default/GameStop/Product_gamestop_imagee/sony_playstation_ps5_slim_digital.jpg differ diff --git a/app/static/default/Lego/Lego_shop_logo/lego_logo.jpg b/app/static/default/Lego/Lego_shop_logo/lego_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d2674720c21eb633b15898be9193dd3f81ded1e Binary files /dev/null and b/app/static/default/Lego/Lego_shop_logo/lego_logo.jpg differ diff --git a/app/static/default/Lego/Product_lego_image/lego_city.jpg b/app/static/default/Lego/Product_lego_image/lego_city.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c88a7ac3b11e3148d506f13857a47642b555d286 Binary files /dev/null and b/app/static/default/Lego/Product_lego_image/lego_city.jpg differ diff --git a/app/static/default/Lego/Product_lego_image/lego_city_special_edition.jpg b/app/static/default/Lego/Product_lego_image/lego_city_special_edition.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1077679c11d583b431382b8f7ea920c084111b23 Binary files /dev/null and b/app/static/default/Lego/Product_lego_image/lego_city_special_edition.jpg differ diff --git a/app/static/default/Nike/Nike_shop_logo/nike_logo.jpg b/app/static/default/Nike/Nike_shop_logo/nike_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5d9d27f6dcefa0a5afbc854ba704e683caf55d1b Binary files /dev/null and b/app/static/default/Nike/Nike_shop_logo/nike_logo.jpg differ diff --git a/app/static/default/Nike/Product_nike_image/shoes.jpg b/app/static/default/Nike/Product_nike_image/shoes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..10c01097e78c9f33c46d1345432d06636a3b07b2 Binary files /dev/null and b/app/static/default/Nike/Product_nike_image/shoes.jpg differ diff --git a/app/static/default/Nike/Product_nike_image/sweater.jpg b/app/static/default/Nike/Product_nike_image/sweater.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a716704490fdb5a05a0433acf3ae40bca688c722 Binary files /dev/null and b/app/static/default/Nike/Product_nike_image/sweater.jpg differ diff --git a/app/static/default/default_product.png b/app/static/default/default_product.png deleted file mode 100644 index f8a26b2d08887153a17797e6762ee17d71a89d08..0000000000000000000000000000000000000000 Binary files a/app/static/default/default_product.png and /dev/null differ diff --git a/app/static/default/default_shop.png b/app/static/default/default_shop.png deleted file mode 100644 index ed0ec74950384a12f8f40a8e3bba9d1247c4c562..0000000000000000000000000000000000000000 Binary files a/app/static/default/default_shop.png and /dev/null differ