diff --git a/app/frontend/components/auth/login.py b/app/frontend/components/auth/login.py index f2330a5d06f516e7995c152cc2317b06c1b52848..79f360b50f3b8e40a327bcd04dacd8934c2ac2f1 100644 --- a/app/frontend/components/auth/login.py +++ b/app/frontend/components/auth/login.py @@ -3,6 +3,7 @@ from tkinter import messagebox from PIL import Image from utils.api_requests import login_api # Import the login function from login_api.py + def login_frame(parent, switch_func, API_URL): # Create a container frame to hold both left (empty) and right (login) frames container = ctk.CTkFrame(parent) @@ -16,10 +17,12 @@ def login_frame(parent, switch_func, API_URL): # Load and display the image image_path = "app/static/front_end_img/login.jpg" # Change this to your image path - img = ctk.CTkImage(light_image=Image.open(image_path), size=(1000, 1000)) # Resize as needed + img = ctk.CTkImage( + light_image=Image.open(image_path), size=(1000, 1000) + ) # Resize as needed image_label = ctk.CTkLabel(left_frame, image=img, text="") # No text, only image image_label.place(relwidth=1, relheight=1) - + # Right login frame right_frame = ctk.CTkFrame(container) right_frame.grid(row=0, column=1, sticky="nsew") @@ -38,14 +41,16 @@ def login_frame(parent, switch_func, API_URL): if status_code == 200: access_token = response_data.get("access_token") messagebox.showinfo("Login Successful", f"Welcome back, {email}!") - switch_func("view_shop", access_token) + switch_func("dashboard", access_token) print(f"Access Token in login: {access_token}") # Debugging line else: messagebox.showerror( "Login Failed", response_data.get("detail", "Invalid credentials") ) - ctk.CTkLabel(right_frame, text="Login", font=("Helvetica", 18, "bold")).pack(pady=10) + ctk.CTkLabel(right_frame, text="Login", font=("Helvetica", 18, "bold")).pack( + pady=10 + ) ctk.CTkLabel(right_frame, text="Email:").pack(pady=5) entry_email = ctk.CTkEntry(right_frame) diff --git a/app/frontend/components/dashboard.py b/app/frontend/components/dashboard.py index 934e685f890434bf3dee60b1ea50cdb686538809..75622c90c5895d8d39a7685a2debacae8704d269 100644 --- a/app/frontend/components/dashboard.py +++ b/app/frontend/components/dashboard.py @@ -57,6 +57,73 @@ def dashboard_frame(parent, switch_func, API_URL, token): ) search_button.place(relx=0.71, rely=0.25, relwidth=0.08, relheight=0.5) + # ------------- USER & CART ICONS (Top-Right) ------------- + def open_user_details(): + # Switch to user_details.py screen or run it + switch_func("user_details") + # Alternatively, use os.system("python user_details.py") + + def open_cart_details(): + # Switch to cart_shopping.py screen or run it + switch_func("cart_shopping") + # Alternatively, use os.system("python cart_shopping.py") + + # Try loading icon images; update paths as needed. + try: + user_image = Image.open("path/to/user_icon.png").resize((30, 30)) + user_icon = ImageTk.PhotoImage(user_image) + except Exception as e: + print(f"User icon load error: {e}") + user_icon = None + + try: + cart_image = Image.open("path/to/cart_icon.png").resize((30, 30)) + cart_icon = ImageTk.PhotoImage(cart_image) + except Exception as e: + print(f"Cart icon load error: {e}") + cart_icon = None + + if user_icon: + user_button = ctk.CTkButton( + header_frame, + image=user_icon, + text="", + fg_color="transparent", + command=open_user_details, + ) + switch_func("user_details") + user_button.image = user_icon + user_button.place(relx=0.82, rely=0.25, relwidth=0.08, relheight=0.5) + else: + user_button = ctk.CTkButton( + header_frame, + text="User", + fg_color="white", + text_color="black", + command=open_user_details, + ) + user_button.place(relx=0.82, rely=0.25, relwidth=0.08, relheight=0.5) + + if cart_icon: + cart_button = ctk.CTkButton( + header_frame, + image=cart_icon, + text="", + fg_color="transparent", + command=open_cart_details, + ) + cart_button.image = cart_icon + cart_button.place(relx=0.91, rely=0.25, relwidth=0.08, relheight=0.5) + else: + cart_button = ctk.CTkButton( + header_frame, + text="Cart", + fg_color="white", + text_color="black", + command=open_cart_details, + ) + cart_button.place(relx=0.91, rely=0.25, relwidth=0.08, relheight=0.5) + # ------------- MIDDLE (Featured/Top Shops) ------------- middle_frame = ctk.CTkFrame(frame, fg_color="transparent") middle_frame.place(relx=0, rely=0.1, relwidth=1, relheight=0.25) diff --git a/app/frontend/components/user_details.py b/app/frontend/components/user_details.py new file mode 100644 index 0000000000000000000000000000000000000000..7a4903265babe73a82b142e70bd2a2047efe7335 --- /dev/null +++ b/app/frontend/components/user_details.py @@ -0,0 +1,297 @@ +import customtkinter as ctk +import requests +from PIL import Image, ImageTk +from tkinter import messagebox, filedialog +import io + +SHOPPING = "#00c1ff" + +def user_details_frame(parent, switch_func, API_URL, token): + """ + A two-column user details page that matches the dashboard color scheme: + - Top bar with SHOPPING color (#00c1ff) + - Left sidebar with a dark background (#2b2b2b) + - Right content area (profile form, profile picture) + """ + + # Main container frame (transparent background, like dashboard) + frame = ctk.CTkFrame(parent, fg_color="transparent") + + # ----------------- TOP BAR ----------------- + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=40) + top_bar.pack(fill="x", side="top") + + top_label = ctk.CTkLabel( + top_bar, + text="My Profile", + text_color="white", + font=("Helvetica", 16, "bold"), + ) + top_label.pack(side="left", padx=20) + + def go_back(): + switch_func("dashboard") + + back_button = ctk.CTkButton( + top_bar, + text="Back", + fg_color="white", + text_color="black", + command=go_back, + width=60, + height=30, + ) + back_button.pack(side="right", padx=20, pady=5) + + # ----------------- MAIN SECTION (Sidebar + Content) ----------------- + main_section = ctk.CTkFrame(frame, fg_color="transparent") + main_section.pack(fill="both", expand=True) + + # ----------------- LEFT SIDEBAR ----------------- + + sidebar_frame = ctk.CTkFrame(main_section, width=200, fg_color="#2b2b2b") + sidebar_frame.pack(side="left", fill="y") + + sidebar_title = ctk.CTkLabel( + sidebar_frame, + text="Menu", + font=("Helvetica", 14, "bold"), + text_color="white" + ) + sidebar_title.pack(pady=(10, 5)) + + nav_dashboard = ctk.CTkButton( + sidebar_frame, + text="Dashboard", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=go_back + ) + nav_dashboard.pack(fill="x", padx=10, pady=5) + + nav_profile = ctk.CTkButton( + sidebar_frame, + text="My Profile", + fg_color="#3b3b3b", # Active/selected state + text_color="white", + hover_color="#3b3b3b", + state="disabled" + ) + nav_profile.pack(fill="x", padx=10, pady=5) + + nav_orders = ctk.CTkButton( + sidebar_frame, + text="My Orders", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=lambda: switch_func("user_orders") + ) + nav_orders.pack(fill="x", padx=10, pady=5) + + nav_address = ctk.CTkButton( + sidebar_frame, + text="Address Book", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=lambda: messagebox.showinfo("Info", "Address Book clicked!") + ) + nav_address.pack(fill="x", padx=10, pady=5) + + # ----------------- RIGHT CONTENT (User Details) ----------------- + content_frame = ctk.CTkFrame(main_section, fg_color="transparent") + content_frame.pack(side="left", fill="both", expand=True, padx=20, pady=20) + + # Title / Subtitle + title_label = ctk.CTkLabel( + content_frame, + text="My Profile", + font=("Helvetica", 18, "bold"), + text_color="white" + ) + title_label.pack(anchor="w", pady=(0, 5)) + + subtitle_label = ctk.CTkLabel( + content_frame, + text="Manage your profile information to keep your account secure", + font=("Helvetica", 12), + text_color="#cccccc" + ) + subtitle_label.pack(anchor="w", pady=(0, 15)) + + # -- Split the right content into two frames: Left form & Right picture -- + right_main = ctk.CTkFrame(content_frame, fg_color="transparent") + right_main.pack(fill="both", expand=True) + + # LEFT FORM + form_frame = ctk.CTkFrame(right_main, fg_color="transparent") + form_frame.pack(side="left", fill="both", expand=True, padx=(0, 20)) + + # Username + username_label = ctk.CTkLabel(form_frame, text="Username", font=("Helvetica", 12), text_color="white") + username_label.pack(anchor="w") + username_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your username...") + username_entry.pack(anchor="w", pady=(0, 10)) + + # Name + name_label = ctk.CTkLabel(form_frame, text="Name", font=("Helvetica", 12), text_color="white") + name_label.pack(anchor="w") + name_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your name...") + name_entry.pack(anchor="w", pady=(0, 10)) + + # Email + email_label = ctk.CTkLabel(form_frame, text="Email", font=("Helvetica", 12), text_color="white") + email_label.pack(anchor="w") + email_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your email...") + email_entry.pack(anchor="w", pady=(0, 10)) + + # Phone + phone_label = ctk.CTkLabel(form_frame, text="Phone", font=("Helvetica", 12), text_color="white") + phone_label.pack(anchor="w") + phone_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your phone...") + phone_entry.pack(anchor="w", pady=(0, 10)) + + # Gender + gender_label = ctk.CTkLabel(form_frame, text="Gender", font=("Helvetica", 12), text_color="white") + gender_label.pack(anchor="w", pady=(10, 0)) + + gender_var = ctk.StringVar(value="Male") # Default + gender_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + gender_frame.pack(anchor="w", pady=5) + + male_radio = ctk.CTkRadioButton(gender_frame, text="Male", variable=gender_var, value="Male", text_color="white") + female_radio = ctk.CTkRadioButton(gender_frame, text="Female", variable=gender_var, value="Female", text_color="white") + other_radio = ctk.CTkRadioButton(gender_frame, text="Other", variable=gender_var, value="Other", text_color="white") + male_radio.pack(side="left", padx=5) + female_radio.pack(side="left", padx=5) + other_radio.pack(side="left", padx=5) + + # Birthday + birthday_label = ctk.CTkLabel(form_frame, text="Date of Birth", font=("Helvetica", 12), text_color="white") + birthday_label.pack(anchor="w", pady=(10, 0)) + birthday_entry = ctk.CTkEntry(form_frame, placeholder_text="dd/mm/yyyy") + birthday_entry.pack(anchor="w", pady=(0, 10)) + + # Save Button + def save_profile(): + """ + Example: Send a request to update user details. + Adjust the endpoint and payload for your backend. + """ + headers = {"Authorization": f"Bearer {token}"} + payload = { + "username": username_entry.get().strip(), + "name": name_entry.get().strip(), + "email": email_entry.get().strip(), + "phone": phone_entry.get().strip(), + "gender": gender_var.get(), + "birthday": birthday_entry.get().strip() + } + try: + resp = requests.put(f"{API_URL}/user/update", headers=headers, json=payload) + if resp.status_code == 200: + messagebox.showinfo("Success", "Profile updated successfully!") + else: + messagebox.showerror("Error", "Unable to update profile.") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + + save_button = ctk.CTkButton( + form_frame, + text="Save", + fg_color=SHOPPING, # #00c1ff + text_color="white", + command=save_profile, + width=80 + ) + save_button.pack(anchor="w", pady=(20, 10)) + + # RIGHT PICTURE SECTION + pic_frame = ctk.CTkFrame(right_main, fg_color="transparent") + pic_frame.pack(side="left", fill="both", expand=True) + + pic_label = ctk.CTkLabel(pic_frame, text="Profile Picture", font=("Helvetica", 12, "bold"), text_color="white") + pic_label.pack(anchor="w", pady=(0, 10)) + + photo_label = ctk.CTkLabel(pic_frame, text="No image", text_color="white") + photo_label.pack(anchor="w", pady=(0, 10)) + + def choose_photo(): + """ + Let the user pick a photo from their files. + Then optionally upload it or just display it. + """ + file_path = filedialog.askopenfilename( + title="Choose a Profile Picture", + filetypes=[("Image Files", "*.png *.jpg *.jpeg *.gif")] + ) + if file_path: + # Display chosen image + try: + pil_img = Image.open(file_path).resize((100, 100)) + tk_img = ImageTk.PhotoImage(pil_img) + photo_label.configure(image=tk_img, text="") + photo_label.image = tk_img + + # Optionally, upload the file to your server: + # headers = {"Authorization": f"Bearer {token}"} + # with open(file_path, 'rb') as f: + # requests.post(f'{API_URL}/user/upload_photo', files={'file': f}, headers=headers) + except Exception as e: + print(f"Image load error: {e}") + messagebox.showerror("Error", "Cannot load the image.") + + change_photo_button = ctk.CTkButton( + pic_frame, + text="Choose Image", + fg_color="white", + text_color="black", + command=choose_photo, + width=80 + ) + change_photo_button.pack(anchor="w") + + # Fetch existing user info (if needed) + def fetch_user_info(): + """ + Fetch the user's existing data to populate the form fields. + """ + headers = {"Authorization": f"Bearer {token}"} + try: + resp = requests.get(f"{API_URL}/user/profile", headers=headers) + if resp.status_code == 200: + data = resp.json() + username_entry.delete(0, "end") + username_entry.insert(0, data.get("username", "")) + name_entry.delete(0, "end") + name_entry.insert(0, data.get("name", "")) + email_entry.delete(0, "end") + email_entry.insert(0, data.get("email", "")) + phone_entry.delete(0, "end") + phone_entry.insert(0, data.get("phone", "")) + gender_var.set(data.get("gender", "Male")) + birthday_entry.delete(0, "end") + birthday_entry.insert(0, data.get("birthday", "")) + + # If there's a profile picture URL, load it + pic_url = data.get("photo_url") + if pic_url: + try: + resp_pic = requests.get(pic_url) + if resp_pic.status_code == 200: + pil_img = Image.open(io.BytesIO(resp_pic.content)).resize((100, 100)) + tk_img = ImageTk.PhotoImage(pil_img) + photo_label.configure(image=tk_img, text="") + photo_label.image = tk_img + except Exception as e: + print(f"Profile picture load error: {e}") + else: + messagebox.showerror("Error", "Unable to retrieve user information.") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + + fetch_user_info() + + return frame diff --git a/app/frontend/components/user_orders.py b/app/frontend/components/user_orders.py new file mode 100644 index 0000000000000000000000000000000000000000..b71b34fc7f27dd6ebfcf97eb4b3b9ad1210b57a2 --- /dev/null +++ b/app/frontend/components/user_orders.py @@ -0,0 +1,228 @@ +import customtkinter as ctk +import requests +from PIL import Image, ImageTk +from tkinter import messagebox +import io + +SHOPPING = "#00c1ff" + +def user_orders_frame(parent, switch_func, API_URL, token): + """ + A two-column user orders page that displays the products the user has purchased + along with associated shop details. The layout and color scheme match your dashboard. + """ + + # Main container frame with transparent background + frame = ctk.CTkFrame(parent, fg_color="transparent") + + # ----------------- TOP BAR ----------------- + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=40) + top_bar.pack(fill="x", side="top") + + top_label = ctk.CTkLabel( + top_bar, + text="My Orders", + text_color="white", + font=("Helvetica", 16, "bold") + ) + top_label.pack(side="left", padx=20) + + def go_back(): + switch_func("dashboard") + + back_button = ctk.CTkButton( + top_bar, + text="Back", + fg_color="white", + text_color="black", + command=go_back, + width=60, + height=30 + ) + back_button.pack(side="right", padx=20, pady=5) + + # ----------------- MAIN SECTION (Sidebar + Content) ----------------- + main_section = ctk.CTkFrame(frame, fg_color="transparent") + main_section.pack(fill="both", expand=True) + + # ----------------- LEFT SIDEBAR ----------------- + sidebar_frame = ctk.CTkFrame(main_section, width=200, fg_color="#2b2b2b") + sidebar_frame.pack(side="left", fill="y") + + sidebar_title = ctk.CTkLabel( + sidebar_frame, + text="Menu", + font=("Helvetica", 14, "bold"), + text_color="white" + ) + sidebar_title.pack(pady=(10, 5)) + + def open_profile(): + switch_func("user_details") + + nav_dashboard = ctk.CTkButton( + sidebar_frame, + text="Dashboard", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=go_back + ) + nav_dashboard.pack(fill="x", padx=10, pady=5) + + nav_profile = ctk.CTkButton( + sidebar_frame, + text="My Profile", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=open_profile + ) + nav_profile.pack(fill="x", padx=10, pady=5) + + nav_orders = ctk.CTkButton( + sidebar_frame, + text="My Orders", + fg_color="#3b3b3b", # Active/selected state + text_color="white", + hover_color="#3b3b3b", + state="disabled" + ) + nav_orders.pack(fill="x", padx=10, pady=5) + + # ----------------- RIGHT CONTENT (Orders List) ----------------- + content_frame = ctk.CTkFrame(main_section, fg_color="transparent") + content_frame.pack(side="left", fill="both", expand=True, padx=20, pady=20) + + main_title_label = ctk.CTkLabel( + content_frame, + text="Your Orders", + font=("Helvetica", 18, "bold"), + text_color="white" + ) + main_title_label.pack(anchor="w", pady=(0, 5)) + + subtitle_label = ctk.CTkLabel( + content_frame, + text="Review the products you have purchased.", + font=("Helvetica", 12), + text_color="#cccccc" + ) + subtitle_label.pack(anchor="w", pady=(0, 15)) + + # A frame to hold the list of orders + orders_list_frame = ctk.CTkFrame(content_frame, fg_color="transparent") + orders_list_frame.pack(fill="both", expand=True) + + # ----------- Functions to fetch and display orders ----------- + def fetch_orders(): + """ + Fetch the list of user orders from the API. + """ + headers = {"Authorization": f"Bearer {token}"} + try: + resp = requests.get(f"{API_URL}/orders/list", headers=headers) + if resp.status_code == 200: + orders = resp.json() # Expect a list of order dicts + display_orders(orders, orders_list_frame) + else: + messagebox.showerror("Error", "Failed to fetch orders.") + except Exception as ex: + messagebox.showerror("Error", f"Request error: {ex}") + + def display_orders(orders, container): + """ + Display each order with product and shop details. + """ + # Clear previous content + for widget in container.winfo_children(): + widget.destroy() + + if not orders: + ctk.CTkLabel(container, text="No orders found.", text_color="white").pack(pady=10) + return + + for order in orders: + # Assume each order dict includes a 'product' dict and an 'order_date' + product = order.get("product", {}) + order_date = order.get("order_date", "Unknown Date") + + order_frame = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b") + order_frame.pack(fill="x", padx=5, pady=5) + + # Left: Product image + image_label = ctk.CTkLabel(order_frame, text="") + image_label.pack(side="left", padx=5, pady=5) + if product.get("images"): + try: + img_url = product["images"][0]["image_url"] + img_resp = requests.get(img_url) + if img_resp.status_code == 200: + pil_img = Image.open(io.BytesIO(img_resp.content)).resize((60, 60)) + tk_img = ImageTk.PhotoImage(pil_img) + image_label.configure(image=tk_img, text="") + image_label.image = tk_img + except Exception as ex: + print(f"Product image error: {ex}") + + # Right: Order details and shop info + info_frame = ctk.CTkFrame(order_frame, fg_color="transparent") + info_frame.pack(side="left", fill="both", expand=True, padx=10) + + # Product Name + ctk.CTkLabel( + info_frame, + text=product.get("name", "No Name"), + font=("Helvetica", 13, "bold"), + text_color="white" + ).pack(anchor="w") + + # Price (and order date) + price = product.get("price", 0.0) + ctk.CTkLabel( + info_frame, + text=f"Price: {price:.2f}", + text_color="#cccccc" + ).pack(anchor="w") + ctk.CTkLabel( + info_frame, + text=f"Ordered on: {order_date}", + text_color="#cccccc" + ).pack(anchor="w") + + # Shop Name + shop_id = product.get("shop_id") + shop_name_label = ctk.CTkLabel(info_frame, text="Shop: Loading...", text_color="#cccccc") + shop_name_label.pack(anchor="w") + + def fetch_shop_and_update_label(sid, label_widget): + headers = {"Authorization": f"Bearer {token}"} + try: + sresp = requests.get(f"{API_URL}/shop/get/{sid}", headers=headers) + if sresp.status_code == 200: + shop_data = sresp.json() + label_widget.configure(text=f"Shop: {shop_data.get('name', 'No Shop Name')}") + else: + label_widget.configure(text="Shop: Not found") + except Exception: + label_widget.configure(text="Shop: Error fetching") + + fetch_shop_and_update_label(shop_id, shop_name_label) + + # "View Order" button (placeholder action) + def view_order(): + messagebox.showinfo("Order Details", f"View details for order of {product.get('name')}") + + view_button = ctk.CTkButton( + info_frame, + text="View Order", + fg_color=SHOPPING, + text_color="white", + command=view_order + ) + view_button.pack(anchor="e", pady=(5, 0)) + + # Fetch orders when the frame loads + fetch_orders() + + return frame diff --git a/app/frontend/main.py b/app/frontend/main.py index 67733940b729ef0edc98fb41e7524bb7e64f0ce8..65b8f0af7e3ad2c8f48b1b343ae2719166ed899e 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -5,7 +5,9 @@ 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 -from components.dashboard import dashboard_frame # import the dashboard +from components.dashboard import dashboard_frame +from components.user_details import user_details_frame +from components.user_orders import user_orders_frame API_URL = "http://127.0.0.1:8000" access_token = None @@ -37,6 +39,8 @@ 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) dashboard = dashboard_frame(root, switch_frame, API_URL, access_token) +user_details = user_details_frame(root, switch_frame, API_URL, access_token) +user_orders = user_orders_frame(root, switch_frame, API_URL, access_token) frames = { "login": login, @@ -46,6 +50,8 @@ frames = { "category": category, "view_shop": view_shop, "dashboard": dashboard, + "user_details": user_details, + "user_orders": user_orders, } for frame in frames.values(): diff --git a/requirements.txt b/requirements.txt index 2383f12bc661c32a3b8eac0fcf9146bd60a0f3fe..1c5bb150f1acbbb5b29d84ddfede66a9ed8b7204 100644 Binary files a/requirements.txt and b/requirements.txt differ