diff --git a/app/backend/main.py b/app/backend/main.py index a9f862a64f3d884a89b9985c8e2661cb57584dcb..d6995aceddf6fb7e6093eb6b7831b489161d0ff5 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -5,7 +5,17 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from backend.routes import auth, shop, product, category, search, order, payment, cart, admin +from backend.routes import ( + auth, + shop, + product, + category, + search, + order, + payment, + cart, + admin, +) from backend.database import init_db from core.config import settings diff --git a/app/backend/routes/admin.py b/app/backend/routes/admin.py index 3ba82d2f32ab9d0aa6e3bd1a5db6833f54fa7818..c858dbf140cc31ae4ed99cc39a96d29aec8bf042 100644 --- a/app/backend/routes/admin.py +++ b/app/backend/routes/admin.py @@ -23,7 +23,7 @@ def get_all_users( ): """Get all users""" verify_admin(current_user) - + users = session.exec(select(User)).all() return users @@ -36,7 +36,7 @@ def get_user( ): """Get a specific user""" verify_admin(current_user) - + user = session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -51,23 +51,23 @@ def delete_user( ): """Delete a user (ban)""" verify_admin(current_user) - + # Prevent admin from deleting themselves if user_id == current_user.id: raise HTTPException(status_code=400, detail="Cannot delete own account") - + # Prevent admin from deleting other admins user = session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") - + if user.role == "admin": raise HTTPException(status_code=400, detail="Cannot delete admin accounts") - + # Delete the user session.delete(user) session.commit() - + return {"message": "User deleted successfully"} @@ -78,14 +78,14 @@ def get_shop_owners( ): """Get all shop owners""" verify_admin(current_user) - + # Get users with role="shop_owner" shop_owners = session.exec(select(User).where(User.role == "shop_owner")).all() - + # For each shop owner, get their shops for owner in shop_owners: owner.shops = session.exec(select(Shop).where(Shop.owner_id == owner.id)).all() - + return shop_owners @@ -97,17 +97,17 @@ def get_shop_owner( ): """Get a specific shop owner""" verify_admin(current_user) - + owner = session.get(User, owner_id) if not owner: raise HTTPException(status_code=404, detail="Shop owner not found") - + if owner.role != "shop_owner": raise HTTPException(status_code=400, detail="User is not a shop owner") - + # Get the owner's shops owner.shops = session.exec(select(Shop).where(Shop.owner_id == owner.id)).all() - + return owner @@ -119,18 +119,18 @@ def get_owner_shops( ): """Get shops owned by a specific shop owner""" verify_admin(current_user) - + # Check if the owner exists owner = session.get(User, owner_id) if not owner: raise HTTPException(status_code=404, detail="Shop owner not found") - + if owner.role != "shop_owner": raise HTTPException(status_code=400, detail="User is not a shop owner") - + # Get the owner's shops shops = session.exec(select(Shop).where(Shop.owner_id == owner_id)).all() - + return shops @@ -142,28 +142,28 @@ def delete_shop_owner( ): """Delete a shop owner (ban)""" verify_admin(current_user) - + # Prevent admin from deleting themselves if owner_id == current_user.id: raise HTTPException(status_code=400, detail="Cannot delete own account") - + # Verify the owner exists owner = session.get(User, owner_id) if not owner: raise HTTPException(status_code=404, detail="Shop owner not found") - + if owner.role != "shop_owner": raise HTTPException(status_code=400, detail="User is not a shop owner") - + # Delete all shops owned by this user first shops = session.exec(select(Shop).where(Shop.owner_id == owner_id)).all() for shop in shops: session.delete(shop) - + # Delete the owner session.delete(owner) session.commit() - + return {"message": "Shop owner deleted successfully"} @@ -174,6 +174,6 @@ def get_all_categories( ): """Get all categories for admin panel""" verify_admin(current_user) - + categories = session.exec(select(Category)).all() - return categories \ No newline at end of file + return categories diff --git a/app/backend/routes/category.py b/app/backend/routes/category.py index 16f48b34cf78c6f84975c32ae837baaa8c2777cd..60e46d62c5e810d8c6514bf7d3441340a5c40aec 100644 --- a/app/backend/routes/category.py +++ b/app/backend/routes/category.py @@ -24,9 +24,11 @@ def create_category( current_user: User = Depends(get_current_user), ): verify_admin(current_user) - + # Check if category with this name already exists - existing_category = session.exec(select(Category).where(Category.name == name)).first() + existing_category = session.exec( + select(Category).where(Category.name == name) + ).first() if existing_category: raise HTTPException( status_code=400, detail=f"Category with name '{name}' already exists" @@ -84,7 +86,8 @@ def update_category( except IntegrityError: session.rollback() raise HTTPException( - status_code=400, detail="Failed to update category. Name may be a duplicate." + status_code=400, + detail="Failed to update category. Name may be a duplicate.", ) @@ -99,19 +102,19 @@ def delete_category( category = session.get(Category, category_id) if not category: raise HTTPException(status_code=404, detail="Category not found") - + # Check if any products are using this category products_using_category = session.exec( select(Product).where(Product.category_id == category_id) ).all() - + if products_using_category: product_count = len(products_using_category) raise HTTPException( - status_code=400, - detail=f"Cannot delete category: {product_count} products are using this category. Please reassign or delete those products first." + status_code=400, + detail=f"Cannot delete category: {product_count} products are using this category. Please reassign or delete those products first.", ) - + session.delete(category) session.commit() return {"message": "Category deleted successfully"} diff --git a/app/backend/routes/product.py b/app/backend/routes/product.py index 4c30adbdc616b7362d1e12395d9b2d6f9fac6ed5..aad6023040571c97df66229cdb23e0f7b01f4892 100644 --- a/app/backend/routes/product.py +++ b/app/backend/routes/product.py @@ -2,7 +2,15 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from sqlmodel import Session, func, select from sqlalchemy.orm import joinedload from datetime import datetime -from backend.models.models import Product, ProductImage, User, Shop, OrderItem, CartItem, Category +from backend.models.models import ( + Product, + ProductImage, + User, + Shop, + OrderItem, + CartItem, + Category, +) from backend.schemas.product import ProductRead, ProductUpdate from backend.database import get_session from backend.routes.auth import get_current_user @@ -90,7 +98,9 @@ def read_all_products(order: str = "desc", session: Session = Depends(get_sessio ) products = ( session.query(Product) - .options(joinedload(Product.category)) # Explicitly load the category relationship + .options( + joinedload(Product.category) + ) # Explicitly load the category relationship .outerjoin(OrderItem, Product.id == OrderItem.product_id) .group_by(Product.id) .order_by(order_by) @@ -114,7 +124,9 @@ def get_shop_products( if shop.owner_id != current_user.id: products = ( session.query(Product) - .options(joinedload(Product.category)) # Explicitly load the category relationship + .options( + joinedload(Product.category) + ) # Explicitly load the category relationship .filter( Product.shop_id == shop_id, Product.stock > 0, # Only available products @@ -125,7 +137,9 @@ def get_shop_products( # Shop owner can see all their products products = ( session.query(Product) - .options(joinedload(Product.category)) # Explicitly load the category relationship + .options( + joinedload(Product.category) + ) # Explicitly load the category relationship .filter(Product.shop_id == shop_id) .all() ) @@ -138,7 +152,7 @@ def read_product(product_id: int, session: Session = Depends(get_session)): product = session.get(Product, product_id) if not product: raise HTTPException(status_code=404, detail="Product not found") - + # Make sure category is loaded session.refresh(product, ["category"]) return product diff --git a/app/backend/schemas/category.py b/app/backend/schemas/category.py index fbc1e0634cf25e0f722102255e6ac3448482778c..f508a2d806b90d8ed6eac25594a15f97814130c8 100644 --- a/app/backend/schemas/category.py +++ b/app/backend/schemas/category.py @@ -5,7 +5,7 @@ from typing import Optional class CategoryBase(BaseModel): name: str - + class Config: from_attributes = True @@ -16,7 +16,7 @@ class CategoryCreate(CategoryBase): class CategoryRead(CategoryBase): id: int - + class Config: from_attributes = True diff --git a/app/backend/schemas/product.py b/app/backend/schemas/product.py index ac8d818a9f6d21cbaf47beb4b4a16ac3900fbfa5..e806bf35e5f5aa099af0be2e4789311206bcf84d 100644 --- a/app/backend/schemas/product.py +++ b/app/backend/schemas/product.py @@ -6,13 +6,13 @@ from datetime import datetime class CategoryBase(BaseModel): id: int name: str - + class Config: from_attributes = True # Forward references for circular relationships -ProductImageRead = ForwardRef('ProductImageRead') +ProductImageRead = ForwardRef("ProductImageRead") class ProductBase(BaseModel): diff --git a/app/backend/scripts/admin_init.py b/app/backend/scripts/admin_init.py index 70ac94a7aa6ce155dccdc9285dac6b5f72b9ce30..5a52ec5d0d2c1cf0d936b00a1a2a50701e13b785 100644 --- a/app/backend/scripts/admin_init.py +++ b/app/backend/scripts/admin_init.py @@ -2,7 +2,9 @@ import sys import os # Add the parent directory to the path to allow importing from the backend -sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +) from backend.database import get_session, engine from backend.models.models import SQLModel, User @@ -13,18 +15,18 @@ from sqlmodel import select, Session def init_admin(): """Initialize a default admin user if no admin user exists""" print("Initializing admin user...") - + # Create all tables if they don't exist SQLModel.metadata.create_all(engine) - + with Session(engine) as session: # Check if admin user already exists admin = session.exec(select(User).where(User.role == "admin")).first() - + if admin: print("Admin user already exists.") return - + print("Creating default admin user...") # Create default admin user with username and password both "admin" hashed_password = hash_password("admin") @@ -33,13 +35,13 @@ def init_admin(): email="admin@example.com", password=hashed_password, phone_number="1234567890", - role="admin" + role="admin", ) - + session.add(admin_user) session.commit() print("Default admin user created successfully!") if __name__ == "__main__": - init_admin() \ No newline at end of file + init_admin() diff --git a/app/frontend/components/admin/category.py b/app/frontend/components/admin/category.py index 270f94f0ce86126002d7cfd63753da8ff641cdb3..3504aa38db65021accc1a065211c3b3827e63d34 100644 --- a/app/frontend/components/admin/category.py +++ b/app/frontend/components/admin/category.py @@ -7,246 +7,233 @@ from datetime import datetime def category_frame(parent, switch_func, API_URL, access_token): frame = ctk.CTkFrame(parent) - + # Update token function for consistency with other frames def update_token(new_token): nonlocal access_token headers["Authorization"] = f"Bearer {new_token}" access_token = new_token - + frame.update_token = update_token - + # 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="#9c27b0") 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="Category Management", + title_frame, + text="Category Management", font=("Helvetica", 24, "bold"), - text_color="#ffffff" + text_color="#ffffff", ).pack(anchor="w") - + ctk.CTkLabel( - title_frame, - text="Create, view, update and delete product categories", + title_frame, + text="Create, view, update and delete product categories", font=("Helvetica", 14), - text_color="#e0e0e0" + text_color="#e0e0e0", ).pack(anchor="w") - + # Two column layout content_frame = ctk.CTkFrame(main_container) content_frame.pack(fill="both", expand=True) - + # Left column - category form form_frame = ctk.CTkFrame(content_frame, fg_color="transparent") form_frame.pack(side="left", fill="both", expand=True, padx=20, pady=20) - + # Form section title ctk.CTkLabel( - form_frame, - text="Category Details", - font=("Helvetica", 16, "bold") + form_frame, text="Category Details", font=("Helvetica", 16, "bold") ).pack(anchor="w", pady=(0, 15)) - + # Form fields with better spacing and styling input_frame = ctk.CTkFrame(form_frame, fg_color="#f5f5f5", corner_radius=10) input_frame.pack(fill="x", pady=10) - + # Category name input name_frame = ctk.CTkFrame(input_frame, fg_color="transparent") name_frame.pack(fill="x", padx=20, pady=15) - + ctk.CTkLabel( - name_frame, - text="Category Name:", + name_frame, + text="Category Name:", font=("Helvetica", 12, "bold"), - text_color="#333333" + text_color="#333333", ).pack(side="left", padx=(0, 10)) - + entry_name = ctk.CTkEntry( - name_frame, - width=250, - height=35, - placeholder_text="Enter category name" + name_frame, width=250, height=35, placeholder_text="Enter category name" ) entry_name.pack(side="left", fill="x", expand=True) - + # Category ID input (for update/delete) id_frame = ctk.CTkFrame(input_frame, fg_color="transparent") id_frame.pack(fill="x", padx=20, pady=15) - + ctk.CTkLabel( - id_frame, - text="Category ID:", + id_frame, + text="Category ID:", font=("Helvetica", 12, "bold"), - text_color="#333333" + text_color="#333333", ).pack(side="left", padx=(0, 10)) - + entry_id = ctk.CTkEntry( - id_frame, - width=150, - height=35, - placeholder_text="For update/delete" + id_frame, width=150, height=35, placeholder_text="For update/delete" ) entry_id.pack(side="left") - + # Buttons frame buttons_frame = ctk.CTkFrame(form_frame, fg_color="transparent") buttons_frame.pack(fill="x", pady=15) - + # Row 1 of buttons buttons_row1 = ctk.CTkFrame(buttons_frame, fg_color="transparent") buttons_row1.pack(fill="x", pady=5) - + create_button = ctk.CTkButton( - buttons_row1, - text="Create Category", + buttons_row1, + text="Create Category", font=("Helvetica", 12, "bold"), height=40, fg_color="#4caf50", hover_color="#388e3c", - corner_radius=8 + corner_radius=8, ) create_button.pack(side="left", padx=(0, 10), fill="x", expand=True) - + update_button = ctk.CTkButton( - buttons_row1, - text="Update Category", + buttons_row1, + text="Update Category", font=("Helvetica", 12, "bold"), height=40, fg_color="#ff9800", hover_color="#f57c00", - corner_radius=8 + corner_radius=8, ) update_button.pack(side="left", fill="x", expand=True) - + # Row 2 of buttons buttons_row2 = ctk.CTkFrame(buttons_frame, fg_color="transparent") buttons_row2.pack(fill="x", pady=5) - + list_button = ctk.CTkButton( - buttons_row2, - text="List Categories", + buttons_row2, + text="List Categories", font=("Helvetica", 12, "bold"), height=40, fg_color="#3a7ebf", hover_color="#2a6da9", - corner_radius=8 + corner_radius=8, ) list_button.pack(side="left", padx=(0, 10), fill="x", expand=True) - + delete_button = ctk.CTkButton( - buttons_row2, - text="Delete Category", + buttons_row2, + text="Delete Category", font=("Helvetica", 12, "bold"), height=40, fg_color="#f44336", hover_color="#d32f2f", - corner_radius=8 + corner_radius=8, ) delete_button.pack(side="left", fill="x", expand=True) - + # Right column - category list table table_frame = ctk.CTkFrame(content_frame) table_frame.pack(side="right", fill="both", expand=True, padx=20, pady=20) - + # Table title ctk.CTkLabel( - table_frame, - text="Category List", - font=("Helvetica", 16, "bold") + table_frame, text="Category List", font=("Helvetica", 16, "bold") ).pack(anchor="w", padx=15, pady=15) - + # Create TreeView for categories tree_container = ctk.CTkFrame(table_frame) tree_container.pack(fill="both", expand=True, padx=15, pady=(0, 15)) - + # 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", + "Treeview", background="#2b2b2b", foreground="#ffffff", fieldbackground="#2b2b2b", borderwidth=0, - rowheight=35 + rowheight=35, ) - + # Configure the headings style.configure( - "Treeview.Heading", - background="#9c27b0", + "Treeview.Heading", + background="#9c27b0", foreground="#ffffff", borderwidth=0, - font=("Helvetica", 12, "bold") + font=("Helvetica", 12, "bold"), ) - + # Selection color style.map("Treeview", background=[("selected", "#9c27b0")]) - + # Create scrollbar scrollbar = ttk.Scrollbar(tree_container) scrollbar.pack(side="right", fill="y") - + # Create the TreeView columns = ("id", "name", "total_products") category_tree = ttk.Treeview( - tree_container, - columns=columns, - show="headings", - yscrollcommand=scrollbar.set + tree_container, columns=columns, show="headings", yscrollcommand=scrollbar.set ) - + # Configure scrollbar scrollbar.config(command=category_tree.yview) - + # Define headings category_tree.heading("id", text="ID") category_tree.heading("name", text="Category Name") category_tree.heading("total_products", text="Products") - + # Define column widths category_tree.column("id", width=60, anchor="center") category_tree.column("name", width=200) category_tree.column("total_products", width=80, anchor="center") - + # Add tag configurations category_tree.tag_configure("odd", background="#252525") category_tree.tag_configure("even", background="#2d2d2d") - + category_tree.pack(fill="both", expand=True) - + # 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", + 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" + hover_color="#444444", ) back_button.pack(side="left") - + # Set up auth headers headers = {"Authorization": f"Bearer {access_token}"} if access_token else {} - + # Enhanced category functions with table updates def fetch_categories(): """Fetch and display categories in the treeview""" @@ -256,55 +243,54 @@ def category_frame(parent, switch_func, API_URL, access_token): # Clear existing items for item in category_tree.get_children(): category_tree.delete(item) - + categories = response.json() - + if categories: for i, cat in enumerate(categories): # Get product count (placeholder - would need backend support) product_count = len(cat.get("products", [])) - + # Add with alternating row colors tag = "even" if i % 2 == 0 else "odd" category_tree.insert( - "", "end", + "", + "end", values=(cat["id"], cat["name"], product_count), - tags=(tag,) + tags=(tag,), ) else: # Empty message could be displayed here pass - + return categories elif response.status_code == 403: CTkMessagebox( title="Error", message="Unauthorized. Admin access required.", - icon="cancel" + icon="cancel", ) return [] else: CTkMessagebox( - title="Error", - message="Failed to fetch categories", - icon="cancel" + title="Error", message="Failed to fetch categories", icon="cancel" ) return [] except requests.exceptions.RequestException as e: CTkMessagebox( title="Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) return [] - + def create_category(): name = entry_name.get().strip() if not name: CTkMessagebox( title="Input Error", message="Category name is required!", - icon="warning" + icon="warning", ) return @@ -316,7 +302,7 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Success", message=f"Category '{name}' created successfully!", - icon="check" + icon="check", ) entry_name.delete(0, "end") fetch_categories() # Refresh the list @@ -324,7 +310,7 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Error", message="Unauthorized. Admin access required.", - icon="cancel" + icon="cancel", ) else: error_detail = "Failed to create category" @@ -334,36 +320,26 @@ def category_frame(parent, switch_func, API_URL, access_token): error_detail = error_data["detail"] except: pass - CTkMessagebox( - title="Error", - message=error_detail, - icon="cancel" - ) + CTkMessagebox(title="Error", message=error_detail, icon="cancel") except requests.exceptions.RequestException as e: CTkMessagebox( title="Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) def list_categories_dialog(): """Show categories in a dialog and also update the treeview""" categories = fetch_categories() - + if categories: category_list = "\n".join( [f"{cat['id']}: {cat['name']}" for cat in categories] ) - CTkMessagebox( - title="Categories", - message=category_list, - icon="info" - ) + CTkMessagebox(title="Categories", message=category_list, icon="info") else: CTkMessagebox( - title="Categories", - message="No categories available.", - icon="info" + title="Categories", message="No categories available.", icon="info" ) def update_category(): @@ -373,14 +349,14 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Input Error", message="Enter a valid Category ID!", - icon="warning" + icon="warning", ) return if not name: CTkMessagebox( title="Input Error", message="Category name is required for update!", - icon="warning" + icon="warning", ) return @@ -394,7 +370,7 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Success", message="Category updated successfully!", - icon="check" + icon="check", ) entry_id.delete(0, "end") entry_name.delete(0, "end") @@ -403,7 +379,7 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Error", message="Unauthorized. Admin access required.", - icon="cancel" + icon="cancel", ) else: error_detail = "Failed to update category" @@ -413,16 +389,12 @@ def category_frame(parent, switch_func, API_URL, access_token): error_detail = error_data["detail"] except: pass - CTkMessagebox( - title="Error", - message=error_detail, - icon="cancel" - ) + CTkMessagebox(title="Error", message=error_detail, icon="cancel") except requests.exceptions.RequestException as e: CTkMessagebox( title="Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) def delete_category(): @@ -431,7 +403,7 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Input Error", message="Enter a valid Category ID!", - icon="warning" + icon="warning", ) return @@ -443,7 +415,7 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Success", message="Category deleted successfully!", - icon="check" + icon="check", ) entry_id.delete(0, "end") fetch_categories() # Refresh the list @@ -451,31 +423,31 @@ def category_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Error", message="Unauthorized. Admin access required.", - icon="cancel" + icon="cancel", ) elif response.status_code == 400: try: error_data = response.json() - error_message = error_data.get("detail", "Failed to delete category") - + error_message = error_data.get( + "detail", "Failed to delete category" + ) + # Check if the error is about products using the category if "products are using this category" in error_message: CTkMessagebox( title="Cannot Delete Category", message=error_message, - icon="warning" + icon="warning", ) else: CTkMessagebox( - title="Error", - message=error_message, - icon="cancel" + title="Error", message=error_message, icon="cancel" ) except: CTkMessagebox( title="Error", message="Failed to delete category. Please try again.", - icon="cancel" + icon="cancel", ) else: error_detail = "Failed to delete category" @@ -485,18 +457,14 @@ def category_frame(parent, switch_func, API_URL, access_token): error_detail = error_data["detail"] except: pass - CTkMessagebox( - title="Error", - message=error_detail, - icon="cancel" - ) + CTkMessagebox(title="Error", message=error_detail, icon="cancel") except requests.exceptions.RequestException as e: CTkMessagebox( title="Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) - + # Helper function to fill form from selected category def on_category_select(event): selected_item = category_tree.selection() @@ -504,10 +472,10 @@ def category_frame(parent, switch_func, API_URL, access_token): values = category_tree.item(selected_item[0])["values"] entry_id.delete(0, "end") entry_id.insert(0, values[0]) - + entry_name.delete(0, "end") entry_name.insert(0, values[1]) - + # Bind the selection event category_tree.bind("<<TreeviewSelect>>", on_category_select) @@ -516,7 +484,7 @@ def category_frame(parent, switch_func, API_URL, access_token): list_button.configure(command=list_categories_dialog) update_button.configure(command=update_category) delete_button.configure(command=delete_category) - + # Initial fetch of categories frame.after(100, fetch_categories) diff --git a/app/frontend/components/admin/category_management.py b/app/frontend/components/admin/category_management.py index 0bfcf56bf447bcf2e27d871e347553bbeac57f56..ac130e5027364e2fc6a36667cc5de065cd8d3d10 100644 --- a/app/frontend/components/admin/category_management.py +++ b/app/frontend/components/admin/category_management.py @@ -4,155 +4,164 @@ from CTkMessagebox import CTkMessagebox import requests from datetime import datetime + def admin_category_management_frame(parent, switch_func, API_URL, access_token): """ Admin dashboard component for managing product categories """ 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="Category Management", + title_frame, + text="Category Management", font=("Helvetica", 24, "bold"), - text_color="#ffffff" + text_color="#ffffff", ).pack(anchor="w") - + ctk.CTkLabel( - title_frame, - text="Manage marketplace product categories", + title_frame, + text="Manage marketplace product categories", font=("Helvetica", 14), - text_color="#e0e0e0" + text_color="#e0e0e0", ).pack(anchor="w") - + # Control panel control_panel = ctk.CTkFrame(main_container, corner_radius=10, height=80) control_panel.pack(fill="x", pady=(0, 15)) - + # Left side - search search_frame = ctk.CTkFrame(control_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 categories...") + + 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 categories..." + ) search_entry.pack(side="left", padx=(0, 10)) - + search_button = ctk.CTkButton( - search_frame, - text="Search", + search_frame, + text="Search", font=("Helvetica", 12, "bold"), height=35, corner_radius=8, fg_color="#2e8b57", - hover_color="#1f6e42" + hover_color="#1f6e42", ) search_button.pack(side="left") - + # Right side - actions actions_frame = ctk.CTkFrame(control_panel, fg_color="transparent") actions_frame.pack(side="right", padx=20, pady=15, fill="y") - + add_button = ctk.CTkButton( - actions_frame, - text="Add Category", + actions_frame, + text="Add Category", font=("Helvetica", 12, "bold"), height=35, width=120, corner_radius=8, fg_color="#4caf50", - hover_color="#388e3c" + hover_color="#388e3c", ) add_button.pack(side="left", padx=(0, 10)) - + edit_button = ctk.CTkButton( - actions_frame, - text="Edit", + actions_frame, + text="Edit", font=("Helvetica", 12, "bold"), height=35, width=80, corner_radius=8, fg_color="#3a7ebf", - hover_color="#2a6da9" + hover_color="#2a6da9", ) edit_button.pack(side="left", padx=(0, 10)) - + delete_button = ctk.CTkButton( - actions_frame, - text="Delete", + actions_frame, + text="Delete", font=("Helvetica", 12, "bold"), height=35, width=80, corner_radius=8, fg_color="#f44336", - hover_color="#d32f2f" + hover_color="#d32f2f", ) delete_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", + "Treeview", background="#2b2b2b", foreground="#ffffff", fieldbackground="#2b2b2b", borderwidth=0, - rowheight=40 + rowheight=40, ) - + # Configure the headings style.configure( - "Treeview.Heading", - background="#2e8b57", + "Treeview.Heading", + background="#2e8b57", foreground="#ffffff", borderwidth=0, - font=("Helvetica", 12, "bold") + 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 - columns = ("id", "name", "description", "product_count", "created_at", "parent_category") + columns = ( + "id", + "name", + "description", + "product_count", + "created_at", + "parent_category", + ) category_tree = ttk.Treeview( - table_frame, - columns=columns, - show="headings", - yscrollcommand=scrollbar.set + table_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set ) - + # Configure scrollbar scrollbar.config(command=category_tree.yview) - + # Define column headings category_tree.heading("id", text="ID") category_tree.heading("name", text="Category Name") @@ -160,7 +169,7 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): category_tree.heading("product_count", text="Products") category_tree.heading("created_at", text="Created At") category_tree.heading("parent_category", text="Parent Category") - + # Define column widths and alignment category_tree.column("id", width=60, anchor="center") category_tree.column("name", width=150) @@ -168,87 +177,88 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): 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) - + category_tree.pack(fill="both", expand=True, padx=5, pady=5) - + # 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", + 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" + hover_color="#444444", ) back_button.pack(side="left") - + # Function to fetch categories def fetch_categories(search_term=None): """Fetch categories from backend""" headers = {"Authorization": f"Bearer {access_token}"} - + params = {} if search_term: params["search"] = search_term - + try: - response = requests.get(f"{API_URL}/admin/categories", headers=headers, params=params) - + response = requests.get( + f"{API_URL}/admin/categories", headers=headers, params=params + ) + if response.status_code == 200: # Clear current items for item in category_tree.get_children(): category_tree.delete(item) - + 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")) + 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", + "", + "end", values=( category["id"], category.get("name", "Unnamed Category"), category.get("description", ""), category.get("product_count", 0), created_at, - parent_category - ) + parent_category, + ), ) else: CTkMessagebox( - title="Error", - message="Failed to fetch categories", - icon="cancel" + title="Error", message="Failed to fetch categories", icon="cancel" ) except Exception as e: CTkMessagebox( - title="Error", - message=f"An error occurred: {str(e)}", - icon="cancel" + 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() @@ -256,110 +266,105 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Warning", message="Please select a category to delete", - icon="warning" + 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] - + category_id = category_tree.item(selected_item[0])["values"][0] + category_name = category_tree.item(selected_item[0])["values"][1] + # Confirm deletion confirm = CTkMessagebox( title="Confirm Deletion", 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" + option_2="Delete", ) - + if confirm.get() != "Delete": return - + try: response = requests.delete( - f"{API_URL}/category/delete/{category_id}", - headers={"Authorization": f"Bearer {access_token}"} + 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!", - icon="check" + title="Success", + message=f"Category '{category_name}' deleted successfully!", + icon="check", ) fetch_categories() else: try: error_data = response.json() - error_message = error_data.get("detail", "Failed to delete category. Please try again.") - + error_message = error_data.get( + "detail", "Failed to delete category. Please try again." + ) + # Check if the error is about products using the category if "products are using this category" in error_message: CTkMessagebox( title="Cannot Delete Category", message=error_message, - icon="warning" + icon="warning", ) else: CTkMessagebox( - title="Error", - message=error_message, - icon="cancel" + title="Error", message=error_message, icon="cancel" ) except: CTkMessagebox( - title="Error", - message="Failed to delete category. Please try again.", - icon="cancel" + title="Error", + message="Failed to delete category. Please try again.", + icon="cancel", ) except Exception as e: CTkMessagebox( - title="Error", - message=f"An error occurred: {str(e)}", - icon="cancel" + title="Error", message=f"An error occurred: {str(e)}", icon="cancel" ) - + # 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", + title="Warning", message="Please select a category to edit", - icon="warning" + 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 + f"{API_URL}/admin/categories/{category_id}", headers=headers ) - + if response.status_code == 200: category = response.json() show_category_dialog("Edit Category", category) else: CTkMessagebox( - title="Error", + title="Error", message="Failed to fetch category details", - icon="cancel" + icon="cancel", ) except Exception as e: CTkMessagebox( - title="Error", - message=f"An error occurred: {str(e)}", - icon="cancel" + title="Error", message=f"An error occurred: {str(e)}", icon="cancel" ) - + def show_category_dialog(title, category=None): """Show dialog for adding/editing a category""" dialog = ctk.CTkToplevel(frame) @@ -368,92 +373,88 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): dialog.resizable(False, False) dialog.transient(frame) dialog.grab_set() - + # Form container form_frame = ctk.CTkFrame(dialog, fg_color="transparent") form_frame.pack(padx=20, pady=20, fill="both", expand=True) - + # Category name name_label = ctk.CTkLabel( - form_frame, - text="Category Name:", - font=("Helvetica", 12, "bold") + form_frame, text="Category Name:", font=("Helvetica", 12, "bold") ) name_label.pack(anchor="w", pady=(0, 5)) - + name_entry = ctk.CTkEntry(form_frame, width=460, height=35) name_entry.pack(fill="x", pady=(0, 15)) - + # Description desc_label = ctk.CTkLabel( - form_frame, - text="Description:", - font=("Helvetica", 12, "bold") + form_frame, text="Description:", font=("Helvetica", 12, "bold") ) desc_label.pack(anchor="w", pady=(0, 5)) - + desc_entry = ctk.CTkTextbox(form_frame, width=460, height=100) desc_entry.pack(fill="x", pady=(0, 15)) - + # Parent category parent_label = ctk.CTkLabel( - form_frame, - text="Parent Category (optional):", - font=("Helvetica", 12, "bold") + form_frame, + text="Parent Category (optional):", + font=("Helvetica", 12, "bold"), ) parent_label.pack(anchor="w", pady=(0, 5)) - + # Get all categories for parent dropdown def get_all_categories(): try: headers = {"Authorization": f"Bearer {access_token}"} response = requests.get(f"{API_URL}/admin/categories", headers=headers) - + if response.status_code == 200: categories = response.json() parent_options = ["None"] - + for cat in categories: # Don't include itself as a parent option when editing if category and cat["id"] == category["id"]: continue parent_options.append(f"{cat['name']} (ID: {cat['id']})") - + return parent_options else: return ["None"] except: return ["None"] - + parent_options = get_all_categories() parent_var = ctk.StringVar(value="None") - + parent_menu = ctk.CTkOptionMenu( form_frame, values=parent_options, variable=parent_var, width=460, height=35, - dropdown_font=("Helvetica", 12) + dropdown_font=("Helvetica", 12), ) parent_menu.pack(fill="x", pady=(0, 20)) - + # Fill fields if editing if category: name_entry.insert(0, category.get("name", "")) desc_entry.insert("1.0", category.get("description", "")) - + # Set parent category if it exists if "parent_id" in category and category["parent_id"]: for option in parent_options: if f"(ID: {category['parent_id']})" in option: parent_var.set(option) break - + # Buttons button_frame = ctk.CTkFrame(form_frame, fg_color="transparent") button_frame.pack(fill="x", pady=(10, 0)) - + cancel_button = ctk.CTkButton( button_frame, text="Cancel", @@ -461,24 +462,22 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): width=100, fg_color="#9e9e9e", hover_color="#757575", - command=dialog.destroy + command=dialog.destroy, ) cancel_button.pack(side="left", padx=(0, 10)) - + # Save function def save_category(): name = name_entry.get().strip() description = desc_entry.get("1.0", "end-1c").strip() parent = parent_var.get() - + if not name: CTkMessagebox( - title="Warning", - message="Category name is required", - icon="warning" + title="Warning", message="Category name is required", icon="warning" ) return - + # Extract parent ID if selected parent_id = None if parent != "None": @@ -486,35 +485,33 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): parent_id = int(parent.split("ID: ")[1].split(")")[0]) except: pass - + # Prepare data - data = { - "name": name, - "description": description, - "parent_id": parent_id - } - + data = {"name": name, "description": description, "parent_id": parent_id} + headers = {"Authorization": f"Bearer {access_token}"} - + try: if category: # Editing existing category response = requests.put( f"{API_URL}/category/put/{category['id']}", headers=headers, - data={"name": name} # Use data format for form data + data={"name": name}, # Use data format for form data ) else: # Adding new category response = requests.post( f"{API_URL}/category/create", headers=headers, - data={"name": name} # Use data format for form data + data={"name": name}, # Use data format for form data ) - + if response.status_code in [200, 201]: dialog.destroy() CTkMessagebox( - title="Success", - message="Category updated successfully" if category else "Category added successfully" + title="Success", + message="Category updated successfully" + if category + else "Category added successfully", ) fetch_categories() # Refresh the list else: @@ -525,18 +522,12 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): error_detail = error_data["detail"] except: pass - CTkMessagebox( - title="Error", - message=error_detail, - icon="cancel" - ) + CTkMessagebox(title="Error", message=error_detail, icon="cancel") except Exception as e: CTkMessagebox( - title="Error", - message=f"An error occurred: {str(e)}", - icon="cancel" + title="Error", message=f"An error occurred: {str(e)}", icon="cancel" ) - + save_button = ctk.CTkButton( button_frame, text="Save", @@ -544,27 +535,27 @@ def admin_category_management_frame(parent, switch_func, API_URL, access_token): width=100, fg_color="#2e8b57", hover_color="#1f6e42", - command=save_category + command=save_category, ) save_button.pack(side="right") - + # 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) - + # Initial data fetch def on_frame_shown(): fetch_categories() - + frame.after(100, on_frame_shown) - + # Add refresh method to frame for external calls def refresh_data(*args): """Public method to refresh category data""" fetch_categories() frame.refresh_data = refresh_data - - return frame \ No newline at end of file + + return frame diff --git a/app/frontend/components/admin/dashboard.py b/app/frontend/components/admin/dashboard.py index 4ed2eb939e7ba114bac63fe90004d938ca5c8270..3a25bcfacc3de04fd8a5abf3e801ee23e5ff22d6 100644 --- a/app/frontend/components/admin/dashboard.py +++ b/app/frontend/components/admin/dashboard.py @@ -4,35 +4,40 @@ import requests from PIL import Image, ImageTk import os + def admin_dashboard_frame(parent, switch_func, API_URL, access_token): """ Main admin dashboard with buttons to access different admin functions """ 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 with gradient effect header_frame = ctk.CTkFrame(main_container, corner_radius=15, fg_color="#1f538d") header_frame.pack(pady=(0, 20), fill="x") - + # Header content header_content = ctk.CTkFrame(header_frame, fg_color="transparent") header_content.pack(padx=20, pady=20) - + # Try to load an admin icon if available - icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), - "static", "icons", "admin.png") - + icon_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "static", + "icons", + "admin.png", + ) + try: if os.path.exists(icon_path): admin_icon = ctk.CTkImage(Image.open(icon_path), size=(64, 64)) @@ -40,22 +45,22 @@ def admin_dashboard_frame(parent, switch_func, API_URL, access_token): icon_label.pack(pady=(0, 10)) except Exception as e: print(f"Could not load admin icon: {e}") - + # Header text ctk.CTkLabel( - header_content, - text="Admin Dashboard", + header_content, + text="Admin Dashboard", font=("Helvetica", 28, "bold"), - text_color="#ffffff" + text_color="#ffffff", ).pack(pady=(0, 5)) - + ctk.CTkLabel( - header_content, - text="Manage your application from this control panel", + header_content, + text="Manage your application from this control panel", font=("Helvetica", 14), - text_color="#e0e0e0" + text_color="#e0e0e0", ).pack() - + # Welcome message with admin name def get_admin_name(): headers = {"Authorization": f"Bearer {access_token}"} @@ -67,27 +72,29 @@ def admin_dashboard_frame(parent, switch_func, API_URL, access_token): except Exception: pass return "Admin" - - welcome_frame = ctk.CTkFrame(main_container, corner_radius=10, fg_color="#f0f9ff", height=60) + + welcome_frame = ctk.CTkFrame( + main_container, corner_radius=10, fg_color="#f0f9ff", height=60 + ) welcome_frame.pack(fill="x", pady=(0, 20)) - + ctk.CTkLabel( welcome_frame, text=f"Welcome, {get_admin_name()}!", font=("Helvetica", 16), - text_color="#1a1a1a" + text_color="#1a1a1a", ).pack(padx=20, pady=15) - + # Dashboard cards section cards_frame = ctk.CTkFrame(main_container, fg_color="transparent") cards_frame.pack(fill="both", expand=True) - + # Configure grid for cards cards_frame.grid_columnconfigure(0, weight=1) cards_frame.grid_columnconfigure(1, weight=1) cards_frame.grid_rowconfigure(0, weight=1) cards_frame.grid_rowconfigure(1, weight=1) - + # Check admin access def check_admin(): headers = {"Authorization": f"Bearer {access_token}"} @@ -97,127 +104,142 @@ def admin_dashboard_frame(parent, switch_func, API_URL, access_token): role_data = resp.json() if role_data.get("role") != "admin": CTkMessagebox( - title="Access Denied", + title="Access Denied", message="You need admin privileges to access this dashboard", - icon="cancel" + icon="cancel", ) switch_func("dashboard") return False else: CTkMessagebox( - title="Error", + title="Error", message="Failed to verify admin privileges", - icon="cancel" + icon="cancel", ) switch_func("login") return False except Exception as e: CTkMessagebox( - title="Error", + title="Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) return False return True - + # Create dashboard cards def create_card(row, col, title, description, icon_text, command, color="#3a7ebf"): - card = ctk.CTkFrame(cards_frame, corner_radius=12, fg_color=color, border_width=1, border_color="#2a5a8f") + card = ctk.CTkFrame( + cards_frame, + corner_radius=12, + fg_color=color, + border_width=1, + border_color="#2a5a8f", + ) card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") - + # Create icon circle - icon_frame = ctk.CTkFrame(card, width=50, height=50, corner_radius=25, fg_color="#ffffff") + icon_frame = ctk.CTkFrame( + card, width=50, height=50, corner_radius=25, fg_color="#ffffff" + ) icon_frame.place(relx=0.1, rely=0.5, anchor="w") - + # Icon text (could be replaced with an actual icon) ctk.CTkLabel( - icon_frame, - text=icon_text, - font=("Helvetica", 18, "bold"), - text_color=color + icon_frame, text=icon_text, font=("Helvetica", 18, "bold"), text_color=color ).place(relx=0.5, rely=0.5, anchor="center") - + # Card content content_frame = ctk.CTkFrame(card, fg_color="transparent") content_frame.place(relx=0.6, rely=0.5, anchor="center") - + ctk.CTkLabel( - content_frame, - text=title, - font=("Helvetica", 18, "bold"), - text_color="#ffffff" + content_frame, + text=title, + font=("Helvetica", 18, "bold"), + text_color="#ffffff", ).pack(pady=(0, 5)) - + ctk.CTkLabel( - content_frame, - text=description, - font=("Helvetica", 12), - text_color="#e6e6e6" + content_frame, + text=description, + font=("Helvetica", 12), + text_color="#e6e6e6", ).pack() - + # Make entire card clickable card.bind("<Button-1>", lambda e: command() if check_admin() else None) for widget in card.winfo_children(): widget.bind("<Button-1>", lambda e: command() if check_admin() else None) for subwidget in widget.winfo_children(): - subwidget.bind("<Button-1>", lambda e: command() if check_admin() else None) - + subwidget.bind( + "<Button-1>", lambda e: command() if check_admin() else None + ) + # Create the cards create_card( - 0, 0, - "User Management", - "View and manage user accounts", - "👤", + 0, + 0, + "User Management", + "View and manage user accounts", + "👤", lambda: switch_func("admin_user_management"), - "#3a7ebf" + "#3a7ebf", ) - + create_card( - 0, 1, - "Shop Owner Management", - "Manage shop owner accounts", - "🏪", + 0, + 1, + "Shop Owner Management", + "Manage shop owner accounts", + "🏪", lambda: switch_func("admin_shop_owner_management"), - "#2e8b57" + "#2e8b57", ) - + create_card( - 1, 0, - "Category Management", - "Manage product categories", - "🏷️", + 1, + 0, + "Category Management", + "Manage product categories", + "🏷️", lambda: switch_func("category"), - "#9c27b0" + "#9c27b0", ) - + create_card( - 1, 1, - "System Statistics", - "View system analytics and data", - "��", - lambda: CTkMessagebox(title="Coming Soon", message="System statistics will be available in a future update.", icon="info"), - "#e91e63" + 1, + 1, + "System Statistics", + "View system analytics and data", + "��", + lambda: CTkMessagebox( + title="Coming Soon", + message="System statistics will be available in a future update.", + icon="info", + ), + "#e91e63", ) - + # Footer with logout button footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) footer_frame.pack(fill="x", pady=(20, 0)) - + # Logout button logout_btn = ctk.CTkButton( - footer_frame, - text="Logout", + footer_frame, + text="Logout", command=lambda: switch_func("login"), fg_color="#f44336", hover_color="#d32f2f", font=("Helvetica", 14, "bold"), corner_radius=8, border_width=0, - height=40 + height=40, ) logout_btn.pack(side="right") - + # Check admin access on load frame.after(100, check_admin) - - return frame \ No newline at end of file + + return frame diff --git a/app/frontend/components/admin/product_management.py b/app/frontend/components/admin/product_management.py index f093acfa0572cf0060459446cd8e88617273981e..cc9ba172dbd84487ed7a0d8f5863baaaa29fd717 100644 --- a/app/frontend/components/admin/product_management.py +++ b/app/frontend/components/admin/product_management.py @@ -8,181 +8,197 @@ 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", + title_frame, + text="Product Management", font=("Helvetica", 24, "bold"), - text_color="#ffffff" + text_color="#ffffff", ).pack(anchor="w") - + ctk.CTkLabel( - title_frame, - text="Manage marketplace products", + title_frame, + text="Manage marketplace products", font=("Helvetica", 14), - text_color="#e0e0e0" + 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...") + + 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", + search_frame, + text="Search", font=("Helvetica", 12, "bold"), height=35, corner_radius=8, fg_color="#2e8b57", - hover_color="#1f6e42" + 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) + 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"], + values=[ + "Newest First", + "Oldest First", + "Price: High to Low", + "Price: Low to High", + ], font=("Helvetica", 12), width=150, height=35, - dropdown_font=("Helvetica", 12) + 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", + action_buttons, + text="Approve", font=("Helvetica", 12, "bold"), height=35, width=100, corner_radius=8, fg_color="#4caf50", - hover_color="#388e3c" + hover_color="#388e3c", ) approve_button.pack(side="left", padx=(0, 10)) - + reject_button = ctk.CTkButton( - action_buttons, - text="Reject", + action_buttons, + text="Reject", font=("Helvetica", 12, "bold"), height=35, width=100, corner_radius=8, fg_color="#f44336", - hover_color="#d32f2f" + hover_color="#d32f2f", ) reject_button.pack(side="left", padx=(0, 10)) - + view_button = ctk.CTkButton( - action_buttons, - text="View Details", + action_buttons, + text="View Details", font=("Helvetica", 12, "bold"), height=35, width=120, corner_radius=8, fg_color="#3a7ebf", - hover_color="#2a6da9" + 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", + "Treeview", background="#2b2b2b", foreground="#ffffff", fieldbackground="#2b2b2b", borderwidth=0, - rowheight=40 + rowheight=40, ) - + # Configure the headings style.configure( - "Treeview.Heading", - background="#2e8b57", + "Treeview.Heading", + background="#2e8b57", foreground="#ffffff", borderwidth=0, - font=("Helvetica", 12, "bold") + 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") + columns = ( + "id", + "name", + "category", + "price", + "owner", + "status", + "created_at", + "image", + ) product_tree = ttk.Treeview( - table_frame, - columns=columns, - show="headings", - yscrollcommand=scrollbar.set + 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") @@ -192,7 +208,7 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): 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) @@ -202,40 +218,40 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): 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("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", + 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" + 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": @@ -251,50 +267,55 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): params["sort"] = "price_asc" if search_term: params["search"] = search_term - + try: - response = requests.get(f"{API_URL}/admin/products", headers=headers, params=params) - + 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")) + 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", + "", + "end", values=( product["id"], product.get("name", "Unnamed Product"), @@ -303,11 +324,11 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): owner, status.capitalize(), created_at, - img_text + img_text, ), - tags=(status,) + tags=(status,), ) - + # Try to load the image if product.get("images") and len(product["images"]) > 0: try: @@ -319,79 +340,101 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): 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 - )) - + 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") + 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") - + 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") + 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 + f"{API_URL}/admin/products/{product_id}/approve", headers=headers ) - + if response.status_code == 200: - CTkMessagebox(title="Success", message="Product approved successfully", icon="check") + 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") + 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") - + 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") + 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") @@ -399,98 +442,129 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): reject_dialog.resizable(False, False) reject_dialog.transient(frame) reject_dialog.grab_set() - + # Dialog content ctk.CTkLabel( - reject_dialog, + reject_dialog, text=f"Reject Product: {product_name}", - font=("Helvetica", 18, "bold") + font=("Helvetica", 18, "bold"), ).pack(pady=(20, 10)) - + ctk.CTkLabel( - reject_dialog, + reject_dialog, text="Please provide a reason for rejection:", - font=("Helvetica", 14) + 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) + 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 + json=data, ) - + if response.status_code == 200: reject_dialog.destroy() - CTkMessagebox(title="Success", message="Product rejected successfully", icon="check") + 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) + 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) - + CTkMessagebox( + title="Error", + message=f"An error occurred: {str(e)}", + icon="cancel", + parent=reject_dialog, + ) + ctk.CTkButton( - button_frame, - text="Cancel", + button_frame, + text="Cancel", command=on_cancel, font=("Helvetica", 12, "bold"), fg_color="#9e9e9e", hover_color="#757575", width=120, - height=35 + height=35, ).pack(side="left", padx=10) - + ctk.CTkButton( - button_frame, - text="Reject Product", + button_frame, + text="Reject Product", command=on_submit, font=("Helvetica", 12, "bold"), fg_color="#f44336", hover_color="#d32f2f", width=150, - height=35 + 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") + 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) - + 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") + 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") - + 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) @@ -499,27 +573,27 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): 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, + main_scroll, text=product.get("name", "Unnamed Product"), - font=("Helvetica", 24, "bold") + 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: @@ -529,139 +603,142 @@ def admin_product_management_frame(parent, switch_func, API_URL, access_token): 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") + 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") + 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") - + 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, + info_grid, text=f"{label}:", font=("Helvetica", 14, "bold"), width=150, - anchor="e" + 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" + 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")) + 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") + 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", + buttons_frame, + text="Close", command=detail_dialog.destroy, height=35, - width=100 + 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", + buttons_frame, + text="Reject", command=lambda: detail_dialog.destroy() or reject_product(), fg_color="#f44336", hover_color="#d32f2f", height=35, - width=100 + width=100, ) reject_button.pack(side="right", padx=(10, 0)) - + approve_button = ctk.CTkButton( - buttons_frame, - text="Approve", + buttons_frame, + text="Approve", command=lambda: detail_dialog.destroy() or approve_product(), fg_color="#4caf50", hover_color="#388e3c", height=35, - width=100 + 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 \ No newline at end of file + + return frame diff --git a/app/frontend/components/admin/shop_owner_management.py b/app/frontend/components/admin/shop_owner_management.py index 76f6a5216d95e9ac1b406b088503c2a3a16e500e..4c872a016e2fbf85743be82699c337fc6c6dcf2e 100644 --- a/app/frontend/components/admin/shop_owner_management.py +++ b/app/frontend/components/admin/shop_owner_management.py @@ -4,207 +4,212 @@ from CTkMessagebox import CTkMessagebox import requests from datetime import datetime + def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token): """ Admin dashboard component for managing shop owners """ 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="Shop Owner Management", + title_frame, + text="Shop Owner Management", font=("Helvetica", 24, "bold"), - text_color="#ffffff" + text_color="#ffffff", ).pack(anchor="w") - + ctk.CTkLabel( - title_frame, - text="Manage shop owner accounts and their shops", + title_frame, + text="Manage shop owner accounts and their shops", font=("Helvetica", 14), - text_color="#e0e0e0" + text_color="#e0e0e0", ).pack(anchor="w") - + # Search and actions panel control_panel = ctk.CTkFrame(main_container, corner_radius=10, height=80) control_panel.pack(fill="x", pady=(0, 15)) - + # Left side - search search_frame = ctk.CTkFrame(control_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 by username or email") + + 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 by username or email", + ) search_entry.pack(side="left", padx=(0, 10)) - + search_button = ctk.CTkButton( - search_frame, - text="Search", + search_frame, + text="Search", font=("Helvetica", 12, "bold"), height=35, corner_radius=8, fg_color="#2e8b57", - hover_color="#1f6e42" + hover_color="#1f6e42", ) search_button.pack(side="left") - + # Right side - actions 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", + actions_frame, + text="View Shops", font=("Helvetica", 12, "bold"), height=35, width=120, corner_radius=8, fg_color="#3a7ebf", - hover_color="#2a6da9" + hover_color="#2a6da9", ) view_shops_button.pack(side="left", padx=(0, 10)) - + refresh_button = ctk.CTkButton( - actions_frame, - text="Refresh", + actions_frame, + text="Refresh", font=("Helvetica", 12, "bold"), height=35, width=100, corner_radius=8, fg_color="#4caf50", - hover_color="#388e3c" + hover_color="#388e3c", ) refresh_button.pack(side="left", padx=(0, 10)) - + delete_button = ctk.CTkButton( - actions_frame, - text="Delete Owner", + actions_frame, + text="Delete Owner", font=("Helvetica", 12, "bold"), height=35, width=120, corner_radius=8, fg_color="#f44336", - hover_color="#d32f2f" + hover_color="#d32f2f", ) delete_button.pack(side="left") - + # Data table frame 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", + "Treeview", background="#2b2b2b", foreground="#ffffff", fieldbackground="#2b2b2b", borderwidth=0, - rowheight=35 + rowheight=35, ) - + # Configure the headings style.configure( - "Treeview.Heading", - background="#2e8b57", + "Treeview.Heading", + background="#2e8b57", foreground="#ffffff", borderwidth=0, - font=("Helvetica", 12, "bold") + 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 with styled tags columns = ("id", "username", "email", "phone", "role") owner_tree = ttk.Treeview( - table_frame, - columns=columns, - show="headings", - yscrollcommand=scrollbar.set + table_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set ) - + # Configure scrollbar scrollbar.config(command=owner_tree.yview) - + # 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") - + # 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 tag configurations owner_tree.tag_configure("owner", background="#252525") owner_tree.tag_configure("highlight", background="#1a4731") owner_tree.tag_configure("match", background="#1f3d5a") - + owner_tree.pack(fill="both", expand=True, padx=5, pady=5) - + # 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", + 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" + hover_color="#444444", ) back_button.pack(side="left") - + # 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}"} - + try: response = requests.get(f"{API_URL}/admin/owners", headers=headers) - + if response.status_code == 200: # Clear existing items for item in owner_tree.get_children(): owner_tree.delete(item) - + shop_owners = response.json() - + # Filter users by role (only shop owners) row_count = 0 for owner in shop_owners: @@ -212,44 +217,49 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token created_at = "N/A" if "created_at" in owner: try: - dt = datetime.fromisoformat(owner["created_at"].replace("Z", "+00:00")) + 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", + "", + "end", values=( owner["id"], owner.get("username", "Unknown"), owner.get("email", ""), owner.get("phone_number", ""), - owner.get("role", "shop_owner") + owner.get("role", "shop_owner"), ), - tags=(tag,) + tags=(tag,), ) row_count += 1 else: CTkMessagebox( title="Error", - message=response.json().get("detail", "Failed to fetch shop owners"), - icon="cancel" + 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" + icon="cancel", ) - + def delete_shop_owner(): """Delete (ban) the selected shop owner""" selected_item = owner_tree.selection() @@ -257,49 +267,53 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token CTkMessagebox( title="Warning", message="Please select a shop owner to delete", - icon="warning" + icon="warning", ) return - + owner_id = owner_tree.item(selected_item[0])["values"][0] username = owner_tree.item(selected_item[0])["values"][1] - + # 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!", icon="question", option_1="Cancel", - option_2="Delete" + option_2="Delete", ) - + if confirm.get() != "Delete": return - + try: headers = {"Authorization": f"Bearer {access_token}"} - response = requests.delete(f"{API_URL}/admin/owners/{owner_id}", headers=headers) - + response = requests.delete( + f"{API_URL}/admin/owners/{owner_id}", headers=headers + ) + if response.status_code == 200: CTkMessagebox( title="Success", message="Shop owner deleted successfully", - icon="check" + icon="check", ) fetch_shop_owners() # Refresh the list else: CTkMessagebox( title="Error", - message=response.json().get("detail", "Failed to delete shop owner"), - icon="cancel" + message=response.json().get( + "detail", "Failed to delete shop owner" + ), + icon="cancel", ) except Exception as e: CTkMessagebox( title="Connection Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) - + def view_shops(): """View shops owned by the selected shop owner""" selected_item = owner_tree.selection() @@ -307,68 +321,66 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token CTkMessagebox( title="Warning", message="Please select a shop owner first", - icon="warning" + icon="warning", ) return - + # 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_tree.item(selected_item[0])["values"][0] + username = owner_tree.item(selected_item[0])["values"][1] + # Create a popup to display shops show_shops_dialog(owner_id, username) - + def show_shops_dialog(owner_id, username): # Fetch shops for this owner shops = fetch_owner_shops(owner_id) - + if not shops: CTkMessagebox( - title="Shops", - message=f"Owner '{username}' has no shops.", - icon="info" + title="Shops", message=f"Owner '{username}' has no shops.", icon="info" ) return - + # Create popup dialog dialog = ctk.CTkToplevel() dialog.title(f"Shops owned by {username}") dialog.geometry("600x400") dialog.transient(frame) # Make dialog modal dialog.grab_set() - + # Create a header header_frame = ctk.CTkFrame(dialog, fg_color="#2e8b57", height=50) header_frame.pack(fill="x", padx=10, pady=10) - + ctk.CTkLabel( - header_frame, - text=f"Shops owned by {username}", + header_frame, + text=f"Shops owned by {username}", font=("Helvetica", 16, "bold"), - text_color="white" + text_color="white", ).pack(padx=20, pady=10) - + # Create scrollable frame for shops shops_frame = ctk.CTkScrollableFrame(dialog) shops_frame.pack(fill="both", expand=True, padx=10, pady=10) - + # Add each shop as a card for shop in shops: shop_card = ctk.CTkFrame(shops_frame, corner_radius=10, fg_color="#333333") shop_card.pack(fill="x", padx=5, pady=5) - + # Shop details details_frame = ctk.CTkFrame(shop_card, fg_color="transparent") details_frame.pack(fill="both", expand=True, padx=15, pady=15) - + # Shop name ctk.CTkLabel( details_frame, text=shop.get("name", "Unnamed Shop"), font=("Helvetica", 14, "bold"), - text_color="#2e8b57" + text_color="#2e8b57", ).pack(anchor="w") - + # Description description = shop.get("description", "No description") ctk.CTkLabel( @@ -376,34 +388,36 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token text=f"Description: {description}", font=("Helvetica", 12), text_color="white", - wraplength=550 + wraplength=550, ).pack(anchor="w", pady=(5, 0)) - + # Address address = shop.get("address", "No address") ctk.CTkLabel( details_frame, text=f"Address: {address}", font=("Helvetica", 12), - text_color="white" + text_color="white", ).pack(anchor="w", pady=(5, 0)) - + # Created date created_at = "N/A" if "created_at" in shop: try: - dt = datetime.fromisoformat(shop["created_at"].replace("Z", "+00:00")) + dt = datetime.fromisoformat( + shop["created_at"].replace("Z", "+00:00") + ) created_at = dt.strftime("%Y-%m-%d %H:%M") except: pass - + ctk.CTkLabel( details_frame, text=f"Created: {created_at}", font=("Helvetica", 12), - text_color="#999999" + text_color="#999999", ).pack(anchor="w", pady=(5, 0)) - + # Close button close_button = ctk.CTkButton( dialog, @@ -413,66 +427,70 @@ def admin_shop_owner_management_frame(parent, switch_func, API_URL, access_token height=40, corner_radius=8, fg_color="#555555", - hover_color="#444444" + hover_color="#444444", ) close_button.pack(pady=(0, 10)) - + def fetch_owner_shops(owner_id): try: headers = {"Authorization": f"Bearer {access_token}"} - response = requests.get(f"{API_URL}/admin/owners/{owner_id}/shops", headers=headers) - + response = requests.get( + f"{API_URL}/admin/owners/{owner_id}/shops", headers=headers + ) + if response.status_code == 200: return response.json() else: CTkMessagebox( title="Error", message=response.json().get("detail", "Failed to fetch shops"), - icon="cancel" + icon="cancel", ) return [] except Exception as e: CTkMessagebox( title="Connection Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) 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()): + 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) - + # Fetch shop owners when the frame is shown def on_frame_shown(): fetch_shop_owners() - + frame.after(100, on_frame_shown) - + # Add refresh method to frame for external calls def refresh_data(*args): """Public method to refresh shop owner data""" fetch_shop_owners() - + frame.refresh_data = refresh_data - - return frame \ No newline at end of file + + return frame diff --git a/app/frontend/components/admin/user_management.py b/app/frontend/components/admin/user_management.py index 9e4c05b14f177883d94889996aa1a4c00073eb23..8d9a3f425e103ebb47e4dcc5a9a5c30cf777bc11 100644 --- a/app/frontend/components/admin/user_management.py +++ b/app/frontend/components/admin/user_management.py @@ -4,194 +4,199 @@ from CTkMessagebox import CTkMessagebox import requests from datetime import datetime + def admin_user_management_frame(parent, switch_func, API_URL, access_token): """ Admin dashboard component for managing regular users """ 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="#1f538d") 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="User Management", + title_frame, + text="User Management", font=("Helvetica", 24, "bold"), - text_color="#ffffff" + text_color="#ffffff", ).pack(anchor="w") - + ctk.CTkLabel( - title_frame, - text="View and manage user accounts", + title_frame, + text="View and manage user accounts", font=("Helvetica", 14), - text_color="#e0e0e0" + text_color="#e0e0e0", ).pack(anchor="w") - + # Search and actions panel control_panel = ctk.CTkFrame(main_container, corner_radius=10, height=80) control_panel.pack(fill="x", pady=(0, 15)) - + # Left side - search search_frame = ctk.CTkFrame(control_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 by username or email") + + 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 by username or email", + ) search_entry.pack(side="left", padx=(0, 10)) - + search_button = ctk.CTkButton( - search_frame, - text="Search", + search_frame, + text="Search", font=("Helvetica", 12, "bold"), height=35, corner_radius=8, fg_color="#3a7ebf", - hover_color="#2a6da9" + hover_color="#2a6da9", ) search_button.pack(side="left") - + # Right side - actions actions_frame = ctk.CTkFrame(control_panel, fg_color="transparent") actions_frame.pack(side="right", padx=20, pady=15, fill="y") - + refresh_button = ctk.CTkButton( - actions_frame, - text="Refresh", + actions_frame, + text="Refresh", font=("Helvetica", 12, "bold"), height=35, width=100, corner_radius=8, fg_color="#4caf50", - hover_color="#388e3c" + hover_color="#388e3c", ) refresh_button.pack(side="left", padx=(0, 10)) - + delete_button = ctk.CTkButton( - actions_frame, - text="Delete User", + actions_frame, + text="Delete User", font=("Helvetica", 12, "bold"), height=35, width=120, corner_radius=8, fg_color="#f44336", - hover_color="#d32f2f" + hover_color="#d32f2f", ) delete_button.pack(side="left") - + # Data table frame 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", + "Treeview", background="#2b2b2b", foreground="#ffffff", fieldbackground="#2b2b2b", borderwidth=0, - rowheight=35 + rowheight=35, ) - + # Configure the headings style.configure( - "Treeview.Heading", - background="#1f538d", + "Treeview.Heading", + background="#1f538d", foreground="#ffffff", borderwidth=0, - font=("Helvetica", 12, "bold") + font=("Helvetica", 12, "bold"), ) - + # Selection color style.map("Treeview", background=[("selected", "#3a7ebf")]) - + # Create scrollbar scrollbar = ttk.Scrollbar(table_frame) scrollbar.pack(side="right", fill="y") - + # Create Treeview with styled tags columns = ("id", "username", "email", "phone", "role") user_tree = ttk.Treeview( - table_frame, - columns=columns, - show="headings", - yscrollcommand=scrollbar.set + table_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set ) - + # Configure scrollbar scrollbar.config(command=user_tree.yview) - + # 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") - + # 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 tag configurations user_tree.tag_configure("user", background="#252525") user_tree.tag_configure("match", background="#1f3d5a") - + user_tree.pack(fill="both", expand=True, padx=5, pady=5) - + # 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", + 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" + hover_color="#444444", ) back_button.pack(side="left") - + # 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 for item in user_tree.get_children(): user_tree.delete(item) - + users = response.json() - + # Filter users by role (only regular users, not admins) row_count = 0 for user in users: @@ -199,30 +204,31 @@ def admin_user_management_frame(parent, switch_func, API_URL, access_token): # Add row with alternating tags for zebra striping tag = "user" if row_count % 2 == 0 else "" user_tree.insert( - "", "end", + "", + "end", values=( user["id"], user["username"], user["email"], user.get("phone_number", "N/A"), - user["role"] + user["role"], ), - tags=(tag,) + tags=(tag,), ) row_count += 1 else: CTkMessagebox( - title="Error", + title="Error", message=response.json().get("detail", "Failed to fetch users"), - icon="cancel" + icon="cancel", ) except requests.exceptions.RequestException as e: CTkMessagebox( - title="Connection Error", + title="Connection Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) - + def delete_user(): """Delete (ban) the selected user""" selected_item = user_tree.selection() @@ -230,84 +236,86 @@ def admin_user_management_frame(parent, switch_func, API_URL, access_token): CTkMessagebox( title="Warning", message="Please select a user to delete", - icon="warning" + icon="warning", ) return - + user_id = user_tree.item(selected_item[0])["values"][0] username = user_tree.item(selected_item[0])["values"][1] - + # Confirm deletion confirm = CTkMessagebox( title="Confirm Deletion", message=f"Are you sure you want to delete user '{username}'?", icon="question", - option_1="Cancel", - option_2="Delete" + option_1="Cancel", + option_2="Delete", ) - + if confirm.get() != "Delete": return - + # Send delete request headers = {"Authorization": f"Bearer {access_token}"} - + try: - response = requests.delete(f"{API_URL}/admin/users/{user_id}", headers=headers) - + response = requests.delete( + f"{API_URL}/admin/users/{user_id}", headers=headers + ) + if response.status_code == 200: CTkMessagebox( - title="Success", - message="User deleted successfully", - icon="check" + title="Success", message="User deleted successfully", icon="check" ) fetch_users() # Refresh the list else: CTkMessagebox( title="Error", message=response.json().get("detail", "Failed to delete user"), - icon="cancel" + icon="cancel", ) except requests.exceptions.RequestException as e: CTkMessagebox( title="Connection Error", message=f"Failed to connect to server: {e}", - icon="cancel" + icon="cancel", ) - + def search_users(): """Search users based on the search term""" search_term = search_entry.get().lower() if not search_term: 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()): + if ( + search_term in str(values[1]).lower() + or search_term in str(values[2]).lower() + ): user_tree.item(item, tags=("match",)) else: user_tree.detach(item) # Hide non-matching items - + # Connect functions to buttons search_button.configure(command=search_users) refresh_button.configure(command=fetch_users) delete_button.configure(command=delete_user) - + # Fetch users when the frame is shown def on_frame_shown(): fetch_users() - + frame.after(100, on_frame_shown) - + # Add refresh method to frame for external calls def refresh_data(*args): """Public method to refresh user data""" fetch_users() - + frame.refresh_data = refresh_data - - return frame \ No newline at end of file + + return frame diff --git a/app/frontend/components/owner/owner_dashboard.py b/app/frontend/components/owner/owner_dashboard.py index c806b86a4130ef1fdc3daad56df354fe3fe5e12f..341019a831f9f242bf189252cfa2d4ec491990ec 100644 --- a/app/frontend/components/owner/owner_dashboard.py +++ b/app/frontend/components/owner/owner_dashboard.py @@ -386,20 +386,20 @@ def owner_dashboard_frame(parent, switch_func, API_URL, token): text_color="white", ) name_label.place(relx=0.5, rely=0.5, anchor="center") - + # Category cell category_cell = ctk.CTkFrame(row, fg_color="transparent") category_cell.place( relx=sum(header_widths[:3]), rely=0, relwidth=header_widths[3], relheight=1 ) - + category_name = "Uncategorized" try: # Debug the category data print(f"DEBUG - Processing category for product {product.get('name')}:") print(f" Category ID: {product.get('category_id')}") print(f" Category object: {product.get('category')}") - + if product.get("category") is not None and product["category"].get("name"): category_name = product["category"]["name"] print(f" Using category name: {category_name}") @@ -407,7 +407,7 @@ def owner_dashboard_frame(parent, switch_func, API_URL, token): print(" Using default: Uncategorized") except Exception as e: print(f"Error processing category: {e}") - + category_label = ctk.CTkLabel( category_cell, text=category_name, @@ -537,20 +537,28 @@ def owner_dashboard_frame(parent, switch_func, API_URL, token): ) if products_resp.status_code == 200: products = products_resp.json() - + # Debug output to see product data print("DEBUG: Products response received:") try: # Print the raw response - print(f"Raw response: {products_resp.text[:200]}...") # First 200 chars only - + print( + f"Raw response: {products_resp.text[:200]}..." + ) # First 200 chars only + # Print product details for first few products - for i, p in enumerate(products[:2]): # Just print the first 2 to keep it manageable - print(f"Product {i+1}: {p.get('name')}") + for i, p in enumerate( + products[:2] + ): # Just print the first 2 to keep it manageable + print(f"Product {i + 1}: {p.get('name')}") print(f" Category ID: {p.get('category_id')}") - print(f" Has Category Object: {p.get('category') is not None}") - if p.get('category'): - print(f" Category Name: {p.get('category').get('name')}") + print( + f" Has Category Object: {p.get('category') is not None}" + ) + if p.get("category"): + print( + f" Category Name: {p.get('category').get('name')}" + ) else: print(" No category object") print(" Full Product Data:") diff --git a/app/frontend/components/owner/owner_orders.py b/app/frontend/components/owner/owner_orders.py index a027d4b0bc02dbaf0eecf254bb1fc74193f86fa8..9314034d7ec22293f90b460e80d1ef0a09eb3174 100644 --- a/app/frontend/components/owner/owner_orders.py +++ b/app/frontend/components/owner/owner_orders.py @@ -279,15 +279,17 @@ def owner_orders_frame(parent, switch_func, API_URL, token): for i, item in enumerate(items[:2]): product_name = item.get("product", {}).get("name", "Unknown Product") quantity = item.get("quantity", 0) - + # Get category name category_name = "Uncategorized" - if item.get("product", {}).get("category", {}) and item["product"]["category"].get("name"): + if item.get("product", {}).get("category", {}) and item["product"][ + "category" + ].get("name"): category_name = item["product"]["category"]["name"] - + product_frame = ctk.CTkFrame(products_list, fg_color="transparent") product_frame.pack(anchor="w", fill="x") - + item_label = ctk.CTkLabel( product_frame, text=f"{product_name} (x{quantity})", @@ -295,12 +297,12 @@ def owner_orders_frame(parent, switch_func, API_URL, token): text_color="#ccc", ) item_label.pack(anchor="w", side="left") - + category_label = ctk.CTkLabel( product_frame, text=f"[{category_name}]", font=("Helvetica", 10), - text_color="#A0A0A0", # Light gray for category + text_color="#A0A0A0", # Light gray for category ) category_label.pack(anchor="w", side="left", padx=(5, 0)) diff --git a/app/frontend/components/owner/owner_products.py b/app/frontend/components/owner/owner_products.py index a338623e9bce2a232610714e865912f750e57561..bf49d962a3ce3b5dd5db0de764b0fe7fe2e8150b 100644 --- a/app/frontend/components/owner/owner_products.py +++ b/app/frontend/components/owner/owner_products.py @@ -152,7 +152,15 @@ def owner_products_frame(parent, switch_func, API_URL, token): headers_frame.pack_propagate(False) # Create the headers - header_texts = ["Image", "Title", "Product Name", "Category", "Price", "Quantity", "Action"] + header_texts = [ + "Image", + "Title", + "Product Name", + "Category", + "Price", + "Quantity", + "Action", + ] header_widths = [ 0.15, 0.15, @@ -239,20 +247,20 @@ def owner_products_frame(parent, switch_func, API_URL, token): text_color="white", ) name_label.place(relx=0.5, rely=0.5, anchor="center") - + # Category cell category_cell = ctk.CTkFrame(row, fg_color="transparent") category_cell.place( relx=sum(header_widths[:3]), rely=0, relwidth=header_widths[3], relheight=1 ) - + category_name = "Uncategorized" try: # Debug the category data print(f"DEBUG - Processing category for product {product.get('name')}:") print(f" Category ID: {product.get('category_id')}") print(f" Category object: {product.get('category')}") - + if product.get("category") is not None and product["category"].get("name"): category_name = product["category"]["name"] print(f" Using category name: {category_name}") @@ -260,7 +268,7 @@ def owner_products_frame(parent, switch_func, API_URL, token): print(" Using default: Uncategorized") except Exception as e: print(f"Error processing category: {e}") - + category_label = ctk.CTkLabel( category_cell, text=category_name, @@ -470,14 +478,16 @@ def owner_products_frame(parent, switch_func, API_URL, token): ) if prod_resp.status_code == 200: products = prod_resp.json() - + # Debug output to see product data print("DEBUG: Products response received in owner_products:") - for p in products[:2]: # Just print the first 2 to keep it manageable + for p in products[ + :2 + ]: # Just print the first 2 to keep it manageable print(f"Product: {p.get('name')}") print(f"Category ID: {p.get('category_id')}") print(f"Category: {p.get('category')}") - if p.get('category'): + if p.get("category"): print(f"Category Name: {p.get('category').get('name')}") print("---") diff --git a/app/frontend/components/product/create_product.py b/app/frontend/components/product/create_product.py index e0b672958dbbd38660015f060b28f364896ae81b..68e8f1b3eec7945a0849f6f32907ec2332b98d56 100644 --- a/app/frontend/components/product/create_product.py +++ b/app/frontend/components/product/create_product.py @@ -122,21 +122,21 @@ def create_product_frame(parent, switch_func, API_URL, token): stock_entry = create_form_field( left_col, "Stock Quantity", "Enter available stock..." ) - + # Category dropdown category_frame = ctk.CTkFrame(left_col, fg_color="transparent") category_frame.pack(fill="x", pady=(0, 15)) - + category_label = ctk.CTkLabel( category_frame, text="Category", font=("Helvetica", 14), text_color="white" ) category_label.pack(anchor="w") - + # Dictionary to store category id -> name mapping categories_data = {} # List to store category names for the dropdown category_options = ["Loading categories..."] - + category_dropdown = ctk.CTkOptionMenu( category_frame, values=category_options, @@ -149,7 +149,7 @@ def create_product_frame(parent, switch_func, API_URL, token): text_color="white", ) category_dropdown.pack(fill="x", pady=(5, 0)) - + # Function to fetch categories from API def fetch_categories(): headers = {"Authorization": f"Bearer {token}"} @@ -157,26 +157,26 @@ def create_product_frame(parent, switch_func, API_URL, token): response = requests.get(f"{API_URL}/category", headers=headers) if response.status_code == 200: categories = response.json() - + # Clear and update the categories dictionary categories_data.clear() category_names = [] - + if categories: for cat in categories: categories_data[cat["name"]] = cat["id"] category_names.append(cat["name"]) else: category_names = ["No categories available"] - + # Update the dropdown category_dropdown.configure(values=category_names) if category_names: category_dropdown.set(category_names[0]) else: messagebox.showerror( - "Error", - f"Failed to fetch categories. Status: {response.status_code}" + "Error", + f"Failed to fetch categories. Status: {response.status_code}", ) except Exception as e: messagebox.showerror("Error", f"Failed to fetch categories: {e}") @@ -269,7 +269,7 @@ def create_product_frame(parent, switch_func, API_URL, token): desc_val = desc_entry.get("1.0", "end-1c").strip() stock_val = stock_entry.get().strip() category_name = category_dropdown.get() - + # Get the category ID from the selected name category_id = categories_data.get(category_name) @@ -390,7 +390,7 @@ def create_product_frame(parent, switch_func, API_URL, token): hover_color="#0096ff", ) submit_button.pack(side="right", fill="x", expand=True, padx=(5, 0)) - + # Fetch categories when frame is created frame.after(100, fetch_categories) diff --git a/app/frontend/components/product/edit_product.py b/app/frontend/components/product/edit_product.py index ea04ff35fd8a346ad38a84de24d56d61878a8974..c15a367bfc8e6c2add878643b77cf7db23976bfe 100644 --- a/app/frontend/components/product/edit_product.py +++ b/app/frontend/components/product/edit_product.py @@ -124,23 +124,23 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): stock_entry = create_form_field( left_col, "Stock Quantity", "Enter available stock..." ) - + # Category dropdown category_frame = ctk.CTkFrame(left_col, fg_color="transparent") category_frame.pack(fill="x", pady=(0, 15)) - + category_label = ctk.CTkLabel( category_frame, text="Category", font=("Helvetica", 14), text_color="white" ) category_label.pack(anchor="w") - + # Dictionary to store category id -> name mapping categories_data = {} # Dictionary to store category name -> id mapping categories_id_to_name = {} # List to store category names for the dropdown category_options = ["Loading categories..."] - + category_dropdown = ctk.CTkOptionMenu( category_frame, values=category_options, @@ -153,7 +153,7 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): text_color="white", ) category_dropdown.pack(fill="x", pady=(5, 0)) - + # Function to fetch categories from API def fetch_categories(): headers = {"Authorization": f"Bearer {token}"} @@ -161,12 +161,12 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): response = requests.get(f"{API_URL}/category", headers=headers) if response.status_code == 200: categories = response.json() - + # Clear and update the categories dictionaries categories_data.clear() categories_id_to_name.clear() category_names = [] - + if categories: for cat in categories: cat_id = cat["id"] @@ -176,10 +176,10 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): category_names.append(cat_name) else: category_names = ["No categories available"] - + # Update the dropdown category_dropdown.configure(values=category_names) - + # If we have a category_id from the product, select it if category_id and category_id in categories_id_to_name: category_name = categories_id_to_name[category_id] @@ -188,8 +188,8 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): category_dropdown.set(category_names[0]) else: messagebox.showerror( - "Error", - f"Failed to fetch categories. Status: {response.status_code}" + "Error", + f"Failed to fetch categories. Status: {response.status_code}", ) except Exception as e: messagebox.showerror("Error", f"Failed to fetch categories: {e}") @@ -295,7 +295,7 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): desc_val = desc_entry.get("1.0", "end-1c").strip() stock_val = stock_entry.get().strip() category_name = category_dropdown.get() - + # Get the category ID from the selected name selected_category_id = categories_data.get(category_name, None) @@ -342,10 +342,12 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): "stock": stock_val, "shop_id": shop_id, # Use the loaded shop_id } - + # Only add category_id to the data if it's a valid value if selected_category_id is not None: - data["category_id"] = str(selected_category_id) # Ensure it's a string for the form data + data["category_id"] = str( + selected_category_id + ) # Ensure it's a string for the form data # Prepare files for upload if a new image was selected files = {} @@ -371,12 +373,14 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): print(f" Name: {title_val}") print(f" Shop ID: {shop_id}") print(f" Category ID: {selected_category_id}") - + # Check if product_id is None if product_id is None: - messagebox.showerror("Error", "Product ID is missing. Cannot update product.") + messagebox.showerror( + "Error", "Product ID is missing. Cannot update product." + ) return - + # Use PUT for updating existing product resp = requests.put( f"{API_URL}/product/put/{product_id}", @@ -468,7 +472,7 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): # Store shop_id and category_id for submission shop_id = product_data.get("shop_id") category_id = product_data.get("category_id") - + # Fetch categories to populate the dropdown with the current category selected fetch_categories() @@ -527,7 +531,7 @@ def edit_product_frame(parent, switch_func, API_URL, token, product_id=None): nonlocal product_id print(f"DEBUG - refresh_data called with new_product_id: {new_product_id}") print(f"DEBUG - current product_id before refresh: {product_id}") - + if new_product_id is not None: product_id = new_product_id print(f"DEBUG - Updated product_id to: {product_id}") diff --git a/app/frontend/components/product/view_product.py b/app/frontend/components/product/view_product.py index f958a74e0eed5d4d5f064efd7d7335ab5eb4a775..d7bac1aba0107975bd186228885cbfb2d81af604 100644 --- a/app/frontend/components/product/view_product.py +++ b/app/frontend/components/product/view_product.py @@ -29,8 +29,8 @@ def fix_url(url): if not url.startswith("http"): # Strip app/static/ prefix if present if url.startswith("app/static/"): - url = url[len("app/static/"):] - + url = url[len("app/static/") :] + # Add the backend static prefix result = f"{BACKEND_HOST}/static/{url}" print(f"[DEBUG] Final URL after fixing: {result}") diff --git a/app/frontend/main.py b/app/frontend/main.py index 7c0eefc999d349ca933245d549459746197b7e67..709fbd1ea42109022fd7ec98f4d76895ad86e183 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -143,16 +143,15 @@ def switch_frame(frame_name, *args): # Redirect shop owners to the owner dashboard, but only if not explicitly navigating to shop print("Redirecting shop owner from dashboard to owner_dashboard") frame_name = "owner_dashboard" - elif ( - user_role == "shop_owner" - and frame_name == "view_product" - ): + elif user_role == "shop_owner" and frame_name == "view_product": # Always allow shop owners to view products print("Shop owner viewing product - allowing access") pass # Just proceed with view_product frame # Explicitly allow all users to access view_shop and view_product elif frame_name in ["view_shop", "view_product"]: - print(f"User with role {user_role} accessing {frame_name} - allowing access") + print( + f"User with role {user_role} accessing {frame_name} - allowing access" + ) pass # Allow access to view_shop and view_product for all roles elif user_role == "admin" and frame_name == "dashboard": # Redirect admins to the admin dashboard @@ -164,7 +163,11 @@ def switch_frame(frame_name, *args): f"Preventing non-owner (role: {user_role}) from accessing owner_dashboard" ) frame_name = "dashboard" - elif user_role != "admin" and frame_name in ["admin_dashboard", "admin_user_management", "admin_shop_owner_management"]: + elif user_role != "admin" and frame_name in [ + "admin_dashboard", + "admin_user_management", + "admin_shop_owner_management", + ]: # Prevent non-admins from accessing admin dashboards print( f"Preventing non-admin (role: {user_role}) from accessing admin pages" @@ -183,7 +186,15 @@ def switch_frame(frame_name, *args): print(f"Explicitly refreshing {frame_name}") frame.refresh_data(*args[1:] if len(args) > 1 else []) # Otherwise, refresh when switching to admin components to keep data current - elif frame_name in ["admin_dashboard", "admin_user_management", "admin_shop_owner_management", "category", "owner_dashboard", "owner_products", "owner_orders"]: + elif frame_name in [ + "admin_dashboard", + "admin_user_management", + "admin_shop_owner_management", + "category", + "owner_dashboard", + "owner_products", + "owner_orders", + ]: print(f"Auto-refreshing {frame_name}") frame.refresh_data() # Add special handling for view_shop and view_product @@ -199,14 +210,18 @@ def switch_frame(frame_name, *args): # If we have product data, pass it to refresh_data if len(args) > 0 and args[0] is not None: product_data = args[0] - print(f"*** DEBUG: Refreshing view_product with product data: {product_data}") + print( + f"*** DEBUG: Refreshing view_product with product data: {product_data}" + ) frame.refresh_data(product_data) elif frame_name == "edit_product": print(f"*** DEBUG: Switching to edit_product with args: {args}") # If we have a product_id, pass it to refresh_data if len(args) > 0 and args[0] is not None: product_id = args[0] - print(f"*** DEBUG: Refreshing edit_product with product_id: {product_id}") + print( + f"*** DEBUG: Refreshing edit_product with product_id: {product_id}" + ) frame.refresh_data(product_id) # Make sure we have a valid token for authenticated pages @@ -268,9 +283,15 @@ def initialize_authenticated_frames(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) # Admin frames - frames["admin_dashboard"] = admin_dashboard_frame(root, switch_frame, API_URL, token) - frames["admin_user_management"] = admin_user_management_frame(root, switch_frame, API_URL, token) - frames["admin_shop_owner_management"] = admin_shop_owner_management_frame(root, switch_frame, API_URL, token) + frames["admin_dashboard"] = admin_dashboard_frame( + root, switch_frame, API_URL, token + ) + frames["admin_user_management"] = admin_user_management_frame( + root, switch_frame, API_URL, token + ) + frames["admin_shop_owner_management"] = admin_shop_owner_management_frame( + root, switch_frame, API_URL, token + ) # Place all authenticated frames for key, frame in frames.items(): diff --git a/app/init.py b/app/init.py index 9624c1d3f04fb909314e3b6fc017f94f684091a4..3a3ba782a4c3ce80de2b592e45e347b11e9e2421 100644 --- a/app/init.py +++ b/app/init.py @@ -8,14 +8,16 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) # Import admin initialization from backend.scripts.admin_init import init_admin + def main(): """Main initialization function""" print("Running initialization scripts...") - + # Initialize admin user init_admin() - + print("Initialization complete!") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/run_app.py b/run_app.py index cf87b92348e63f9ec59c5a76e77d884b3921e1b6..e6c697f2d1706070b8f3f9589ce4c8c094618b13 100644 --- a/run_app.py +++ b/run_app.py @@ -18,7 +18,7 @@ if __name__ == "__main__": print("Initializing admin user...") init_admin() print("Admin initialization complete!") - + # Start FastAPI in a separate thread fastapi_thread = threading.Thread(target=run_fastapi, daemon=True) fastapi_thread.start()