From ec918660ea4b659275986ba552e4e2b20d4874ba Mon Sep 17 00:00:00 2001 From: nn2-minh <Nguyen12.Minh@live.uwe.ac.uk> Date: Sun, 27 Apr 2025 15:11:15 +0700 Subject: [PATCH] add statistics view for admin --- app/backend/routes/admin.py | 170 +++- app/frontend/components/admin/__init__.py | 3 +- app/frontend/components/admin/dashboard.py | 10 +- .../components/admin/system_statistics.py | 733 ++++++++++++++++++ app/frontend/main.py | 4 + 5 files changed, 910 insertions(+), 10 deletions(-) create mode 100644 app/frontend/components/admin/system_statistics.py diff --git a/app/backend/routes/admin.py b/app/backend/routes/admin.py index 698f454..54e5a3d 100644 --- a/app/backend/routes/admin.py +++ b/app/backend/routes/admin.py @@ -1,9 +1,10 @@ from fastapi import APIRouter, Depends, HTTPException -from sqlmodel import Session, select -from app.backend.models.models import User, Shop, Category +from sqlmodel import Session, select, func +from app.backend.models.models import User, Shop, Category, Product, Order, OrderItem from app.backend.database import get_session from app.backend.routes.auth import get_current_user from typing import List +from datetime import datetime, timedelta router = APIRouter() @@ -177,3 +178,168 @@ def get_all_categories( categories = session.exec(select(Category)).all() return categories + + +@router.get("/stats/users") +def get_user_statistics( + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user), +): + """Get user statistics for the admin dashboard""" + verify_admin(current_user) + + # Count total users + total_users = session.exec( + select(func.count()).where(User.role == "customer") + ).one() + + # Count shop owners + total_shop_owners = session.exec( + select(func.count()).where(User.role == "shop_owner") + ).one() + + # Get user growth over time (last 30 days) + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # Get all users created within the time period + users_by_day = {} + + # Initialize all days with 0 count + for i in range(31): + day = end_date - timedelta(days=i) + day_str = day.strftime("%Y-%m-%d") + users_by_day[day_str] = 0 + + # Count users by creation date + user_counts = session.exec( + select(func.date(User.created_at).label("day"), func.count().label("count")) + .where(User.created_at >= start_date) + .group_by(func.date(User.created_at)) + ).all() + + # Update counts + for day_data in user_counts: + day_str = day_data[0].strftime("%Y-%m-%d") + users_by_day[day_str] = day_data[1] + + # Convert to list of dictionaries + user_growth = [ + {"date": date, "count": count} for date, count in users_by_day.items() + ] + + # Sort by date + user_growth.sort(key=lambda x: x["date"]) + + return { + "total_users": total_users, + "total_shop_owners": total_shop_owners, + "user_growth": user_growth, + } + + +@router.get("/stats/revenue") +def get_revenue_statistics( + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user), +): + """Get revenue statistics for the admin dashboard""" + verify_admin(current_user) + + # Calculate total revenue (5% of product sales + shipping) + orders = session.exec(select(Order)).all() + + total_product_revenue = 0 + total_shipping_revenue = 0 + + for order in orders: + # 5% of the product price is the revenue + product_revenue = order.total_price * 0.05 + total_product_revenue += product_revenue + + # Shipping fee is also revenue + total_shipping_revenue += order.shipping_price + + total_revenue = total_product_revenue + total_shipping_revenue + + # Get revenue over time (last 30 days) + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # Initialize all days with 0 revenue + revenue_by_day = {} + for i in range(31): + day = end_date - timedelta(days=i) + day_str = day.strftime("%Y-%m-%d") + revenue_by_day[day_str] = 0 + + # Calculate revenue by day + for order in orders: + if order.created_at >= start_date: + day_str = order.created_at.strftime("%Y-%m-%d") + if day_str in revenue_by_day: + revenue_by_day[day_str] += ( + order.total_price * 0.05 + ) + order.shipping_price + + # Convert to list of dictionaries + revenue_over_time = [ + {"date": date, "amount": round(amount, 2)} + for date, amount in revenue_by_day.items() + ] + + # Sort by date + revenue_over_time.sort(key=lambda x: x["date"]) + + return { + "total_revenue": round(total_revenue, 2), + "product_revenue": round(total_product_revenue, 2), + "shipping_revenue": round(total_shipping_revenue, 2), + "revenue_over_time": revenue_over_time, + } + + +@router.get("/stats/products") +def get_product_statistics( + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user), +): + """Get product statistics for the admin dashboard""" + verify_admin(current_user) + + # Total products count + total_products = session.exec(select(func.count(Product.id))).one() + + # Products by category + products_by_category = session.exec( + select(Category.name, func.count(Product.id).label("count")) + .join(Category, Product.category_id == Category.id) + .group_by(Category.name) + ).all() + + category_stats = [ + {"category_name": name, "product_count": count} + for name, count in products_by_category + ] + + # Top selling products (using order items) + product_sales = session.exec( + select( + Product.id, Product.name, func.sum(OrderItem.quantity).label("total_sold") + ) + .join(OrderItem, OrderItem.product_id == Product.id) + .group_by(Product.id, Product.name) + .order_by(func.sum(OrderItem.quantity).desc()) + .limit(20) + ).all() + + product_sales_stats = [ + {"product_id": id, "product_name": name, "sales_count": total_sold} + for id, name, total_sold in product_sales + ] + + return { + "total_products": total_products, + "products_by_category": category_stats, + "product_sales": product_sales_stats, + } diff --git a/app/frontend/components/admin/__init__.py b/app/frontend/components/admin/__init__.py index 1f6bf6c..6b3b6d1 100644 --- a/app/frontend/components/admin/__init__.py +++ b/app/frontend/components/admin/__init__.py @@ -2,4 +2,5 @@ from .dashboard import admin_dashboard_frame from .user_management import admin_user_management_frame from .shop_owner_management import admin_shop_owner_management_frame -from .category import category_frame \ No newline at end of file +from .category import category_frame +from .system_statistics import admin_system_statistics_frame diff --git a/app/frontend/components/admin/dashboard.py b/app/frontend/components/admin/dashboard.py index 3a25bcf..dfa637a 100644 --- a/app/frontend/components/admin/dashboard.py +++ b/app/frontend/components/admin/dashboard.py @@ -202,7 +202,7 @@ def admin_dashboard_frame(parent, switch_func, API_URL, access_token): 0, "Category Management", "Manage product categories", - "🏷️", + "📑", lambda: switch_func("category"), "#9c27b0", ) @@ -212,12 +212,8 @@ def admin_dashboard_frame(parent, switch_func, API_URL, access_token): 1, "System Statistics", "View system analytics and data", - "��", - lambda: CTkMessagebox( - title="Coming Soon", - message="System statistics will be available in a future update.", - icon="info", - ), + "📊", + lambda: switch_func("admin_system_statistics"), "#e91e63", ) diff --git a/app/frontend/components/admin/system_statistics.py b/app/frontend/components/admin/system_statistics.py new file mode 100644 index 0000000..9ec931a --- /dev/null +++ b/app/frontend/components/admin/system_statistics.py @@ -0,0 +1,733 @@ +import customtkinter as ctk +from CTkMessagebox import CTkMessagebox +import requests +from tkinter import ttk +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import numpy as np +from datetime import datetime, timedelta + + +def admin_system_statistics_frame(parent, switch_func, API_URL, access_token): + """ + Admin dashboard component for system statistics and analytics + """ + frame = ctk.CTkFrame(parent) + + # Store the token for later use + def update_token(new_token): + nonlocal access_token + access_token = new_token + + frame.update_token = update_token + + # Create main container with padding + main_container = ctk.CTkFrame(frame, fg_color="transparent") + main_container.pack(padx=40, pady=30, fill="both", expand=True) + + # Header section + header_frame = ctk.CTkFrame(main_container, corner_radius=15, fg_color="#e91e63") + header_frame.pack(fill="x", pady=(0, 20)) + + # Title and description + title_frame = ctk.CTkFrame(header_frame, fg_color="transparent") + title_frame.pack(padx=20, pady=15) + + ctk.CTkLabel( + title_frame, + text="System Statistics", + font=("Helvetica", 24, "bold"), + text_color="#ffffff", + ).pack(anchor="w") + + ctk.CTkLabel( + title_frame, + text="Key performance indicators and system analytics", + font=("Helvetica", 14), + text_color="#e0e0e0", + ).pack(anchor="w") + + # Function to create statistic cards + def create_stat_card(parent, title, value, icon, color, row, column): + card = ctk.CTkFrame( + parent, + corner_radius=12, + fg_color=color, + border_width=1, + border_color="#2a5a8f", + height=100, + ) + card.grid(row=row, column=column, padx=10, pady=10, sticky="nsew") + + # Make sure card keeps its height + card.grid_propagate(False) + + # Create icon circle + icon_frame = ctk.CTkFrame( + card, width=40, height=40, corner_radius=20, fg_color="#ffffff" + ) + icon_frame.place(relx=0.1, rely=0.3, anchor="center") + + # Icon text + ctk.CTkLabel( + icon_frame, text=icon, font=("Helvetica", 16, "bold"), text_color=color + ).place(relx=0.5, rely=0.5, anchor="center") + + # Card title + ctk.CTkLabel( + card, + text=title, + font=("Helvetica", 14, "bold"), + text_color="#ffffff", + ).place(relx=0.5, rely=0.3, anchor="center") + + # Card value + value_label = ctk.CTkLabel( + card, + text=value, + font=("Helvetica", 22, "bold"), + text_color="#ffffff", + ) + value_label.place(relx=0.5, rely=0.7, anchor="center") + + return value_label + + # Statistics overview cards + stats_container = ctk.CTkFrame(main_container, fg_color="transparent") + stats_container.pack(fill="x", pady=(0, 20)) + + # Configure grid for stat cards + stats_container.grid_columnconfigure(0, weight=1) + stats_container.grid_columnconfigure(1, weight=1) + stats_container.grid_columnconfigure(2, weight=1) + + # Placeholder values (will be filled with real data) + user_count = "Loading..." + owner_count = "Loading..." + revenue = "Loading..." + + # User accounts count card + user_card = create_stat_card( + stats_container, "Total Users", user_count, "👤", "#3a7ebf", 0, 0 + ) + + # Shop owner accounts count card + owner_card = create_stat_card( + stats_container, "Shop Owners", owner_count, "🏪", "#2e8b57", 0, 1 + ) + + # Revenue card + revenue_card = create_stat_card( + stats_container, "Total Revenue", revenue, "💰", "#e91e63", 0, 2 + ) + + # Create tabs for different charts + tab_view = ctk.CTkTabview(main_container, corner_radius=10) + tab_view.pack(fill="both", expand=True, pady=(10, 20)) + + # Add tabs + tab_view.add("User Growth") + tab_view.add("Revenue") + tab_view.add("Product Sales") + + # Set default tab + tab_view.set("User Growth") + + # Place for future graphs in each tab + user_graph_frame = ctk.CTkFrame(tab_view.tab("User Growth"), fg_color="transparent") + user_graph_frame.pack(fill="both", expand=True, padx=10, pady=10) + + revenue_graph_frame = ctk.CTkFrame(tab_view.tab("Revenue"), fg_color="transparent") + revenue_graph_frame.pack(fill="both", expand=True, padx=10, pady=10) + + sales_graph_frame = ctk.CTkFrame( + tab_view.tab("Product Sales"), fg_color="transparent" + ) + sales_graph_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Store current active tab + current_tab = "User Growth" + + # Function to handle tab changes + def handle_tab_change(): + nonlocal current_tab + selected_tab = tab_view.get() + + # Skip if no change + if selected_tab == current_tab: + return + + # Unbind mousewheel from previous tab + if current_tab == "User Growth" and hasattr( + user_graph_frame, "unbind_mousewheel" + ): + user_graph_frame.unbind_mousewheel() + elif current_tab == "Revenue" and hasattr( + revenue_graph_frame, "unbind_mousewheel" + ): + revenue_graph_frame.unbind_mousewheel() + elif current_tab == "Product Sales" and hasattr( + sales_graph_frame, "unbind_mousewheel" + ): + sales_graph_frame.unbind_mousewheel() + + # Bind mousewheel to new tab + if selected_tab == "User Growth" and hasattr( + user_graph_frame, "bind_mousewheel" + ): + user_graph_frame.bind_mousewheel() + elif selected_tab == "Revenue" and hasattr( + revenue_graph_frame, "bind_mousewheel" + ): + revenue_graph_frame.bind_mousewheel() + elif selected_tab == "Product Sales" and hasattr( + sales_graph_frame, "bind_mousewheel" + ): + sales_graph_frame.bind_mousewheel() + + # Update current tab + current_tab = selected_tab + + # Configure tab change event + original_select = tab_view._segmented_button._command + + def tab_changed(*args, **kwargs): + result = original_select(*args, **kwargs) + handle_tab_change() + return result + + tab_view._segmented_button._command = tab_changed + + # Control panel with refresh button + control_panel = ctk.CTkFrame(main_container, corner_radius=10, height=50) + control_panel.pack(fill="x", pady=(0, 15)) + + refresh_button = ctk.CTkButton( + control_panel, + text="Refresh Statistics", + font=("Helvetica", 12, "bold"), + height=35, + width=150, + corner_radius=8, + fg_color="#4caf50", + hover_color="#388e3c", + command=lambda: fetch_statistics(), + ) + refresh_button.pack(side="right", padx=20, pady=10) + + date_range = ctk.CTkOptionMenu( + control_panel, + values=["Last 7 Days", "Last 30 Days", "Last 90 Days", "All Time"], + font=("Helvetica", 12), + width=150, + height=35, + dropdown_font=("Helvetica", 12), + ) + date_range.pack(side="left", padx=20, pady=10) + date_range.set("Last 30 Days") + + # Footer with back button + footer_frame = ctk.CTkFrame(main_container, fg_color="transparent", height=50) + footer_frame.pack(fill="x", pady=(15, 0)) + + back_button = ctk.CTkButton( + footer_frame, + text="Back to Admin Dashboard", + command=lambda: switch_func("admin_dashboard"), + font=("Helvetica", 12, "bold"), + height=40, + corner_radius=8, + fg_color="#555555", + hover_color="#444444", + ) + back_button.pack(side="left") + + # Function to fetch statistics data + def fetch_statistics(): + headers = {"Authorization": f"Bearer {access_token}"} + + try: + # Fetch user count + response = requests.get(f"{API_URL}/admin/stats/users", headers=headers) + if response.status_code == 200: + stats = response.json() + user_count = stats.get("total_users", 0) + owner_count = stats.get("total_shop_owners", 0) + + # Update the cards + user_card.configure(text=str(user_count)) + owner_card.configure(text=str(owner_count)) + + # Create user growth chart + create_user_growth_chart(user_graph_frame, stats.get("user_growth", [])) + else: + CTkMessagebox( + title="Error", + message="Failed to fetch user statistics", + icon="cancel", + ) + + # Fetch revenue data + response = requests.get(f"{API_URL}/admin/stats/revenue", headers=headers) + if response.status_code == 200: + stats = response.json() + total_revenue = stats.get("total_revenue", 0) + + # Format revenue with currency symbol and 2 decimal places + revenue_card.configure(text=f"${total_revenue:.2f}") + + # Create revenue chart + create_revenue_chart( + revenue_graph_frame, stats.get("revenue_over_time", []) + ) + else: + CTkMessagebox( + title="Error", + message="Failed to fetch revenue statistics", + icon="cancel", + ) + + # Fetch product sales data + response = requests.get(f"{API_URL}/admin/stats/products", headers=headers) + if response.status_code == 200: + stats = response.json() + create_product_sales_chart( + sales_graph_frame, stats.get("product_sales", []) + ) + else: + CTkMessagebox( + title="Error", + message="Failed to fetch product statistics", + icon="cancel", + ) + + except Exception as e: + CTkMessagebox( + title="Error", + message=f"Failed to connect to server: {e}", + icon="cancel", + ) + + # Function to create user growth chart + def create_user_growth_chart(parent_frame, data): + # Clear existing widgets + for widget in parent_frame.winfo_children(): + widget.destroy() + + # If no data, show message + if not data: + ctk.CTkLabel( + parent_frame, + text="No user growth data available", + font=("Helvetica", 14), + ).pack(expand=True) + return + + # Create a canvas with scrollbar for the chart + canvas_frame = ctk.CTkFrame(parent_frame, fg_color="transparent") + canvas_frame.pack(fill="both", expand=True) + + # Add scrollbar to the frame + scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical") + scrollbar.pack(side="right", fill="y") + + # Create a canvas that will be scrollable + canvas = ctk.CTkCanvas(canvas_frame, bd=0, highlightthickness=0, bg="#333333") + canvas.pack(side="left", fill="both", expand=True) + + # Configure the scrollbar to work with the canvas + scrollbar.config(command=canvas.yview) + canvas.configure(yscrollcommand=scrollbar.set) + + # Create a frame inside the canvas for the chart + chart_frame = ctk.CTkFrame(canvas, fg_color="#333333") + + # Create a window inside the canvas with the chart frame + canvas_window = canvas.create_window((0, 0), window=chart_frame, anchor="nw") + + # Make sure we have sorted data by date + sorted_data = sorted(data, key=lambda x: x.get("date", "")) + + # Extract dates and user counts + dates = [item.get("date") for item in sorted_data] + users = [item.get("count") for item in sorted_data] + + # Set fixed chart dimensions with enough height for scrolling + chart_width = 700 + chart_height = max( + 400, len(dates) * 25 + ) # Adjust height based on number of dates + + # Create figure for the chart - rotate the chart to be vertical + fig, ax = plt.subplots(figsize=(10, max(8, len(dates) * 0.4)), dpi=100) + fig.patch.set_facecolor("#333333") + ax.set_facecolor("#333333") + + # Create a vertical bar chart instead of a line chart + ax.bar(dates, users, color="#3a7ebf", alpha=0.8) + ax.set_title("User Growth Over Time", color="white", fontsize=16) + ax.set_xlabel("Date", color="white", fontsize=12) + ax.set_ylabel("Number of Users", color="white", fontsize=12) + ax.tick_params( + axis="x", colors="white", rotation=90 + ) # Rotate x labels for better readability + ax.tick_params(axis="y", colors="white") + ax.grid(True, linestyle="--", alpha=0.7, axis="y") + + # Add data labels on top of bars + for i, v in enumerate(users): + if v > 0: # Only show labels for bars with values + ax.text( + i, + v + 0.1, + str(v), + ha="center", + va="bottom", + color="white", + fontsize=10, + ) + + # Adjust subplot parameters for better spacing + plt.tight_layout() + + # Embed the chart in the tkinter frame + chart_canvas = FigureCanvasTkAgg(fig, master=chart_frame) + chart_canvas.draw() + chart_widget = chart_canvas.get_tk_widget() + chart_widget.configure(width=chart_width, height=chart_height) + chart_widget.pack(fill="both", expand=True) + + # Update the canvas's scroll region + chart_frame.update_idletasks() + canvas.config(scrollregion=canvas.bbox("all")) + + # Add helpful instruction text + instruction_label = ctk.CTkLabel( + parent_frame, + text="Scroll down to see more data points", + font=("Helvetica", 12), + text_color="#e0e0e0", + ) + instruction_label.pack(pady=(5, 0)) + + # Define mouse wheel scrolling function + def _on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + # Bind the mousewheel to the canvas directly for better response + canvas.bind("<MouseWheel>", _on_mousewheel) + + # Store event bindings to manage them + canvas.bind_all("<MouseWheel>", _on_mousewheel) + + # Function to unbind mousewheel event when tab changes + def _unbind_mousewheel(): + canvas.unbind_all("<MouseWheel>") + + # Store the unbind function to be called when switching tabs + parent_frame.unbind_mousewheel = _unbind_mousewheel + + # Function to bind mousewheel event when tab is selected + def _bind_mousewheel(): + canvas.bind_all("<MouseWheel>", _on_mousewheel) + + # Store the bind function to be called when this tab is selected + parent_frame.bind_mousewheel = _bind_mousewheel + + # Bind frame configure event to adjust the canvas window + def _on_frame_configure(event): + canvas.configure(scrollregion=canvas.bbox("all")) + + chart_frame.bind("<Configure>", _on_frame_configure) + + # Adjust canvas width when the window resizes + def _on_canvas_configure(event): + canvas.itemconfig(canvas_window, width=event.width) + + canvas.bind("<Configure>", _on_canvas_configure) + + # Function to create revenue chart + def create_revenue_chart(parent_frame, data): + # Clear existing widgets + for widget in parent_frame.winfo_children(): + widget.destroy() + + # If no data, show message + if not data: + ctk.CTkLabel( + parent_frame, + text="No revenue data available", + font=("Helvetica", 14), + ).pack(expand=True) + return + + # Create a canvas with scrollbar for the chart + canvas_frame = ctk.CTkFrame(parent_frame, fg_color="transparent") + canvas_frame.pack(fill="both", expand=True) + + # Add scrollbar to the frame + scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical") + scrollbar.pack(side="right", fill="y") + + # Create a canvas that will be scrollable + canvas = ctk.CTkCanvas(canvas_frame, bd=0, highlightthickness=0, bg="#333333") + canvas.pack(side="left", fill="both", expand=True) + + # Configure the scrollbar to work with the canvas + scrollbar.config(command=canvas.yview) + canvas.configure(yscrollcommand=scrollbar.set) + + # Create a frame inside the canvas for the chart + chart_frame = ctk.CTkFrame(canvas, fg_color="#333333") + + # Create a window inside the canvas with the chart frame + canvas_window = canvas.create_window((0, 0), window=chart_frame, anchor="nw") + + # Sort data by date + sorted_data = sorted(data, key=lambda x: x.get("date", "")) + + # Extract dates and amounts + dates = [item.get("date") for item in sorted_data] + amounts = [item.get("amount") for item in sorted_data] + + # Set chart dimensions + chart_width = 700 + chart_height = max( + 400, len(dates) * 25 + ) # Adjust height based on number of dates + + # Create figure for the chart + fig, ax = plt.subplots(figsize=(10, max(8, len(dates) * 0.4)), dpi=100) + fig.patch.set_facecolor("#333333") + ax.set_facecolor("#333333") + + # Create the plot - vertical bars + bars = ax.bar(dates, amounts, color="#e91e63", alpha=0.8) + ax.set_title("Revenue Over Time", color="white", fontsize=16) + ax.set_xlabel("Date", color="white", fontsize=12) + ax.set_ylabel("Revenue (USD)", color="white", fontsize=12) + ax.tick_params( + axis="x", colors="white", rotation=90 + ) # Rotate x labels for better readability + ax.tick_params(axis="y", colors="white") + ax.grid(True, linestyle="--", alpha=0.7, axis="y") + + # Add labels on top of bars for better readability + for bar in bars: + height = bar.get_height() + if height > 0: # Only add labels for bars with values + ax.text( + bar.get_x() + bar.get_width() / 2, + height + 0.1, + f"${height:.2f}", + ha="center", + va="bottom", + color="white", + fontsize=9, + ) + + # Adjust subplot parameters for better spacing + plt.tight_layout() + + # Embed the chart in the tkinter frame + chart_canvas = FigureCanvasTkAgg(fig, master=chart_frame) + chart_canvas.draw() + chart_widget = chart_canvas.get_tk_widget() + chart_widget.configure(width=chart_width, height=chart_height) + chart_widget.pack(fill="both", expand=True) + + # Update the canvas's scroll region + chart_frame.update_idletasks() + canvas.config(scrollregion=canvas.bbox("all")) + + # Add helpful instruction text + instruction_label = ctk.CTkLabel( + parent_frame, + text="Scroll down to see more data points", + font=("Helvetica", 12), + text_color="#e0e0e0", + ) + instruction_label.pack(pady=(5, 0)) + + # Define mouse wheel scrolling function + def _on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + # Bind the mousewheel to the canvas directly for better response + canvas.bind("<MouseWheel>", _on_mousewheel) + + # Store event bindings to manage them + canvas.bind_all("<MouseWheel>", _on_mousewheel) + + # Function to unbind mousewheel event when tab changes + def _unbind_mousewheel(): + canvas.unbind_all("<MouseWheel>") + + # Store the unbind function to be called when switching tabs + parent_frame.unbind_mousewheel = _unbind_mousewheel + + # Function to bind mousewheel event when tab is selected + def _bind_mousewheel(): + canvas.bind_all("<MouseWheel>", _on_mousewheel) + + # Store the bind function to be called when this tab is selected + parent_frame.bind_mousewheel = _bind_mousewheel + + # Bind frame configure event to adjust the canvas window + def _on_frame_configure(event): + canvas.configure(scrollregion=canvas.bbox("all")) + + chart_frame.bind("<Configure>", _on_frame_configure) + + # Adjust canvas width when the window resizes + def _on_canvas_configure(event): + canvas.itemconfig(canvas_window, width=event.width) + + canvas.bind("<Configure>", _on_canvas_configure) + + # Function to create product sales chart + def create_product_sales_chart(parent_frame, data): + # Clear existing widgets + for widget in parent_frame.winfo_children(): + widget.destroy() + + # If no data, show message + if not data: + ctk.CTkLabel( + parent_frame, + text="No product sales data available", + font=("Helvetica", 14), + ).pack(expand=True) + return + + # Create a canvas with scrollbar for the chart + canvas_frame = ctk.CTkFrame(parent_frame, fg_color="transparent") + canvas_frame.pack(fill="both", expand=True) + + # Add scrollbar to the frame + scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical") + scrollbar.pack(side="right", fill="y") + + # Create a canvas that will be scrollable + canvas = ctk.CTkCanvas(canvas_frame, bd=0, highlightthickness=0, bg="#333333") + canvas.pack(side="left", fill="both", expand=True) + + # Configure the scrollbar to work with the canvas + scrollbar.config(command=canvas.yview) + canvas.configure(yscrollcommand=scrollbar.set) + + # Create a frame inside the canvas for the chart + chart_frame = ctk.CTkFrame(canvas, fg_color="#333333") + + # Create a window inside the canvas with the chart frame + canvas_window = canvas.create_window((0, 0), window=chart_frame, anchor="nw") + + # Make the chart wider for better visibility + chart_width = 700 + chart_height = max( + 400, len(data) * 30 + ) # Adjust height based on number of products + + # Sort data by sales count (descending) + sorted_data = sorted(data, key=lambda x: x.get("sales_count", 0), reverse=True) + + # Extract product names and sales counts + products = [item.get("product_name", "Unknown") for item in sorted_data] + sales = [item.get("sales_count", 0) for item in sorted_data] + + # Create figure for the chart + fig, ax = plt.subplots(figsize=(10, max(6, len(products) * 0.4)), dpi=100) + fig.patch.set_facecolor("#333333") + ax.set_facecolor("#333333") + + # Create the horizontal bar plot + bars = ax.barh(products, sales, color="#2e8b57", alpha=0.7) + ax.set_title("Product Sales Ranking", color="white", fontsize=16) + ax.set_xlabel("Number of Sales", color="white", fontsize=12) + ax.set_ylabel("Product Name", color="white", fontsize=12) + ax.tick_params(axis="x", colors="white") + ax.tick_params(axis="y", colors="white") + ax.grid(True, linestyle="--", alpha=0.7, axis="x") + + # Add labels to the bars + for bar in bars: + width = bar.get_width() + ax.text( + width + 0.3, + bar.get_y() + bar.get_height() / 2, + f"{width:.0f}", + ha="left", + va="center", + color="white", + ) + + # Adjust subplot parameters for better spacing + plt.tight_layout() + + # Embed the chart in the tkinter frame + chart_canvas = FigureCanvasTkAgg(fig, master=chart_frame) + chart_canvas.draw() + chart_widget = chart_canvas.get_tk_widget() + chart_widget.configure(width=chart_width, height=chart_height) + chart_widget.pack(fill="both", expand=True) + + # Update the canvas's scroll region + chart_frame.update_idletasks() + canvas.config(scrollregion=canvas.bbox("all")) + + # Add helpful instruction text + instruction_label = ctk.CTkLabel( + parent_frame, + text="Scroll down to see more products", + font=("Helvetica", 12), + text_color="#e0e0e0", + ) + instruction_label.pack(pady=(5, 0)) + + # Define mouse wheel scrolling function + def _on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + # Bind the mousewheel to the canvas directly for better response + canvas.bind("<MouseWheel>", _on_mousewheel) + + # Store event bindings to manage them + canvas.bind_all("<MouseWheel>", _on_mousewheel) + + # Function to unbind mousewheel event when tab changes + def _unbind_mousewheel(): + canvas.unbind_all("<MouseWheel>") + + # Store the unbind function to be called when switching tabs + parent_frame.unbind_mousewheel = _unbind_mousewheel + + # Function to bind mousewheel event when tab is selected + def _bind_mousewheel(): + canvas.bind_all("<MouseWheel>", _on_mousewheel) + + # Store the bind function to be called when this tab is selected + parent_frame.bind_mousewheel = _bind_mousewheel + + # Bind frame configure event to adjust the canvas window + def _on_frame_configure(event): + canvas.configure(scrollregion=canvas.bbox("all")) + + chart_frame.bind("<Configure>", _on_frame_configure) + + # Adjust canvas width when the window resizes + def _on_canvas_configure(event): + canvas.itemconfig(canvas_window, width=event.width) + + canvas.bind("<Configure>", _on_canvas_configure) + + # Load statistics when the frame is shown + def on_frame_shown(): + fetch_statistics() + + # Enable mouse wheel scrolling for the default tab after statistics are loaded + if hasattr(user_graph_frame, "bind_mousewheel"): + user_graph_frame.bind_mousewheel() + + frame.on_frame_shown = on_frame_shown + + return frame diff --git a/app/frontend/main.py b/app/frontend/main.py index 0b5a2c9..4c2806e 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -16,6 +16,7 @@ from components.admin.category import category_frame from components.admin.dashboard import admin_dashboard_frame from components.admin.user_management import admin_user_management_frame from components.admin.shop_owner_management import admin_shop_owner_management_frame +from components.admin.system_statistics import admin_system_statistics_frame from components.dashboard import dashboard_frame from components.user_details import user_details_frame from components.user_orders import user_orders_frame @@ -302,6 +303,9 @@ def initialize_authenticated_frames(token): frames["admin_shop_owner_management"] = admin_shop_owner_management_frame( root, switch_frame, API_URL, token ) + frames["admin_system_statistics"] = admin_system_statistics_frame( + root, switch_frame, API_URL, token + ) # Place all authenticated frames for key, frame in frames.items(): -- GitLab