diff --git a/app/backend/main.py b/app/backend/main.py index 658f0cadb39adc6136f2dbdb2bd3ab70be22cf8e..9606ccc22b982a4ee3f358b37390df63fc26d397 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -4,13 +4,20 @@ import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from backend.routes import auth, shop, product, category, search, order from backend.database import init_db from core.config import settings app = FastAPI(title="Shopping App", version="1.0.0", debug=settings.debug) -# initialize database +# ------------------- NEW: MOUNT STATIC FILES ------------------- +# Suppose your static files are located in "app/static" +# Adjust the path if needed. +static_dir_path = os.path.join(os.path.dirname(__file__), "..", "static") +app.mount("/static", StaticFiles(directory=static_dir_path), name="static") + +# Initialize database init_db() # Include API routes diff --git a/app/backend/routes/product.py b/app/backend/routes/product.py index fcad951db6c8c2d2cf4b7727a2c555599cb95b6b..30ca0e9f308fcd5d627ec95dbcaa7db6407bba75 100644 --- a/app/backend/routes/product.py +++ b/app/backend/routes/product.py @@ -11,9 +11,7 @@ import os router = APIRouter() -static_dir = os.path.join("app", "static") -os.makedirs(static_dir, exist_ok=True) - +BACKEND_HOST = "http://127.0.0.1:8000" # Or use settings.backend_url if available @router.post("/create", response_model=ProductRead) def create_product( @@ -27,6 +25,12 @@ def create_product( session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): + shop = session.get(Shop, shop_id) + if not shop or shop.owner_id != current_user.id: + raise HTTPException( + status_code=403, detail="Unauthorized to create product for this shop" + ) + product = Product( name=name, description=description, @@ -40,35 +44,31 @@ def create_product( session.commit() session.refresh(product) - shop = session.get(Shop, shop_id) - if not shop or shop.owner_id != current_user.id: - raise HTTPException( - status_code=403, detail="Unauthorized to create product for this shop" - ) - - # Directory structure: static/shop_{shop_name}/product_{product_name}/ + # Directory: static/shop_{shop.name}/product_{product.name}/ shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) product_dir = os.path.join(shop_dir, f"product_{product.name}") os.makedirs(product_dir, exist_ok=True) - # Handling multiple image uploads if images: for image in images: if image.filename: file_location = os.path.join(product_dir, image.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(image.file, buffer) - - # Save image record in ProductImage - product_image = ProductImage( - product_id=product.id, image_url=file_location - ) + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + product_image = ProductImage(product_id=product.id, image_url=public_url) session.add(product_image) session.commit() - else: # Save default image - file_location = os.path.join(settings.static_dir, "default/default_product.png") - product_image = ProductImage(product_id=product.id, image_url=file_location) + else: + # Use default product image from static/default folder + default_path = os.path.join(settings.static_dir, "default", "default_product.png") + relative_path = os.path.relpath(default_path, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + product_image = ProductImage(product_id=product.id, image_url=public_url) session.add(product_image) session.commit() @@ -82,7 +82,6 @@ def read_all_products(order: str = "desc", session: Session = Depends(get_sessio if order == "desc" else func.count(OrderItem.id).asc() ) - products = ( session.query(Product) .outerjoin(OrderItem, Product.id == OrderItem.product_id) @@ -90,7 +89,6 @@ def read_all_products(order: str = "desc", session: Session = Depends(get_sessio .order_by(order_by) .all() ) - return products @@ -120,28 +118,16 @@ def update_product( status_code=403, detail="Unauthorized to update this product" ) - if name: - product.name = name - if description: - product.description = description - if price is not None: - product.price = price - if stock is not None: - product.stock = stock - if category_id is not None: - product.category_id = category_id + product.name = name + product.description = description + product.price = price + product.stock = stock + product.category_id = category_id session.add(product) session.commit() session.refresh(product) - product = session.get(Product, product_id) - if not product or product.shop.owner_id != current_user.id: - raise HTTPException( - status_code=403, detail="Unauthorized to update this product" - ) - - # Directory structure: static/shop_{shop_name}/product_{product_name}/ shop = session.get(Shop, product.shop_id) shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) @@ -154,8 +140,10 @@ def update_product( file_location = os.path.join(product_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - - image = ProductImage(product_id=product.id, image_url=file_location) + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + image = ProductImage(product_id=product.id, image_url=public_url) session.add(image) session.commit() @@ -173,7 +161,6 @@ def delete_product( raise HTTPException( status_code=403, detail="Unauthorized to delete this product" ) - session.delete(product) session.commit() return {"message": "Product deleted successfully"} diff --git a/app/backend/routes/shop.py b/app/backend/routes/shop.py index a240d1be12ee0774988de102a631da60042acbc5..d5a651ae41d8c67d3f84b711e2e270707f83e5a7 100644 --- a/app/backend/routes/shop.py +++ b/app/backend/routes/shop.py @@ -11,6 +11,8 @@ import os router = APIRouter() +# Define your backend host URL. +BACKEND_HOST = "http://127.0.0.1:8000" # Or use settings.backend_url if available @router.post("/create", response_model=ShopRead) def create_shop( @@ -39,6 +41,7 @@ def create_shop( session.commit() session.refresh(shop) + # Create a folder in the static directory for the shop shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) @@ -46,13 +49,20 @@ def create_shop( file_location = os.path.join(shop_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - shop.image_url = file_location + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url else: - shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png") + # Use default shop image from static/default folder + default_path = os.path.join(settings.static_dir, "default", "default_shop.png") + relative_path = os.path.relpath(default_path, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url session.commit() session.refresh(shop) - return shop @@ -61,7 +71,6 @@ def get_all_shops(order: str = "desc", session: Session = Depends(get_session)): order_by = ( func.count(Order.id).desc() if order == "desc" else func.count(Order.id).asc() ) - shops = ( session.query(Shop) .outerjoin(Order, Shop.id == Order.shop_id) @@ -69,7 +78,6 @@ def get_all_shops(order: str = "desc", session: Session = Depends(get_session)): .order_by(order_by) .all() ) - return shops @@ -94,10 +102,9 @@ def update_shop( shop = session.get(Shop, shop_id) if not shop: raise HTTPException(status_code=404, detail="Shop not found") - - # Ensure the current user is the shop owner if shop.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Unauthorized to update this shop") + if name: shop.name = name if description: @@ -118,14 +125,20 @@ def update_shop( file_location = os.path.join(shop_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - shop.image_url = file_location + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url else: - shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png") + default_path = os.path.join(settings.static_dir, "default", "default_shop.png") + relative_path = os.path.relpath(default_path, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url session.add(shop) session.commit() session.refresh(shop) - return shop @@ -138,11 +151,8 @@ def delete_shop( shop = session.get(Shop, shop_id) if not shop: raise HTTPException(status_code=404, detail="Shop not found") - - # Ensure the current user is the shop owner if shop.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Unauthorized to delete this shop") - session.delete(shop) session.commit() return {"message": "Shop deleted successfully"} diff --git a/app/frontend/components/auth/login.py b/app/frontend/components/auth/login.py index dc2d30d1dbcd871981f32b94b033c0a3fb92a4d3..31d2560c45f5d7f2890cbcdc6d35b9c98eed19a4 100644 --- a/app/frontend/components/auth/login.py +++ b/app/frontend/components/auth/login.py @@ -15,7 +15,7 @@ def login_frame(parent, switch_func, API_URL): left_frame = ctk.CTkFrame(container, fg_color="transparent") left_frame.grid(row=0, column=0, sticky="nsew") - image_path = "app/static/front_end_img/login.jpg" + image_path = "../../app/static/front_end_img/login.jpg" img = ctk.CTkImage(light_image=Image.open(image_path), size=(800, 800)) image_label = ctk.CTkLabel(left_frame, image=img, text="") image_label.place(relwidth=1, relheight=1) diff --git a/app/frontend/components/dashboard.py b/app/frontend/components/dashboard.py index f6c1af427f1737be2f1b63cffcfaf6bd66ccc972..f5707c926cb655e12186dcfcccba5ebafc3a82b6 100644 --- a/app/frontend/components/dashboard.py +++ b/app/frontend/components/dashboard.py @@ -5,10 +5,29 @@ from tkinter import messagebox import io SHOPPING = "#00c1ff" - +BACKEND_HOST = "http://127.0.0.1:8000" # Adjust if needed + +def fix_url(url): + """ + 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}" def dashboard_frame(parent, switch_func, API_URL, token): - # Main container frame + """ + Main dashboard UI: + - Header (top bar) with logo, search, user, cart + - Middle area with horizontal scroll of top shops + - Bottom area with grid layout of recommended products + """ frame = ctk.CTkFrame(parent, fg_color="transparent") # ------------- HEADER (Top Bar) ------------- @@ -38,16 +57,16 @@ def dashboard_frame(parent, switch_func, API_URL, token): return try: headers = {"Authorization": f"Bearer {token}"} - resp = requests.get(f"{API_URL}/product?search={keyword}", headers=headers) + # Adjust endpoint/params as needed + resp = requests.get(f"{API_URL}/search?name={keyword}&search_type=products", headers=headers) if resp.status_code == 200: products = resp.json() display_products(products, bottom_products_frame) else: - messagebox.showerror("Error", "Failed to fetch search results.") + messagebox.showerror("Error", f"Search failed. Status code: {resp.status_code}") except Exception as e: messagebox.showerror("Error", f"Request error: {e}") - # Search button search_button = ctk.CTkButton( header_frame, text="Search", @@ -59,89 +78,80 @@ def dashboard_frame(parent, switch_func, API_URL, token): # ------------- 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("user_orders") - # 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, - ) switch_func("user_orders") - 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) + + 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) + + 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) - # Section Title featured_label = ctk.CTkLabel( middle_frame, text="TOP SHOP", font=("Helvetica", 16, "bold") ) featured_label.pack(pady=5) - # A frame to hold the featured shops - top_shops_frame = ctk.CTkFrame(middle_frame, fg_color="transparent") + top_shops_frame = ctk.CTkScrollableFrame( + middle_frame, fg_color="transparent", width=750, height=150 + ) top_shops_frame.pack(fill="both", expand=True, padx=10, pady=5) + def create_shop_card(parent, shop_data): + """ + Create a card for a shop with its image on top and name below. + """ + card = ctk.CTkFrame(parent, corner_radius=5, fg_color="#2b2b2b", width=100, height=130) + card.pack_propagate(False) + + image_label = ctk.CTkLabel(card, text="") + image_label.pack(pady=5) + + # Use "image_url" as returned by the backend, then fix it if needed. + image_url = shop_data.get("image_url") + if image_url: + fixed_url = fix_url(image_url) + print(f"[DEBUG] Fetching shop image from: {fixed_url}") + try: + resp = requests.get(fixed_url) + print(f"[DEBUG] Status code for shop image: {resp.status_code}") + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((60, 60)) + tk_img = ImageTk.PhotoImage(pil_img) + image_label.configure(image=tk_img, text="") + image_label.image = tk_img + else: + print(f"[DEBUG] Failed to fetch shop image. Status: {resp.status_code}") + except Exception as e: + print(f"[DEBUG] Shop image error: {e}") + else: + print(f"[DEBUG] No 'image_url' found for shop: {shop_data.get('name')}") + + name_text = shop_data.get("name", "No Name") + name_label = ctk.CTkLabel(card, text=name_text, font=("Helvetica", 11, "bold"), wraplength=90) + name_label.pack() + + return card + def display_shops(shops, container): - """Given a list of shop dicts, display them in the given container.""" - # Clear old widgets for widget in container.winfo_children(): widget.destroy() @@ -149,59 +159,68 @@ def dashboard_frame(parent, switch_func, API_URL, token): ctk.CTkLabel(container, text="No shops found.").pack(pady=10) return - # Display shops in a vertical list - for shop in shops: - sframe = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b") - sframe.pack(fill="x", padx=5, pady=5) + inner_frame = ctk.CTkFrame(container, fg_color="transparent") + inner_frame.pack(side="top", fill="both", expand=True) - # Shop image/logo on left - image_label = ctk.CTkLabel(sframe, text="") - image_label.pack(side="left", padx=5, pady=5) + for shop in shops: + shop_card = create_shop_card(inner_frame, shop) + shop_card.pack(side="left", padx=5, pady=5) - # Load shop logo if available (assumes key "logo_url") - logo_url = shop.get("logo_url") - if logo_url: - try: - resp = requests.get(logo_url) - if resp.status_code == 200: - pil_img = Image.open(io.BytesIO(resp.content)).resize((50, 50)) - tk_img = ImageTk.PhotoImage(pil_img) - image_label.configure(image=tk_img, text="") - image_label.image = tk_img - except Exception as e: - print(f"Shop image error: {e}") - - # Shop info on right - info_frame = ctk.CTkFrame(sframe, fg_color="transparent") - info_frame.pack(side="left", fill="both", expand=True, padx=10) - - ctk.CTkLabel( - info_frame, - text=shop.get("name", "No Name"), - font=("Helvetica", 13, "bold"), - ).pack(anchor="w") - # Optionally add more shop details, for example a rating if available: - if shop.get("rating"): - ctk.CTkLabel(info_frame, text=f"Rating: {shop['rating']}").pack( - anchor="w" - ) - - # ------------- BOTTOM (Recommendations - Products) ------------- + # ------------- BOTTOM (Products) ------------- bottom_frame = ctk.CTkFrame(frame, fg_color="transparent") bottom_frame.place(relx=0, rely=0.35, relwidth=1, relheight=0.65) - # Section Title recommend_label = ctk.CTkLabel( bottom_frame, text="TODAY'S RECOMMENDATIONS", font=("Helvetica", 16, "bold") ) recommend_label.pack(pady=5) - bottom_products_frame = ctk.CTkFrame(bottom_frame, fg_color="transparent") + bottom_products_frame = ctk.CTkScrollableFrame( + bottom_frame, fg_color="transparent", width=750, height=300 + ) bottom_products_frame.pack(fill="both", expand=True, padx=10, pady=5) + def create_product_card(parent, product_data): + card_width = 130 + card_height = 210 + card = ctk.CTkFrame(parent, corner_radius=5, fg_color="#2b2b2b", width=card_width, height=card_height) + card.pack_propagate(False) + + image_label = ctk.CTkLabel(card, text="") + image_label.pack(pady=5) + + product_images = product_data.get("images", []) + if product_images: + img_url = product_images[0].get("image_url") + if img_url: + fixed_url = fix_url(img_url) + print(f"[DEBUG] Fetching product image from: {fixed_url}") + try: + resp = requests.get(fixed_url) + print(f"[DEBUG] Status code for product image: {resp.status_code}") + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((70, 70)) + tk_img = ImageTk.PhotoImage(pil_img) + image_label.configure(image=tk_img, text="") + image_label.image = tk_img + else: + print(f"[DEBUG] Failed to fetch product image. Status: {resp.status_code}") + except Exception as e: + print(f"[DEBUG] Product image error: {e}") + else: + print(f"[DEBUG] No images found for product: {product_data.get('name')}") + + name_text = product_data.get("name", "No Name") + name_label = ctk.CTkLabel(card, text=name_text, font=("Helvetica", 11, "bold"), wraplength=120, anchor="center", justify="center") + name_label.pack(padx=5, pady=3) + + price_val = product_data.get("price", 0.0) + price_label = ctk.CTkLabel(card, text=f"₫ {price_val:,.1f}", font=("Helvetica", 10, "bold"), text_color="#ff4242") + price_label.pack(side="bottom", pady=5) + + return card + def display_products(products, container): - """Given a list of product dicts, display them in the given container.""" - # Clear old widgets for widget in container.winfo_children(): widget.destroy() @@ -209,64 +228,37 @@ def dashboard_frame(parent, switch_func, API_URL, token): ctk.CTkLabel(container, text="No products found.").pack(pady=10) return - # Display products in a vertical list - for product in products: - pframe = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b") - pframe.pack(fill="x", padx=5, pady=5) + grid_frame = ctk.CTkFrame(container, fg_color="transparent") + grid_frame.pack(fill="both", expand=True) - # Product image on left - image_label = ctk.CTkLabel(pframe, text="") - image_label.pack(side="left", padx=5, pady=5) - - # Load first image if available - if product.get("images"): - try: - img_url = product["images"][0]["image_url"] - resp = requests.get(img_url) - if resp.status_code == 200: - pil_img = Image.open(io.BytesIO(resp.content)).resize((50, 50)) - tk_img = ImageTk.PhotoImage(pil_img) - image_label.configure(image=tk_img, text="") - image_label.image = tk_img - except Exception as e: - print(f"Product image error: {e}") - - # Product info on right - info_frame = ctk.CTkFrame(pframe, fg_color="transparent") - info_frame.pack(side="left", fill="both", expand=True, padx=10) - - ctk.CTkLabel( - info_frame, - text=product.get("name", "No Name"), - font=("Helvetica", 13, "bold"), - ).pack(anchor="w") - price = product.get("price", 0.0) - ctk.CTkLabel(info_frame, text=f"Price: {price:.2f}").pack(anchor="w") + columns = 7 # 7 products per row + for idx, product in enumerate(products): + row = idx // columns + col = idx % columns + product_card = create_product_card(grid_frame, product) + product_card.grid(row=row, column=col, padx=5, pady=5) def fetch_featured_shops(): - """Fetch some 'featured' or 'top' shops for the middle section.""" headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get(f"{API_URL}/shops/list", headers=headers) if resp.status_code == 200: shops = resp.json() - # Slice the list to display only a handful of top shops display_shops(shops[:5], top_shops_frame) else: - messagebox.showerror("Error", "Failed to fetch featured shops.") + messagebox.showerror("Error", f"Failed to fetch featured shops. Status: {resp.status_code}") except Exception as e: messagebox.showerror("Error", f"Request error: {e}") def fetch_recommendations(): - """Fetch recommended products for the bottom section.""" headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get(f"{API_URL}/product/list", headers=headers) if resp.status_code == 200: products = resp.json() - display_products(products, bottom_products_frame) + display_products(products[:20], bottom_products_frame) else: - messagebox.showerror("Error", "Failed to fetch recommended products.") + messagebox.showerror("Error", f"Failed to fetch recommended products. Status: {resp.status_code}") except Exception as e: messagebox.showerror("Error", f"Request error: {e}")