diff --git a/app/frontend/components/dashboard.py b/app/frontend/components/dashboard.py index b4bc80b308d771f80d9302b14b3c249a99c695c2..00f8caf17cb3fc055e56793932fee57e572c1cf0 100644 --- a/app/frontend/components/dashboard.py +++ b/app/frontend/components/dashboard.py @@ -138,6 +138,49 @@ def dashboard_frame(parent, switch_func, API_URL, token): carousel_content = ctk.CTkFrame(carousel_frame, fg_color="transparent") carousel_content.pack(fill="both", expand=True, padx=20, pady=20) + # Navigation buttons frame + nav_buttons_frame = ctk.CTkFrame(carousel_frame, fg_color="transparent") + nav_buttons_frame.place(relx=0.98, rely=0.5, anchor="e") + + def prev_slide(): + nonlocal current_shop_index + current_shop_index = (current_shop_index - 1) % len(featured_shops) + update_carousel(skip_timer=True) + + def next_slide(): + nonlocal current_shop_index + current_shop_index = (current_shop_index + 1) % len(featured_shops) + update_carousel(skip_timer=True) + + # Button styling configuration + button_config = { + "width": 35, + "height": 35, + "corner_radius": 20, + "font": ("Helvetica", 16, "bold"), + "fg_color": "#1f1f1f", # Dark background color + "hover_color": SHOPPING, + "text_color": SHOPPING, # Text color matches SHOPPING + "border_width": 2, + "border_color": SHOPPING + } + + prev_button = ctk.CTkButton( + nav_buttons_frame, + text="←", + command=prev_slide, + **button_config + ) + prev_button.pack(side="left", padx=3) + + next_button = ctk.CTkButton( + nav_buttons_frame, + text="→", + command=next_slide, + **button_config + ) + next_button.pack(side="left", padx=3) + featured_shops = [] current_shop_index = 0 @@ -200,7 +243,7 @@ def dashboard_frame(parent, switch_func, API_URL, token): return slide - def update_carousel(): + def update_carousel(skip_timer=False): nonlocal current_shop_index if not featured_shops: return @@ -216,9 +259,10 @@ def dashboard_frame(parent, switch_func, API_URL, token): # Update dots update_carousel_dots() - # Schedule next slide - current_shop_index = (current_shop_index + 1) % len(featured_shops) - carousel_content.after(5000, update_carousel) + # Schedule next slide only if not skipping timer + if not skip_timer: + current_shop_index = (current_shop_index + 1) % len(featured_shops) + carousel_content.after(5000, lambda: update_carousel(False)) # Carousel navigation dots dots_frame = ctk.CTkFrame(carousel_frame, fg_color="transparent", height=30) diff --git a/app/frontend/components/user_details.py b/app/frontend/components/user_details.py index 7adf510e1d7a5f027a9ece8feb9d04486cea3727..395b1a43b31aa4b35f0db0f3ac47f5c96f1d3bc4 100644 --- a/app/frontend/components/user_details.py +++ b/app/frontend/components/user_details.py @@ -83,6 +83,12 @@ def user_details_frame(parent, switch_func, API_URL, token): nav_orders = create_nav_button("My Orders", lambda: switch_func("user_orders")) nav_orders.pack(fill="x", padx=15, pady=5) + def open_payments(): + switch_func("user_payments") + + nav_payments = create_nav_button("My Payments", open_payments) + nav_payments.pack(fill="x", padx=15, pady=5) + become_owner = create_nav_button("Become Shop Owner", lambda: switch_func("create_shop")) become_owner.pack(fill="x", padx=15, pady=5) diff --git a/app/frontend/components/user_orders.py b/app/frontend/components/user_orders.py index cbb83ea355c633cde9e52ff090bfb88d15f1ee5a..f112f31b63c8da40c8f0ca8a000ade6f6aaa3a72 100644 --- a/app/frontend/components/user_orders.py +++ b/app/frontend/components/user_orders.py @@ -87,6 +87,12 @@ def user_orders_frame(parent, switch_func, API_URL, token): nav_orders = create_nav_button("My Orders", is_active=True) nav_orders.pack(fill="x", padx=15, pady=5) + def open_payments(): + switch_func("user_payments") + + nav_payments = create_nav_button("My Payments", open_payments) + nav_payments.pack(fill="x", padx=15, pady=5) + # ----------------- RIGHT CONTENT (Orders List) ----------------- content_frame = ctk.CTkFrame(main_section, fg_color=CARD_BG, corner_radius=15) content_frame.pack(side="left", fill="both", expand=True, padx=(0, 20), pady=20) diff --git a/app/frontend/components/user_payments.py b/app/frontend/components/user_payments.py new file mode 100644 index 0000000000000000000000000000000000000000..9b8e41418444fbb0e6001dfb5b75d8ed0bd7cbb3 --- /dev/null +++ b/app/frontend/components/user_payments.py @@ -0,0 +1,323 @@ +import customtkinter as ctk +import requests +from tkinter import messagebox +from datetime import datetime +from utils.api_requests import add_payment_method + +SHOPPING = "#00c1ff" +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" + + +def user_payments_frame(parent, switch_func, API_URL, token): + """ + User payments page with a modern dark theme and improved layout. + """ + # Main container frame + frame = ctk.CTkFrame(parent, fg_color=DARK_BG) + frame.token = token # Store token as an attribute + + def update_token(new_token): + """Update the token when it changes""" + frame.token = new_token + + # Add the update_token method to the frame + frame.update_token = update_token + + # --- TOP BAR --- + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=50) + top_bar.pack(fill="x", side="top") + + top_label = ctk.CTkLabel( + top_bar, text="My Payments", text_color="white", font=("Helvetica", 20, "bold") + ) + top_label.pack(side="left", padx=25) + + def go_back(): + switch_func("dashboard") + + back_button = ctk.CTkButton( + top_bar, + text="← Back", + fg_color="transparent", + text_color="white", + hover_color="#0096ff", + command=go_back, + width=80, + height=35, + font=("Helvetica", 14), + ) + back_button.pack(side="right", padx=20, pady=7) + + # --- 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=250, fg_color=CARD_BG, corner_radius=15 + ) + sidebar_frame.pack(side="left", fill="y", padx=20, pady=20) + sidebar_frame.pack_propagate(False) + + sidebar_title = ctk.CTkLabel( + sidebar_frame, text="Menu", font=("Helvetica", 18, "bold"), text_color=SHOPPING + ) + sidebar_title.pack(pady=(20, 10)) + + def create_nav_button(text, command=None, is_active=False): + return ctk.CTkButton( + sidebar_frame, + text=text, + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + command=command, + height=40, + font=("Helvetica", 14), + state="disabled" if is_active else "normal", + ) + + nav_dashboard = create_nav_button("Dashboard", go_back) + nav_dashboard.pack(fill="x", padx=15, pady=5) + + def open_profile(): + switch_func("user_details") + + nav_profile = create_nav_button("My Profile", open_profile) + nav_profile.pack(fill="x", padx=15, pady=5) + + def open_orders(): + switch_func("user_orders") + + nav_orders = create_nav_button("My Orders", open_orders) + nav_orders.pack(fill="x", padx=15, pady=5) + + nav_payments = create_nav_button("My Payments", is_active=True) + nav_payments.pack(fill="x", padx=15, pady=5) + + # RIGHT CONTENT + content_frame = ctk.CTkFrame(main_section, fg_color=CARD_BG, corner_radius=15) + content_frame.pack(side="left", fill="both", expand=True, padx=(0, 20), pady=20) + + # Header + header_frame = ctk.CTkFrame(content_frame, fg_color="transparent") + header_frame.pack(fill="x", padx=30, pady=20) + + title_label = ctk.CTkLabel( + header_frame, + text="Payment Methods", + font=("Helvetica", 24, "bold"), + text_color=SHOPPING, + ) + title_label.pack(anchor="w") + + subtitle_label = ctk.CTkLabel( + header_frame, + text="Add and manage your payment methods", + font=("Helvetica", 14), + text_color="gray", + ) + subtitle_label.pack(anchor="w", pady=(5, 0)) + + # Main Content + main_content = ctk.CTkFrame(content_frame, fg_color="transparent") + main_content.pack(fill="both", expand=True, padx=30, pady=(0, 20)) + + # Payment Form + form_frame = ctk.CTkFrame(main_content, fg_color="transparent") + form_frame.pack(fill="x", pady=(0, 20)) + + # Card Number Field + card_label = ctk.CTkLabel( + form_frame, text="Card Number", font=("Helvetica", 14), text_color="white" + ) + card_label.pack(anchor="w", pady=(0, 5)) + + card_entry = ctk.CTkEntry( + form_frame, + placeholder_text="Enter your card number", + height=40, + corner_radius=8, + ) + card_entry.pack(fill="x", pady=(0, 15)) + + # Card Type Selection + card_type_label = ctk.CTkLabel( + form_frame, text="Card Type", font=("Helvetica", 14), text_color="white" + ) + card_type_label.pack(anchor="w", pady=(0, 5)) + + card_type_var = ctk.StringVar(value="visa") + card_type_options = ["Visa", "Mastercard", "PayPal"] + + card_type_dropdown = ctk.CTkOptionMenu( + form_frame, + values=card_type_options, + variable=card_type_var, + width=200, + height=40, + corner_radius=8, + fg_color="#3b3b3b", + button_color=SHOPPING, + button_hover_color="#0096ff", + dropdown_fg_color="#3b3b3b", + font=("Helvetica", 14), + ) + card_type_dropdown.pack(anchor="w", pady=(0, 15)) + + # Expiration Date + exp_label = ctk.CTkLabel( + form_frame, + text="Expiration Date (MM/YY)", + font=("Helvetica", 14), + text_color="white", + ) + exp_label.pack(anchor="w", pady=(0, 5)) + + exp_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + exp_frame.pack(fill="x", pady=(0, 15)) + + month_entry = ctk.CTkEntry( + exp_frame, placeholder_text="MM", width=60, height=40, corner_radius=8 + ) + month_entry.pack(side="left", padx=(0, 5)) + + slash_label = ctk.CTkLabel( + exp_frame, text="/", font=("Helvetica", 14), text_color="white" + ) + slash_label.pack(side="left", padx=5) + + year_entry = ctk.CTkEntry( + exp_frame, placeholder_text="YY", width=60, height=40, corner_radius=8 + ) + year_entry.pack(side="left", padx=5) + + # Security Code + cvv_label = ctk.CTkLabel( + form_frame, text="Security Code", font=("Helvetica", 14), text_color="white" + ) + cvv_label.pack(anchor="w", pady=(0, 5)) + + cvv_entry = ctk.CTkEntry( + form_frame, + placeholder_text="CVV", + width=100, + height=40, + corner_radius=8, + show="*", + ) + cvv_entry.pack(anchor="w") + + def validate_card_number(event=None): + value = card_entry.get().replace(" ", "") + if not value.isdigit(): + card_entry.delete(0, "end") + card_entry.insert(0, "".join(c for c in value if c.isdigit())) + if len(value) > 16: + card_entry.delete(16, "end") + + def validate_month(event=None): + value = month_entry.get() + if not value.isdigit(): + month_entry.delete(0, "end") + return + if len(value) > 2: + month_entry.delete(2, "end") + if value and int(value) > 12: + month_entry.delete(0, "end") + month_entry.insert(0, "12") + + def validate_year(event=None): + value = year_entry.get() + if not value.isdigit(): + year_entry.delete(0, "end") + return + if len(value) > 2: + year_entry.delete(2, "end") + # Only validate the range if we have 2 digits + if len(value) == 2 and (int(value) < 25 or int(value) > 99): + messagebox.showerror("Error", "Please input the correct year") + year_entry.delete(0, "end") + + def validate_cvv(event=None): + value = cvv_entry.get() + if not value.isdigit(): + cvv_entry.delete(0, "end") + cvv_entry.insert(0, "".join(c for c in value if c.isdigit())) + if len(value) > 3: + cvv_entry.delete(3, "end") + + # Bind validation functions + card_entry.bind("<KeyRelease>", validate_card_number) + month_entry.bind("<KeyRelease>", validate_month) + year_entry.bind("<KeyRelease>", validate_year) + cvv_entry.bind("<KeyRelease>", validate_cvv) + + def save_payment(): + # Validate all fields + card_num = card_entry.get().strip() + month = month_entry.get().strip() + year = year_entry.get().strip() + cvv = cvv_entry.get().strip() + card_type = card_type_var.get().lower() # Convert to lowercase to match backend expectations + + if not all([card_num, month, year, cvv]): + messagebox.showerror("Error", "Please fill in all fields") + return + + if len(card_num) < 12: + messagebox.showerror("Error", "Invalid card number") + return + + if not (1 <= int(month) <= 12): + messagebox.showerror("Error", "Invalid month") + return + + if not (25 <= int(year) <= 99): + messagebox.showerror("Error", "Invalid year") + return + + if len(cvv) != 3: + messagebox.showerror("Error", "Invalid security code") + return + + # Create payload + payload = { + "payment_method": card_type, + "card_number": card_num, + "expiry_date": f"{month}/{year}", + "cvv": cvv + } + + # Use the API function to save payment + status_code, response = add_payment_method(API_URL, frame.token, payload) + + if status_code == 200: + messagebox.showinfo("Success", "Payment method added successfully!") + # Clear form + card_entry.delete(0, "end") + month_entry.delete(0, "end") + year_entry.delete(0, "end") + cvv_entry.delete(0, "end") + card_type_var.set("Visa") # Reset to default option + else: + error_msg = "Failed to add payment method" + if isinstance(response, dict) and "detail" in response: + error_msg = response["detail"] + messagebox.showerror("Error", error_msg) + + # Save Button + save_button = ctk.CTkButton( + form_frame, + text="Save Payment Method", + command=save_payment, + height=45, + corner_radius=8, + font=("Helvetica", 14, "bold"), + fg_color=SHOPPING, + hover_color="#0096ff", + ) + save_button.pack(fill="x", pady=(30, 0)) + + return frame diff --git a/app/frontend/main.py b/app/frontend/main.py index e53b707183752a43c62a1a7a4aad15a87447e778..a0de2d46104302d11348dd7be389a8bc85913445 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -8,6 +8,7 @@ from components.admin.category import category_frame from components.dashboard import dashboard_frame from components.user_details import user_details_frame from components.user_orders import user_orders_frame +from components.user_payments import user_payments_frame API_URL = "http://127.0.0.1:8000" access_token = None # Global token @@ -21,14 +22,16 @@ def switch_frame(frame_name, *args): global access_token if args and args[0] and isinstance(args[0], str): access_token = args[0] + # Update token for all frames that need it + for frame_key in ["dashboard", "user_details", "user_orders", "user_payments", "create_shop", "view_shop", "product", "category"]: + if frame_key in frames and hasattr(frames[frame_key], "update_token"): + frames[frame_key].update_token(access_token) frame = frames.get(frame_name) if frame is None: print(f"Frame {frame_name} not found!") return - if hasattr(frame, "update_token"): - frame.update_token(access_token) if hasattr(frame, "refresh_data") and len(args) > 0: frame.refresh_data(*args) @@ -55,6 +58,7 @@ 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) +user_payments = user_payments_frame(root, switch_frame, API_URL, access_token) frames = { "login": login, @@ -66,6 +70,7 @@ frames = { "dashboard": dashboard, "user_details": user_details, "user_orders": user_orders, + "user_payments": user_payments, } for frame in frames.values(): diff --git a/app/frontend/utils/api_requests.py b/app/frontend/utils/api_requests.py index 59aa0806c59531d6ce2b760712be7a3adec561b5..95d56892e2e98abe225e953cff44bc269d42a270 100644 --- a/app/frontend/utils/api_requests.py +++ b/app/frontend/utils/api_requests.py @@ -147,3 +147,28 @@ def load_image_from_url(image_url, size=(150, 150)): return image except Exception: return None + + +# Payment API +def add_payment_method(api_url, token, payment_data): + """ + Sends a request to add a new payment method. + + :param api_url: Base API URL + :param token: User's access token + :param payment_data: Dictionary containing payment details + :return: Tuple (status_code, response_data) + """ + if not token: + return None, {"detail": "Access token not found. Please log in."} + + headers = {"Authorization": f"Bearer {token}"} + try: + response = requests.post( + f"{api_url}/payment/add", + headers=headers, + json=payment_data + ) + return response.status_code, response.json() + except requests.exceptions.RequestException as e: + return None, {"detail": str(e)}