diff --git a/app/backend/routes/payment.py b/app/backend/routes/payment.py index ba9e21e06716657c8d130f7a3a6e192fb64c12ac..e012a56928189e0800aa6bd1177e260781b03009 100644 --- a/app/backend/routes/payment.py +++ b/app/backend/routes/payment.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException -from sqlmodel import Session +from sqlmodel import Session, select from backend.models.models import Payment, User from backend.schemas.payment import PaymentCreate, PaymentRead from backend.database import get_session @@ -32,7 +32,19 @@ def add_payment( session.commit() session.refresh(new_payment) - return new_payment + return PaymentRead.from_orm(new_payment) + + +@router.get("/user", response_model=list[PaymentRead]) +def get_user_payments( + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user), +): + """Get all payment methods for the current user.""" + payments = session.exec( + select(Payment).where(Payment.user_id == current_user.id) + ).all() + return [PaymentRead.from_orm(payment) for payment in payments] @router.get("/{payment_id}", response_model=PaymentRead) @@ -45,7 +57,7 @@ def read_payment( payment = session.get(Payment, payment_id) if not payment or payment.user_id != current_user.id: raise HTTPException(status_code=404, detail="Payment not found") - return payment + return PaymentRead.from_orm(payment) @router.put("/{payment_id}", response_model=PaymentRead) @@ -69,7 +81,7 @@ def update_payment( session.add(payment) session.commit() session.refresh(payment) - return payment + return PaymentRead.from_orm(payment) @router.delete("/{payment_id}", response_model=dict) diff --git a/app/backend/schemas/payment.py b/app/backend/schemas/payment.py index 60090432a134c7910422970073f0066d22508053..3d869a4c93d37ddc868d9c847ed10ee02a529521 100644 --- a/app/backend/schemas/payment.py +++ b/app/backend/schemas/payment.py @@ -2,7 +2,6 @@ from pydantic import BaseModel, Field, validator from datetime import datetime from backend.utils.security import ( encrypt_card_number, - mask_card_number, decrypt_card_number, ) @@ -45,18 +44,19 @@ class PaymentRead(BaseModel): id: int user_id: int payment_method: str - expiry_date: str # Keeping expiry date visible - created_at: datetime # Assuming you have a timestamp field + card_number: str # Now storing the actual card number + expiry_date: str + created_at: datetime @classmethod def from_orm(cls, obj): - """Decrypt card number and mask it before returning.""" + """Decrypt card number before returning.""" decrypted_card = decrypt_card_number(obj.card_number) return cls( id=obj.id, user_id=obj.user_id, payment_method=obj.payment_method, - masked_card_number=mask_card_number(decrypted_card), + card_number=decrypted_card, # Return actual card number expiry_date=obj.expiry_date, created_at=obj.created_at, ) diff --git a/app/backend/utils/security.py b/app/backend/utils/security.py index 9fc9aec845036c8c9c6028e55f96bee06b049a6a..f3b7cbd34cfcb16a5a6d991bf0a4bac8c6810fe4 100644 --- a/app/backend/utils/security.py +++ b/app/backend/utils/security.py @@ -26,8 +26,3 @@ def encrypt_card_number(card_number: str) -> str: def decrypt_card_number(encrypted_card: str) -> str: """Decrypts the encrypted card number.""" return fernet.decrypt(encrypted_card.encode()).decode() - - -def mask_card_number(card_number: str) -> str: - """Masks the card number, showing only the last 4 digits.""" - return f"**** **** **** {card_number[-4:]}" 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/product/view_product.py b/app/frontend/components/product/view_product.py index 5fdca7de7839395162ba535846c1ab8b59ec744c..0e84236e8e5573e69786bacef02256116106179a 100644 --- a/app/frontend/components/product/view_product.py +++ b/app/frontend/components/product/view_product.py @@ -3,16 +3,26 @@ import requests from PIL import Image, ImageTk from tkinter import messagebox import io +from typing import List, Dict, Any -BACKEND_HOST = "http://127.0.0.1:8000" # Adjust if needed +SHOPPING = "#00c1ff" +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" +BACKEND_HOST = "http://127.0.0.1:8000" def fix_url(url): """ - Fix the URL to ensure it points to the correct static file location. + If the provided URL does not start with http, assume it's a relative path. + Remove any unwanted prefix (e.g., "app/static/") and prepend the public URL. """ if url.startswith("http"): return url + # If the URL starts with "app/static/", remove that part. + prefix = "app/static/" + if url.startswith(prefix): + url = url[len(prefix):] + # Prepend the public URL return f"{BACKEND_HOST}/static/{url}" @@ -177,4 +187,310 @@ def view_shop_frame(parent, switch_func, API_URL, token, shop_id): # Display products display_products(products, products_frame) + return frame + +def view_product_frame(parent, switch_func, API_URL, token, product_data: Dict[Any, Any] = None): + """ + Create a professional product view frame similar to modern e-commerce sites. + """ + frame = ctk.CTkFrame(parent, fg_color=DARK_BG) + + # ------------- LEFT SIDEBAR ------------- + sidebar = ctk.CTkFrame(frame, fg_color=CARD_BG, width=60) + sidebar.pack(side="left", fill="y") + sidebar.pack_propagate(False) + + # Dashboard Icon + dashboard_icon = ctk.CTkButton( + sidebar, + text="🏠", + font=("Helvetica", 20), + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + width=40, + height=40, + command=lambda: switch_func("dashboard") + ) + dashboard_icon.pack(pady=(20, 10)) + + # User Icon + user_icon = ctk.CTkButton( + sidebar, + text="👤", + font=("Helvetica", 20), + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + width=40, + height=40, + command=lambda: switch_func("user_details") + ) + user_icon.pack(pady=10) + + # Cart Icon + cart_icon = ctk.CTkButton( + sidebar, + text="🛒", + font=("Helvetica", 20), + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + width=40, + height=40, + command=lambda: switch_func("user_orders") + ) + cart_icon.pack(pady=10) + + # ------------- MAIN CONTENT ------------- + main_content = ctk.CTkFrame(frame, fg_color="transparent") + main_content.pack(side="left", fill="both", expand=True) + + def refresh_data(new_product_data: Dict[Any, Any]): + """Update the frame with new product data""" + nonlocal product_data + if new_product_data: + product_data = new_product_data + update_product_display() + + def update_product_display(): + """Update all product information displays""" + if not product_data: + # Show placeholder state + main_image_label.configure(text="Select a product to view details") + product_name.configure(text="No product selected") + price_label.configure(text="₫0") + description_text.configure(state="normal") + description_text.delete("1.0", "end") + description_text.insert("1.0", "No description available.") + description_text.configure(state="disabled") + add_to_cart_btn.configure(state="disabled") + return + + try: + # Update product name + product_name.configure(text=product_data.get("name", "Product Name")) + + # Update main image + if product_images := product_data.get("images", []): + if img_url := product_images[0].get("image_url"): + fixed_url = fix_url(img_url) + try: + resp = requests.get(fixed_url) + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((400, 400)) + tk_img = ImageTk.PhotoImage(pil_img) + main_image_label.configure(image=tk_img, text="") + main_image_label.image = tk_img + + # Update thumbnails + for idx, img_data in enumerate(product_images[:4]): + if img_url := img_data.get("image_url"): + fixed_url = fix_url(img_url) + try: + resp = requests.get(fixed_url) + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((80, 80)) + tk_img = ImageTk.PhotoImage(pil_img) + thumb_labels[idx].configure(image=tk_img, text="") + thumb_labels[idx].image = tk_img + except Exception as e: + print(f"[DEBUG] Failed to load thumbnail {idx}: {e}") + thumb_labels[idx].configure(text=f"Thumb {idx+1}") + except Exception as e: + print(f"[DEBUG] Failed to load product image: {e}") + main_image_label.configure(text="Image not available") + else: + main_image_label.configure(text="No image available") + for thumb_label in thumb_labels: + thumb_label.configure(text="No image") + + # Update price + current_price = product_data.get("price", 0) + price_label.configure(text=f"₫{current_price:,.0f}") + + # Update description + description_text.configure(state="normal") + description_text.delete("1.0", "end") + description_text.insert("1.0", product_data.get("description", "No description available.")) + description_text.configure(state="disabled") + + # Enable add to cart button + add_to_cart_btn.configure(state="normal") + + except Exception as e: + messagebox.showerror("Error", f"Failed to update product display: {str(e)}") + + # Main container with two columns + main_container = ctk.CTkFrame(main_content, fg_color="transparent") + main_container.pack(fill="both", expand=True, padx=20, pady=20) + + # Left column for images + left_column = ctk.CTkFrame(main_container, fg_color="transparent") + left_column.pack(side="left", fill="both", expand=True) + + # Main image display + main_image_frame = ctk.CTkFrame(left_column, fg_color=CARD_BG, corner_radius=10) + main_image_frame.pack(fill="both", expand=True, padx=10, pady=10) + + main_image_label = ctk.CTkLabel(main_image_frame, text="Select a product to view details") + main_image_label.pack(padx=20, pady=20) + + # Thumbnail container + thumbnail_container = ctk.CTkFrame(left_column, fg_color="transparent") + thumbnail_container.pack(fill="x", pady=10) + + # Create thumbnail frames and labels + thumb_labels = [] + for i in range(4): + thumb_frame = ctk.CTkFrame(thumbnail_container, fg_color=CARD_BG, corner_radius=5) + thumb_frame.pack(side="left", padx=5) + thumb_label = ctk.CTkLabel(thumb_frame, text=f"Thumb {i+1}") + thumb_label.pack(padx=5, pady=5) + thumb_labels.append(thumb_label) + + # Right column for product details + right_column = ctk.CTkFrame(main_container, fg_color="transparent") + right_column.pack(side="right", fill="both", expand=True, padx=20) + + # Product name + product_name = ctk.CTkLabel( + right_column, + text="No product selected", + font=("Helvetica", 24, "bold"), + justify="left", + wraplength=400 + ) + product_name.pack(anchor="w", pady=(0, 10)) + + # Price + price_frame = ctk.CTkFrame(right_column, fg_color="transparent") + price_frame.pack(fill="x", pady=10) + + price_label = ctk.CTkLabel( + price_frame, + text="₫0", + font=("Helvetica", 24, "bold"), + text_color="#ff4242" + ) + price_label.pack(side="left") + + # Quantity selector + quantity_frame = ctk.CTkFrame(right_column, fg_color="transparent") + quantity_frame.pack(fill="x", pady=20) + + quantity_label = ctk.CTkLabel( + quantity_frame, + text="Quantity:", + font=("Helvetica", 16, "bold") + ) + quantity_label.pack(anchor="w", pady=(0, 10)) + + quantity_selector = ctk.CTkFrame(quantity_frame, fg_color="#2b2b2b") + quantity_selector.pack(side="left") + + quantity_var = ctk.IntVar(value=1) + + def update_quantity(delta): + new_val = quantity_var.get() + delta + if 1 <= new_val <= 99: + quantity_var.set(new_val) + + minus_btn = ctk.CTkButton( + quantity_selector, + text="-", + width=30, + command=lambda: update_quantity(-1) + ) + minus_btn.pack(side="left", padx=5, pady=5) + + quantity_entry = ctk.CTkEntry( + quantity_selector, + textvariable=quantity_var, + width=50, + justify="center" + ) + quantity_entry.pack(side="left", padx=5, pady=5) + + plus_btn = ctk.CTkButton( + quantity_selector, + text="+", + width=30, + command=lambda: update_quantity(1) + ) + plus_btn.pack(side="left", padx=5, pady=5) + + def add_to_cart(): + if not product_data: + messagebox.showwarning("Warning", "No product selected") + return + + try: + # Validate quantity + quantity = quantity_var.get() + if not (1 <= quantity <= 99): + messagebox.showwarning("Warning", "Please select a valid quantity (1-99).") + return + + # Prepare data for API call + headers = {"Authorization": f"Bearer {token}"} + data = { + "product_id": product_data.get("id"), + "quantity": quantity + } + + # Make API call + response = requests.post(f"{API_URL}/cart/add", headers=headers, json=data) + + if response.status_code == 200: + messagebox.showinfo("Success", "Product added to cart successfully!") + # Optionally switch to cart view + switch_func("user_orders") + else: + error_msg = response.json().get("detail", "Unknown error occurred") + messagebox.showerror("Error", f"Failed to add to cart: {error_msg}") + + except requests.RequestException as e: + messagebox.showerror("Error", f"Network error: {str(e)}") + except Exception as e: + messagebox.showerror("Error", f"An unexpected error occurred: {str(e)}") + + # Add to cart button + add_to_cart_btn = ctk.CTkButton( + right_column, + text="Add to Cart", + font=("Helvetica", 16, "bold"), + height=45, + fg_color="#ff4242", + hover_color="#ff6b6b", + command=add_to_cart, + state="disabled" # Initially disabled + ) + add_to_cart_btn.pack(fill="x", pady=20) + + # Product description + description_label = ctk.CTkLabel( + right_column, + text="Product Description", + font=("Helvetica", 16, "bold") + ) + description_label.pack(anchor="w", pady=(20, 10)) + + description_text = ctk.CTkTextbox( + right_column, + height=100, + wrap="word", + font=("Helvetica", 12) + ) + description_text.pack(fill="x") + description_text.insert("1.0", "No description available.") + description_text.configure(state="disabled") + + # Set frame attributes + frame.refresh_data = refresh_data + + # Initial display update if product data is available + if product_data: + update_product_display() + return frame \ No newline at end of file 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..0573ad380b4ac90729b5a79b3d018932fdb7d432 --- /dev/null +++ b/app/frontend/components/user_payments.py @@ -0,0 +1,788 @@ +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) + + # Create a scrollable frame for the content + content_scrollable = ctk.CTkScrollableFrame( + content_frame, + fg_color="transparent", + scrollbar_button_color=SHOPPING, + scrollbar_button_hover_color="#0096ff" + ) + content_scrollable.pack(fill="both", expand=True) + + # Header + header_frame = ctk.CTkFrame(content_scrollable, 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_scrollable, fg_color="transparent") + main_content.pack(fill="both", expand=True, padx=30, pady=(0, 20)) + + # Saved Payment Methods Section + saved_payments_frame = ctk.CTkFrame(main_content, fg_color="transparent") + saved_payments_frame.pack(fill="x", pady=(0, 40)) + + saved_payments_label = ctk.CTkLabel( + saved_payments_frame, + text="Current Payment Method", + font=("Helvetica", 18, "bold"), + text_color="white", + ) + saved_payments_label.pack(pady=(0, 20)) + + # Container for card and buttons with fixed width for proper centering + saved_card_section = ctk.CTkFrame(saved_payments_frame, fg_color="transparent", width=600, height=220) + saved_card_section.pack(expand=True) + saved_card_section.pack_propagate(False) + saved_card_section.grid_columnconfigure(0, weight=1) + saved_card_section.grid_columnconfigure(1, weight=0) + + # Container for the saved payment card + saved_card_container = ctk.CTkFrame(saved_card_section, fg_color="transparent") + saved_card_container.grid(row=0, column=0, sticky="e", padx=(0, 20)) + + # Create the saved card frame (initially empty) + saved_card_frame = None + + # Buttons container + buttons_container = ctk.CTkFrame(saved_card_section, fg_color="transparent") + buttons_container.grid(row=0, column=1, sticky="w", pady=(60, 0)) + + def create_payment_card(payment): + """Create or update the saved payment card display""" + nonlocal saved_card_frame + + # Clear buttons container first + for widget in buttons_container.winfo_children(): + widget.destroy() + + # Remove existing card if any + if saved_card_frame is not None: + saved_card_frame.destroy() + saved_card_frame = None + + # If no payment data, don't create a new card + if not payment: + return + + # Create new card frame + saved_card_frame = ctk.CTkFrame( + saved_card_container, + corner_radius=15, + height=200, + width=350 + ) + saved_card_frame.pack(anchor="e") + saved_card_frame.pack_propagate(False) + + # Set card color based on payment method + card_type = payment['payment_method'].upper() + if card_type == "VISA": + saved_card_frame.configure(fg_color="#0066cc") + elif card_type == "MASTERCARD": + saved_card_frame.configure(fg_color="#EB001B") + elif card_type == "PAYPAL": + saved_card_frame.configure(fg_color="#003087") + + # Chip image + chip_frame = ctk.CTkFrame( + saved_card_frame, + width=50, + height=40, + fg_color="#FFD700", + corner_radius=5 + ) + chip_frame.place(x=30, y=50) + + # Card type label + card_type_label = ctk.CTkLabel( + saved_card_frame, + text=card_type, + font=("Helvetica", 20, "bold"), + text_color="white" + ) + card_type_label.place(x=270, y=20) + + # Format card number with spaces + raw_number = payment['card_number'] + # Mask first 12 digits and show last 4 + masked_number = '**** **** **** ' + raw_number[-4:] if len(raw_number) >= 4 else raw_number + formatted_number = masked_number + + # Card number + card_number = ctk.CTkLabel( + saved_card_frame, + text=formatted_number, + font=("Courier", 24, "bold"), + text_color="white" + ) + card_number.place(x=30, y=100) + + # Expiry date label + expiry_label = ctk.CTkLabel( + saved_card_frame, + text="VALID\nTHRU", + font=("Helvetica", 8), + text_color="white" + ) + expiry_label.place(x=30, y=140) + + # Expiry date + expiry_date = ctk.CTkLabel( + saved_card_frame, + text=payment['expiry_date'], + font=("Helvetica", 14, "bold"), + text_color="white" + ) + expiry_date.place(x=30, y=165) + + # Cardholder name + cardholder_name = ctk.CTkLabel( + saved_card_frame, + text="CARDHOLDER NAME", + font=("Helvetica", 14, "bold"), + text_color="white" + ) + cardholder_name.place(x=150, y=165) + + # Add buttons with improved styling + edit_button = ctk.CTkButton( + buttons_container, + text="Edit", + width=120, + height=38, + corner_radius=20, + command=lambda: edit_payment(payment), + fg_color="#00B2FF", + hover_color="#0095D9", + font=("Helvetica", 14, "bold"), + border_width=2, + border_color="#80D9FF" + ) + edit_button.pack(pady=(0, 15)) + + # Delete button with matching style + delete_button = ctk.CTkButton( + buttons_container, + text="Delete", + width=120, + height=38, + corner_radius=20, + command=lambda: delete_payment(payment), + fg_color="#FF4444", + hover_color="#D93939", + font=("Helvetica", 14, "bold"), + border_width=2, + border_color="#FFA4A4" + ) + delete_button.pack() + + def disable_form_inputs(): + """Disable all form inputs""" + card_entry.configure(state="disabled") + month_entry.configure(state="disabled") + year_entry.configure(state="disabled") + cvv_entry.configure(state="disabled") + card_type_dropdown.configure(state="disabled") + save_button.configure(state="disabled") + + def enable_form_inputs(): + """Enable all form inputs""" + card_entry.configure(state="normal") + month_entry.configure(state="normal") + year_entry.configure(state="normal") + cvv_entry.configure(state="normal") + card_type_dropdown.configure(state="normal") + save_button.configure(state="normal") + + def clear_form(): + """Clear all form fields and reset to default state""" + 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") + if hasattr(frame, 'current_payment_id'): + delattr(frame, 'current_payment_id') + update_card_preview() + # Enable form inputs when clearing + enable_form_inputs() + + def edit_payment(payment): + """Populate form with payment data for editing""" + nonlocal saved_card_frame + + try: + # Enable form inputs for editing + enable_form_inputs() + + # Store payment ID for update + frame.current_payment_id = payment['id'] + + # Populate form with existing payment data + card_entry.delete(0, "end") + card_entry.insert(0, payment['card_number']) + + # Set card type (capitalize first letter) + card_type = payment['payment_method'].capitalize() + card_type_var.set(card_type) + + # Set expiry date + month, year = payment['expiry_date'].split('/') + month_entry.delete(0, "end") + month_entry.insert(0, month) + year_entry.delete(0, "end") + year_entry.insert(0, year) + + # Update both previews + update_card_preview() + + # Create a temporary payment object for the saved card preview + temp_payment = { + 'id': payment['id'], + 'payment_method': card_type, + 'card_number': payment['card_number'], + 'expiry_date': payment['expiry_date'] + } + create_payment_card(temp_payment) + except Exception as e: + messagebox.showerror("Error", f"Failed to load payment data: {str(e)}") + + def delete_payment(payment): + """Delete payment method and update UI""" + nonlocal saved_card_frame + + if messagebox.askyesno("Confirm Delete", "Are you sure you want to delete this payment method?"): + try: + headers = {"Authorization": f"Bearer {frame.token}"} + response = requests.delete( + f"{API_URL}/payment/{payment['id']}", + headers=headers + ) + + if response.status_code == 200: + # Clear UI elements + if saved_card_frame is not None: + saved_card_frame.destroy() + saved_card_frame = None + + # Clear buttons container + for widget in buttons_container.winfo_children(): + widget.destroy() + + # Clear form and reset state + clear_form() + + # Remove the current_payment_id if it exists + if hasattr(frame, 'current_payment_id'): + delattr(frame, 'current_payment_id') + + messagebox.showinfo("Success", "Payment method deleted successfully") + + # Enable form inputs after successful delete + enable_form_inputs() + + # Refresh the UI + refresh_payments() + else: + error_msg = response.json().get("detail", "Failed to delete payment method") + messagebox.showerror("Error", error_msg) + except Exception as e: + messagebox.showerror("Error", f"Failed to delete payment method: {str(e)}") + + def refresh_payments(): + """Fetch and display the current payment method""" + nonlocal saved_card_frame + + try: + headers = {"Authorization": f"Bearer {frame.token}"} + response = requests.get(f"{API_URL}/payment/user", headers=headers) + + if response.status_code == 200: + payments = response.json() + # Clear existing card if any + if saved_card_frame is not None: + saved_card_frame.destroy() + saved_card_frame = None + + # Clear buttons container + for widget in buttons_container.winfo_children(): + widget.destroy() + + if payments and len(payments) > 0: + # Display the first/current payment method + create_payment_card(payments[0]) + # Disable form inputs when payment exists + disable_form_inputs() + else: + # If no payment methods, clear everything and enable form + clear_form() + enable_form_inputs() + else: + messagebox.showerror("Error", "Failed to fetch payment method") + except Exception as e: + messagebox.showerror("Error", f"Failed to fetch payment method: {str(e)}") + + # Payment Form - Move this section up before it's used + 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") + + # Card Preview Section + preview_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + preview_frame.pack(fill="x", pady=(20, 10)) + + preview_label = ctk.CTkLabel( + preview_frame, + text="Card Preview", + font=("Helvetica", 14), + text_color="white" + ) + preview_label.pack(anchor="center") + + # Card Preview Frame - Centered + preview_container = ctk.CTkFrame(preview_frame, fg_color="transparent") + preview_container.pack(fill="x", pady=(0, 20)) + + card_preview = ctk.CTkFrame( + preview_container, + fg_color="#0066cc", + corner_radius=15, + height=200, + width=350 + ) + card_preview.pack(anchor="center") + card_preview.pack_propagate(False) + + # Chip image (represented as a small rectangle) + chip_frame = ctk.CTkFrame( + card_preview, + width=50, + height=40, + fg_color="#FFD700", + corner_radius=5 + ) + chip_frame.place(x=30, y=50) + + # Card type label (top right) + card_type_preview = ctk.CTkLabel( + card_preview, + text="VISA", + font=("Helvetica", 20, "bold"), + text_color="white" + ) + card_type_preview.place(x=270, y=20) + + # Card number preview + card_number_preview = ctk.CTkLabel( + card_preview, + text="**** **** **** ****", + font=("Courier", 24, "bold"), + text_color="white" + ) + card_number_preview.place(x=30, y=100) + + # Expiry date preview + expiry_label = ctk.CTkLabel( + card_preview, + text="VALID\nTHRU", + font=("Helvetica", 8), + text_color="white" + ) + expiry_label.place(x=30, y=140) + + expiry_preview = ctk.CTkLabel( + card_preview, + text="MM/YY", + font=("Helvetica", 14, "bold"), + text_color="white" + ) + expiry_preview.place(x=30, y=165) + + # Cardholder name preview + cardholder_preview = ctk.CTkLabel( + card_preview, + text="CARDHOLDER NAME", + font=("Helvetica", 14, "bold"), + text_color="white" + ) + cardholder_preview.place(x=150, y=165) + + 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") + + def update_card_preview(event=None): + # Update card number + card_num = card_entry.get().strip() + if card_num: + # Format card number with spaces and mask first 12 digits + masked_num = '**** **** **** ' + card_num[-4:] if len(card_num) >= 4 else card_num + card_number_preview.configure(text=masked_num) + else: + card_number_preview.configure(text="**** **** **** ****") + + # Update expiry date + month = month_entry.get().strip() + year = year_entry.get().strip() + if month or year: + expiry_preview.configure(text=f"{month.zfill(2)}/{year.zfill(2)}") + else: + expiry_preview.configure(text="MM/YY") + + # Update card type and color + card_type = card_type_var.get().upper() + card_type_preview.configure(text=card_type) + + # Update card color based on type + if card_type == "VISA": + card_preview.configure(fg_color="#0066cc") + elif card_type == "MASTERCARD": + card_preview.configure(fg_color="#EB001B") + elif card_type == "PAYPAL": + card_preview.configure(fg_color="#003087") + + # If we're in edit mode, update the saved card preview as well + if hasattr(frame, 'current_payment_id'): + temp_payment = { + 'payment_method': card_type_var.get(), + 'card_number': card_num, + 'expiry_date': f"{month.zfill(2)}/{year.zfill(2)}" if month and year else "MM/YY" + } + create_payment_card(temp_payment) + + # 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) + + # Bind the update function to all relevant widgets + card_entry.bind("<KeyRelease>", update_card_preview) + month_entry.bind("<KeyRelease>", update_card_preview) + year_entry.bind("<KeyRelease>", update_card_preview) + card_type_dropdown.bind("<KeyRelease>", update_card_preview) + + def save_payment(): + """Save or update payment method""" + nonlocal saved_card_frame + + # 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() + + 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 + } + + try: + headers = {"Authorization": f"Bearer {frame.token}"} + + if hasattr(frame, 'current_payment_id'): + # Update existing payment + response = requests.put( + f"{API_URL}/payment/{frame.current_payment_id}", + headers=headers, + json=payload + ) + action = "updated" + else: + # Add new payment + response = requests.post( + f"{API_URL}/payment/add", + headers=headers, + json=payload + ) + action = "added" + + if response.status_code == 200: + messagebox.showinfo("Success", f"Payment method {action} successfully!") + # Clear form and reset state + clear_form() + # Remove the current_payment_id if it exists + if hasattr(frame, 'current_payment_id'): + delattr(frame, 'current_payment_id') + # Refresh to show updated data + refresh_payments() + # Disable form inputs after successful save + disable_form_inputs() + else: + error_msg = response.json().get("detail", f"Failed to {action} payment method") + messagebox.showerror("Error", error_msg) + except Exception as e: + messagebox.showerror("Error", f"Failed to save payment method: {str(e)}") + + # 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)) + + # Initial refresh of payment methods + refresh_payments() + + return frame diff --git a/app/frontend/main.py b/app/frontend/main.py index e53b707183752a43c62a1a7a4aad15a87447e778..6a4b8328b1463ff7990d6bf5eba07ecc5a318bb8 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -4,10 +4,12 @@ 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 create_product_frame +from components.product.view_product import view_product_frame 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 +23,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) @@ -51,21 +55,25 @@ create_shop = create_shop_frame( # Fix the parameter order for view_shop_frame view_shop = view_shop_frame(root, switch_frame, API_URL, access_token, None) # Pass shop_id as None initially product = create_product_frame(root, switch_frame, API_URL, access_token) +view_product = view_product_frame(root, switch_frame, API_URL, access_token, None) 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, "register": register, "create_shop": create_shop, "create_product": product, + "view_product": view_product, "category": category, "view_shop": view_shop, "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)}