From 23919fa4b04cc87250c7d3694e8ed514c1ae8357 Mon Sep 17 00:00:00 2001 From: nn2-minh <Nguyen12.Minh@live.uwe.ac.uk> Date: Sat, 15 Mar 2025 17:31:02 +0700 Subject: [PATCH] Saved changes before switching branches --- app/frontend/components/admin/category.py | 94 ++++++++++++++ .../components/{ => auth}/forgot_pass.py | 0 app/frontend/components/{ => auth}/login.py | 32 ++--- app/frontend/components/auth/register.py | 68 ++++++++++ .../components/product/create_product.py | 58 +++++++++ app/frontend/components/register.py | 90 ------------- app/frontend/components/shop/create_shop.py | 33 +++-- app/frontend/components/shop/view_shop.py | 119 ++++++++++++++++++ app/frontend/main.py | 47 ++++--- 9 files changed, 391 insertions(+), 150 deletions(-) create mode 100644 app/frontend/components/admin/category.py rename app/frontend/components/{ => auth}/forgot_pass.py (100%) rename app/frontend/components/{ => auth}/login.py (57%) create mode 100644 app/frontend/components/auth/register.py create mode 100644 app/frontend/components/product/create_product.py delete mode 100644 app/frontend/components/register.py create mode 100644 app/frontend/components/shop/view_shop.py diff --git a/app/frontend/components/admin/category.py b/app/frontend/components/admin/category.py new file mode 100644 index 0000000..c4d3225 --- /dev/null +++ b/app/frontend/components/admin/category.py @@ -0,0 +1,94 @@ +import customtkinter as ctk +import requests +from tkinter import messagebox + +def category_frame(parent, switch_func, API_URL, access_token): + frame = ctk.CTkFrame(parent) + + ctk.CTkLabel(frame, text="Category Management", font=("Helvetica", 18, "bold")).pack(pady=10) + + ctk.CTkLabel(frame, text="Category Name:").pack(pady=5) + entry_name = ctk.CTkEntry(frame) + entry_name.pack(pady=5) + + ctk.CTkLabel(frame, text="Category ID (for update/delete):").pack(pady=5) + entry_id = ctk.CTkEntry(frame) + entry_id.pack(pady=5) + + headers = {"Authorization": f"Bearer {access_token}"} if access_token else {} + + def create_category(): + name = entry_name.get().strip() + if not name: + messagebox.showwarning("Input Error", "Category name is required!") + return + + try: + response = requests.post(f"{API_URL}/category", data={"name": name}, headers=headers) + if response.status_code == 200: + messagebox.showinfo("Success", f"Category '{name}' created successfully!") + entry_name.delete(0, 'end') + else: + messagebox.showerror("Error", response.json().get("detail", "Failed to create category")) + except requests.exceptions.RequestException as e: + messagebox.showerror("Error", f"Failed to connect to server: {e}") + + def list_categories(): + try: + response = requests.get(f"{API_URL}/category", headers=headers) + if response.status_code == 200: + categories = response.json() + if categories: + category_list = "\n".join([f"{cat['id']}: {cat['name']}" for cat in categories]) + messagebox.showinfo("Categories", category_list) + else: + messagebox.showinfo("Categories", "No categories available.") + else: + messagebox.showerror("Error", "Failed to fetch categories") + except requests.exceptions.RequestException as e: + messagebox.showerror("Error", f"Failed to connect to server: {e}") + + def update_category(): + category_id = entry_id.get().strip() + name = entry_name.get().strip() + if not category_id.isdigit(): + messagebox.showwarning("Input Error", "Enter a valid Category ID!") + return + if not name: + messagebox.showwarning("Input Error", "Category name is required for update!") + return + + try: + response = requests.put(f"{API_URL}/category/{category_id}", data={"name": name}, headers=headers) + if response.status_code == 200: + messagebox.showinfo("Success", "Category updated successfully!") + entry_id.delete(0, 'end') + entry_name.delete(0, 'end') + else: + messagebox.showerror("Error", response.json().get("detail", "Failed to update category")) + except requests.exceptions.RequestException as e: + messagebox.showerror("Error", f"Failed to connect to server: {e}") + + def delete_category(): + category_id = entry_id.get().strip() + if not category_id.isdigit(): + messagebox.showwarning("Input Error", "Enter a valid Category ID!") + return + + try: + response = requests.delete(f"{API_URL}/category/{category_id}", headers=headers) + if response.status_code == 200: + messagebox.showinfo("Success", "Category deleted successfully!") + entry_id.delete(0, 'end') + else: + messagebox.showerror("Error", response.json().get("detail", "Failed to delete category")) + except requests.exceptions.RequestException as e: + messagebox.showerror("Error", f"Failed to connect to server: {e}") + + ctk.CTkButton(frame, text="Create Category", command=create_category).pack(pady=5) + ctk.CTkButton(frame, text="List Categories", command=list_categories).pack(pady=5) + ctk.CTkButton(frame, text="Update Category", command=update_category).pack(pady=5) + ctk.CTkButton(frame, text="Delete Category", command=delete_category).pack(pady=5) + ctk.CTkButton(frame, text="Back", command=lambda: switch_func("")).pack(pady=5) + + return frame diff --git a/app/frontend/components/forgot_pass.py b/app/frontend/components/auth/forgot_pass.py similarity index 100% rename from app/frontend/components/forgot_pass.py rename to app/frontend/components/auth/forgot_pass.py diff --git a/app/frontend/components/login.py b/app/frontend/components/auth/login.py similarity index 57% rename from app/frontend/components/login.py rename to app/frontend/components/auth/login.py index bedad7b..8f2b656 100644 --- a/app/frontend/components/login.py +++ b/app/frontend/components/auth/login.py @@ -1,17 +1,11 @@ -import ttkbootstrap as tb -import ttkbootstrap.constants +import customtkinter as ctk from tkinter import messagebox import requests -# Global variable to store the access token -access_token = None - - def login_frame(parent, switch_func, api_url): - frame = tb.Frame(parent) + frame = ctk.CTkFrame(parent) def login(): - global access_token email = entry_email.get() password = entry_password.get() @@ -27,34 +21,28 @@ def login_frame(parent, switch_func, api_url): response_data = response.json() if response.status_code == 200: access_token = response_data["access_token"] - print(f"Access Token: {access_token}") # Debugging line - messagebox.showinfo("Login Successful", f"Welcome back, {email}!") switch_func("create_shop", access_token) else: - messagebox.showerror( - "Login Failed", response_data.get("detail", "Invalid credentials") - ) + messagebox.showerror("Login Failed", response_data.get("detail", "Invalid credentials")) except requests.exceptions.JSONDecodeError: messagebox.showerror("Login Failed", "Server returned an invalid response.") - tb.Label(frame, text="Login", font=("Helvetica", 18, "bold")).pack(pady=10) + ctk.CTkLabel(frame, text="Login", font=("Helvetica", 18, "bold")).pack(pady=10) - tb.Label(frame, text="Email:").pack(pady=5) - entry_email = tb.Entry(frame, bootstyle="info") + ctk.CTkLabel(frame, text="Email:").pack(pady=5) + entry_email = ctk.CTkEntry(frame) entry_email.pack(pady=5) - tb.Label(frame, text="Password:").pack(pady=5) - entry_password = tb.Entry(frame, bootstyle="info", show="*") + ctk.CTkLabel(frame, text="Password:").pack(pady=5) + entry_password = ctk.CTkEntry(frame, show="*") entry_password.pack(pady=5) - btn_login = tb.Button(frame, text="Login", bootstyle="primary", command=login) - btn_login.pack(pady=15) + ctk.CTkButton(frame, text="Login", command=login).pack(pady=15) - tb.Button( + ctk.CTkButton( frame, text="Don't have an account? Register", - bootstyle="link", command=lambda: switch_func("register"), ).pack() diff --git a/app/frontend/components/auth/register.py b/app/frontend/components/auth/register.py new file mode 100644 index 0000000..3223abf --- /dev/null +++ b/app/frontend/components/auth/register.py @@ -0,0 +1,68 @@ +import customtkinter as ctk +from tkinter import messagebox +import requests + +def register_frame(parent, switch_func, api_url): + frame = ctk.CTkFrame(parent) + + def register(): + username = entry_username.get() + email = entry_email.get() + phone_number = entry_phone.get() + password = entry_password.get() + confirm_password = entry_confirm_password.get() + + if not username or not email or not phone_number or not password or not confirm_password: + messagebox.showwarning("Input Error", "All fields are required!") + return + + if password != confirm_password: + messagebox.showerror("Password Error", "Passwords do not match!") + return + + response = requests.post( + f"{api_url}/auth/signup", + json={"username": username, "email": email, "phone_number": phone_number, "password": password}, + ) + + try: + response_data = response.json() + if response.status_code == 200: + messagebox.showinfo("Registration Successful", f"Welcome, {username}!") + switch_func("login") + else: + messagebox.showerror("Registration Failed", response_data.get("detail", "Unknown error")) + except requests.exceptions.JSONDecodeError: + messagebox.showerror("Registration Failed", "Server returned an invalid response.") + + ctk.CTkLabel(frame, text="Register", font=("Helvetica", 18, "bold")).pack(pady=10) + + ctk.CTkLabel(frame, text="Username:").pack(pady=5) + entry_username = ctk.CTkEntry(frame) + entry_username.pack(pady=5) + + ctk.CTkLabel(frame, text="Email:").pack(pady=5) + entry_email = ctk.CTkEntry(frame) + entry_email.pack(pady=5) + + ctk.CTkLabel(frame, text="Phone Number:").pack(pady=5) + entry_phone = ctk.CTkEntry(frame) + entry_phone.pack(pady=5) + + ctk.CTkLabel(frame, text="Password:").pack(pady=5) + entry_password = ctk.CTkEntry(frame, show="*") + entry_password.pack(pady=5) + + ctk.CTkLabel(frame, text="Confirm Password:").pack(pady=5) + entry_confirm_password = ctk.CTkEntry(frame, show="*") + entry_confirm_password.pack(pady=5) + + ctk.CTkButton(frame, text="Register", command=register).pack(pady=15) + + ctk.CTkButton( + frame, + text="Already have an account? Login", + command=lambda: switch_func("login"), + ).pack() + + return frame diff --git a/app/frontend/components/product/create_product.py b/app/frontend/components/product/create_product.py new file mode 100644 index 0000000..2fc5b7a --- /dev/null +++ b/app/frontend/components/product/create_product.py @@ -0,0 +1,58 @@ +import customtkinter as ctk +import requests +from tkinter import messagebox + +def product_frame(parent, switch_func, API_URL, token): + frame = ctk.CTkFrame(parent) + + ctk.CTkLabel(frame, text="Product Management", font=("Helvetica", 18, "bold")).pack(pady=10) + + ctk.CTkLabel(frame, text="Product Name:").pack(pady=5) + entry_name = ctk.CTkEntry(frame) + entry_name.pack(pady=5) + + ctk.CTkLabel(frame, text="Price:").pack(pady=5) + entry_price = ctk.CTkEntry(frame) + entry_price.pack(pady=5) + + ctk.CTkLabel(frame, text="Stock:").pack(pady=5) + entry_stock = ctk.CTkEntry(frame) + entry_stock.pack(pady=5) + + def create_product(): + name = entry_name.get().strip() + price = entry_price.get().strip() + stock = entry_stock.get().strip() + + if not name or not price or not stock: + messagebox.showwarning("Input Error", "All fields are required!") + return + + try: + price = float(price) + stock = int(stock) + except ValueError: + messagebox.showwarning("Input Error", "Price must be a number and Stock must be an integer.") + return + + headers = {"Authorization": f"Bearer {token}"} + data = {"name": name, "price": price, "stock": stock} + + try: + response = requests.post(f"{API_URL}/products", json=data, headers=headers) + + if response.status_code == 200: + messagebox.showinfo("Success", f"Product '{name}' created successfully!") + entry_name.delete(0, 'end') + entry_price.delete(0, 'end') + entry_stock.delete(0, 'end') + else: + error_message = response.json().get("detail", "Failed to create product") + messagebox.showerror("Error", error_message) + except requests.exceptions.RequestException as e: + messagebox.showerror("Error", f"Failed to connect to server: {e}") + + ctk.CTkButton(frame, text="Create Product", command=create_product).pack(pady=15) + ctk.CTkButton(frame, text="Back", command=lambda: switch_func("view_shop")).pack(pady=5) + + return frame diff --git a/app/frontend/components/register.py b/app/frontend/components/register.py deleted file mode 100644 index b8aa074..0000000 --- a/app/frontend/components/register.py +++ /dev/null @@ -1,90 +0,0 @@ -import ttkbootstrap as tb -import ttkbootstrap.constants -from tkinter import messagebox -import requests # Import requests for API communication - - -def register_frame(parent, switch_func, api_url): # Added api_url parameter - frame = tb.Frame(parent) - - def register(): - username = entry_username.get() - email = entry_email.get() - phone_number = entry_phone.get() - password = entry_password.get() - confirm_password = entry_confirm_password.get() - - if ( - not username - or not email - or not phone_number - or not password - or not confirm_password - ): - messagebox.showwarning("Input Error", "All fields are required!") - return - - if password != confirm_password: - messagebox.showerror("Password Error", "Passwords do not match!") - return - - # Sending registration data to backend - response = requests.post( - f"{api_url}/auth/signup", - json={ - "username": username, - "email": email, - "phone_number": phone_number, - "password": password, - }, - ) - - try: - response_data = response.json() - if response.status_code == 200: - messagebox.showinfo("Registration Successful", f"Welcome, {username}!") - switch_func("login") # Switch to login after successful registration - else: - messagebox.showerror( - "Registration Failed", response_data.get("detail", "Unknown error") - ) - except requests.exceptions.JSONDecodeError: - messagebox.showerror( - "Registration Failed", "Server returned an invalid response." - ) - - tb.Label(frame, text="Register", font=("Helvetica", 18, "bold")).pack(pady=10) - - tb.Label(frame, text="Username:").pack(pady=5) - entry_username = tb.Entry(frame, bootstyle="info") - entry_username.pack(pady=5) - - tb.Label(frame, text="Email:").pack(pady=5) - entry_email = tb.Entry(frame, bootstyle="info") - entry_email.pack(pady=5) - - tb.Label(frame, text="Phone Number:").pack(pady=5) - entry_phone = tb.Entry(frame, bootstyle="info") - entry_phone.pack(pady=5) - - tb.Label(frame, text="Password:").pack(pady=5) - entry_password = tb.Entry(frame, bootstyle="info", show="*") - entry_password.pack(pady=5) - - tb.Label(frame, text="Confirm Password:").pack(pady=5) - entry_confirm_password = tb.Entry(frame, bootstyle="info", show="*") - entry_confirm_password.pack(pady=5) - - btn_register = tb.Button( - frame, text="Register", bootstyle="success", command=register - ) - btn_register.pack(pady=15) - - tb.Button( - frame, - text="Already have an account? Login", - bootstyle="link", - command=lambda: switch_func("login"), - ).pack() - - return frame diff --git a/app/frontend/components/shop/create_shop.py b/app/frontend/components/shop/create_shop.py index b4f8186..14a44a3 100644 --- a/app/frontend/components/shop/create_shop.py +++ b/app/frontend/components/shop/create_shop.py @@ -1,12 +1,11 @@ -import ttkbootstrap as tb -import ttkbootstrap.constants +import customtkinter as ctk from tkinter import messagebox, filedialog import requests import os def create_shop_frame(parent, switch_func, api_url, token): - frame = tb.Frame(parent) + frame = ctk.CTkFrame(parent) selected_file_path = [None] @@ -14,10 +13,10 @@ def create_shop_frame(parent, switch_func, api_url, token): file_path = filedialog.askopenfilename(title="Select Shop Image") if file_path: selected_file_path[0] = file_path - file_label.config(text=os.path.basename(file_path)) + file_label.configure(text=os.path.basename(file_path)) else: selected_file_path[0] = None - file_label.config(text="No file selected") + file_label.configure(text="No file selected") def create_shop(): name = entry_name.get() @@ -64,30 +63,26 @@ def create_shop_frame(parent, switch_func, api_url, token): except Exception as e: messagebox.showerror("Request Error", str(e)) - tb.Label(frame, text="Create Shop", font=("Helvetica", 18, "bold")).pack(pady=10) + ctk.CTkLabel(frame, text="Create Shop", font=("Helvetica", 18, "bold")).pack(pady=10) - tb.Label(frame, text="Shop Name:").pack(pady=5) - entry_name = tb.Entry(frame, bootstyle="info") + ctk.CTkLabel(frame, text="Shop Name:").pack(pady=5) + entry_name = ctk.CTkEntry(frame, placeholder_text="Enter shop name") entry_name.pack(pady=5) - tb.Label(frame, text="Description:").pack(pady=5) - entry_description = tb.Entry(frame, bootstyle="info") + ctk.CTkLabel(frame, text="Description:").pack(pady=5) + entry_description = ctk.CTkEntry(frame, placeholder_text="Enter shop description") entry_description.pack(pady=5) - tb.Button( - frame, text="Select Image", bootstyle="primary", command=select_file - ).pack(pady=5) - file_label = tb.Label(frame, text="No file selected") + ctk.CTkButton(frame, text="Select Image", command=select_file).pack(pady=5) + file_label = ctk.CTkLabel(frame, text="No file selected") file_label.pack(pady=5) - tb.Button(frame, text="Create Shop", bootstyle="success", command=create_shop).pack( - pady=15 - ) + ctk.CTkButton(frame, text="Create Shop", fg_color="green", command=create_shop).pack(pady=15) - tb.Button( + ctk.CTkButton( frame, text="Back", - bootstyle="link", + fg_color="transparent", command=lambda: switch_func("login"), ).pack(pady=5) diff --git a/app/frontend/components/shop/view_shop.py b/app/frontend/components/shop/view_shop.py new file mode 100644 index 0000000..960fec4 --- /dev/null +++ b/app/frontend/components/shop/view_shop.py @@ -0,0 +1,119 @@ +import customtkinter as ctk +import requests +from tkinter import messagebox +from PIL import Image, ImageTk +import io + +def view_shop_frame(parent, switch_func, api_url, token): + frame = ctk.CTkFrame(parent) + + # Title + title_label = ctk.CTkLabel(frame, text="Your Shop", font=("Helvetica", 18, "bold")) + title_label.pack(pady=10) + + # Shop Details + shop_name_label = ctk.CTkLabel(frame, text="Shop Name: ", font=("Helvetica", 14, "bold")) + shop_name_label.pack(pady=5) + + shop_description_label = ctk.CTkLabel(frame, text="Description: ", font=("Helvetica", 12)) + shop_description_label.pack(pady=5) + + shop_image_label = ctk.CTkLabel(frame, text="") # Placeholder for shop image + shop_image_label.pack(pady=10) + + # Product List Section + product_list_frame = ctk.CTkFrame(frame) + product_list_frame.pack(fill="both", expand=True, padx=10, pady=10) + + def fetch_shop_data(): + """ Fetch the shop created by the logged-in user """ + headers = {"Authorization": f"Bearer {token}"} + try: + response = requests.get(f"{api_url}/shops/my-shop", headers=headers) # Adjust the endpoint as needed + if response.status_code == 200: + shop_data = response.json() + shop_name_label.configure(text=f"Shop Name: {shop_data['name']}") + shop_description_label.configure(text=f"Description: {shop_data.get('description', 'No description')}") + + # Load and display shop image if available + if "image_url" in shop_data and shop_data["image_url"]: + try: + image_response = requests.get(shop_data["image_url"]) + image = Image.open(io.BytesIO(image_response.content)) + image = image.resize((150, 150)) + img_tk = ImageTk.PhotoImage(image) + + shop_image_label.configure(image=img_tk, text="") + shop_image_label.image = img_tk + except Exception: + pass + + fetch_products(shop_data["id"]) # Fetch products for this shop + else: + messagebox.showerror("Error", "Failed to fetch shop details.") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + + def fetch_products(shop_id): + """ Fetch products that belong to the user's shop """ + headers = {"Authorization": f"Bearer {token}"} + try: + response = requests.get(f"{api_url}/products?shop_id={shop_id}", headers=headers) + if response.status_code == 200: + products = response.json() + display_products(products) + else: + messagebox.showerror("Error", "Failed to fetch products.") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + + def display_products(products): + """ Display the list of products in the shop """ + for widget in product_list_frame.winfo_children(): + widget.destroy() + + if not products: + ctk.CTkLabel(product_list_frame, text="No products found.", font=("Helvetica", 12)).pack(pady=10) + return + + for product in products: + product_frame = ctk.CTkFrame(product_list_frame) + product_frame.pack(fill="x", padx=5, pady=5) + + # Product Image + img_label = ctk.CTkLabel(product_frame, text="") # Placeholder + img_label.pack(side="left", padx=5) + + # Load and display product image + if "images" in product and product["images"]: + image_url = product["images"][0]["image_url"] + try: + image_response = requests.get(image_url) + image = Image.open(io.BytesIO(image_response.content)) + image = image.resize((50, 50)) + img_tk = ImageTk.PhotoImage(image) + + img_label.configure(image=img_tk, text="") + img_label.image = img_tk + except Exception: + pass + + # Product Details + details_frame = ctk.CTkFrame(product_frame) + details_frame.pack(side="left", fill="x", expand=True, padx=10) + + ctk.CTkLabel(details_frame, text=product["name"], font=("Helvetica", 12, "bold")).pack(anchor="w") + ctk.CTkLabel(details_frame, text=f"Price: ${product['price']:.2f}", font=("Helvetica", 12)).pack(anchor="w") + + # Refresh Data Button + refresh_button = ctk.CTkButton(frame, text="Refresh", command=fetch_shop_data) + refresh_button.pack(pady=10) + + # Back Button + back_button = ctk.CTkButton(frame, text="Back", command=lambda: switch_func("login")) + back_button.pack(pady=10) + + # Fetch shop data on load + fetch_shop_data() + + return frame diff --git a/app/frontend/main.py b/app/frontend/main.py index 938dbbb..cc8b927 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -1,7 +1,10 @@ -import ttkbootstrap as tb -from components.login import login_frame -from components.register import register_frame +import customtkinter as ctk +from components.auth.login import login_frame +from components.auth.register import register_frame from components.shop.create_shop import create_shop_frame +from components.shop.view_shop import view_shop_frame +from components.product.create_product import product_frame +from components.admin.category import category_frame # Backend API URL API_URL = "http://127.0.0.1:8000" @@ -9,25 +12,19 @@ API_URL = "http://127.0.0.1:8000" # Global variable to store the access token access_token = None - # Function to switch between frames def switch_frame(frame_name, token=None): global access_token if token: access_token = token - if frame_name == "login": - login.tkraise() - elif frame_name == "register": - register.tkraise() - elif frame_name == "create_shop": - create_shop = create_shop_frame(root, switch_frame, API_URL, access_token) - create_shop.place(relx=0, rely=0.2, relwidth=1, relheight=0.8) - create_shop.tkraise() - + frames.get(frame_name, login).tkraise() # Default to login if frame_name is invalid # Create main window -root = tb.Window(themename="superhero") +ctk.set_appearance_mode("dark") # Light, Dark, or System +ctk.set_default_color_theme("blue") + +root = ctk.CTk() root.title("Shopping App") root.geometry("900x800") @@ -35,13 +32,25 @@ root.geometry("900x800") login = login_frame(root, switch_frame, API_URL) register = register_frame(root, switch_frame, API_URL) create_shop = create_shop_frame(root, switch_frame, API_URL, access_token) - -# Place all frames responsively within the window. -# Adjust relx, rely, relwidth, and relheight as needed for your layout. -for frame in (login, register, create_shop): +view_shop = view_shop_frame(root, switch_frame, API_URL, access_token) +product = product_frame(root, switch_frame, API_URL, access_token) +category = category_frame(root, switch_frame, API_URL, access_token) + +# Store frames in a dictionary for easier management +frames = { + "login": login, + "register": register, + "create_shop": create_shop, + "create_product": product, + "category": category, + "view_shop": view_shop +} + +# Place all frames responsively +for frame in frames.values(): frame.place(relx=0, rely=0.2, relwidth=1, relheight=0.8) # Show the login frame first -switch_frame("login") +switch_frame("view_shop") root.mainloop() -- GitLab