438 lines
15 KiB
Python
438 lines
15 KiB
Python
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.category import Category
|
|
from app.models.order import Order, OrderStatus
|
|
from app.models.product import Product, ProductStatus
|
|
from app.models.user import User, UserRole
|
|
from app.schemas.admin import TimePeriod
|
|
|
|
|
|
class AdminDashboardService:
|
|
"""
|
|
Service for admin dashboard analytics.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_date_range(period: TimePeriod) -> tuple:
|
|
"""
|
|
Get date range for a given time period.
|
|
|
|
Args:
|
|
period: Time period to get range for
|
|
|
|
Returns:
|
|
Tuple of (start_date, end_date)
|
|
|
|
"""
|
|
now = datetime.now()
|
|
end_date = now
|
|
|
|
if period == TimePeriod.TODAY:
|
|
start_date = datetime(now.year, now.month, now.day, 0, 0, 0)
|
|
|
|
elif period == TimePeriod.YESTERDAY:
|
|
yesterday = now - timedelta(days=1)
|
|
start_date = datetime(yesterday.year, yesterday.month, yesterday.day, 0, 0, 0)
|
|
end_date = datetime(yesterday.year, yesterday.month, yesterday.day, 23, 59, 59)
|
|
|
|
elif period == TimePeriod.LAST_7_DAYS:
|
|
start_date = now - timedelta(days=7)
|
|
|
|
elif period == TimePeriod.LAST_30_DAYS:
|
|
start_date = now - timedelta(days=30)
|
|
|
|
elif period == TimePeriod.THIS_MONTH:
|
|
start_date = datetime(now.year, now.month, 1)
|
|
|
|
elif period == TimePeriod.LAST_MONTH:
|
|
if now.month == 1:
|
|
start_date = datetime(now.year - 1, 12, 1)
|
|
end_date = datetime(now.year, now.month, 1) - timedelta(days=1)
|
|
else:
|
|
start_date = datetime(now.year, now.month - 1, 1)
|
|
end_date = datetime(now.year, now.month, 1) - timedelta(days=1)
|
|
|
|
elif period == TimePeriod.THIS_YEAR:
|
|
start_date = datetime(now.year, 1, 1)
|
|
|
|
else: # ALL_TIME
|
|
start_date = datetime(1900, 1, 1) # A long time ago
|
|
|
|
return start_date, end_date
|
|
|
|
@staticmethod
|
|
def get_sales_summary(db: Session, period: TimePeriod) -> dict[str, Any]:
|
|
"""
|
|
Get sales summary for a given time period.
|
|
|
|
Args:
|
|
db: Database session
|
|
period: Time period to get summary for
|
|
|
|
Returns:
|
|
Dictionary with sales summary data
|
|
|
|
"""
|
|
start_date, end_date = AdminDashboardService.get_date_range(period)
|
|
|
|
# Get completed orders in the date range
|
|
completed_statuses = [OrderStatus.DELIVERED, OrderStatus.COMPLETED]
|
|
orders = db.query(Order).filter(
|
|
Order.created_at.between(start_date, end_date),
|
|
Order.status.in_(completed_statuses)
|
|
).all()
|
|
|
|
# Get refunded orders in the date range
|
|
refunded_orders = db.query(Order).filter(
|
|
Order.created_at.between(start_date, end_date),
|
|
Order.status == OrderStatus.REFUNDED
|
|
).all()
|
|
|
|
# Calculate totals
|
|
total_sales = sum(order.total_amount for order in orders)
|
|
total_orders = len(orders)
|
|
average_order_value = total_sales / total_orders if total_orders > 0 else 0
|
|
refunded_amount = sum(order.total_amount for order in refunded_orders)
|
|
|
|
return {
|
|
"period": period,
|
|
"total_sales": total_sales,
|
|
"total_orders": total_orders,
|
|
"average_order_value": average_order_value,
|
|
"refunded_amount": refunded_amount
|
|
}
|
|
|
|
@staticmethod
|
|
def get_sales_over_time(db: Session, period: TimePeriod) -> dict[str, Any]:
|
|
"""
|
|
Get sales data over time for the given period.
|
|
|
|
Args:
|
|
db: Database session
|
|
period: Time period to get data for
|
|
|
|
Returns:
|
|
Dictionary with sales over time data
|
|
|
|
"""
|
|
start_date, end_date = AdminDashboardService.get_date_range(period)
|
|
|
|
# Determine the date grouping and format based on the period
|
|
date_format = "%Y-%m-%d" # Default daily format
|
|
delta = timedelta(days=1) # Default daily increment
|
|
|
|
if period in [TimePeriod.THIS_YEAR, TimePeriod.ALL_TIME]:
|
|
date_format = "%Y-%m" # Monthly format
|
|
|
|
# Generate all dates in the range
|
|
date_range = []
|
|
current_date = start_date
|
|
while current_date <= end_date:
|
|
date_range.append(current_date.strftime(date_format))
|
|
current_date += delta
|
|
|
|
# Get completed orders in the date range
|
|
completed_statuses = [OrderStatus.DELIVERED, OrderStatus.COMPLETED]
|
|
orders = db.query(Order).filter(
|
|
Order.created_at.between(start_date, end_date),
|
|
Order.status.in_(completed_statuses)
|
|
).all()
|
|
|
|
# Group orders by date
|
|
date_sales = {}
|
|
for date_str in date_range:
|
|
date_sales[date_str] = {"date": date_str, "total_sales": 0, "order_count": 0}
|
|
|
|
for order in orders:
|
|
date_str = order.created_at.strftime(date_format)
|
|
if date_str in date_sales:
|
|
date_sales[date_str]["total_sales"] += order.total_amount
|
|
date_sales[date_str]["order_count"] += 1
|
|
|
|
# Convert to list and sort by date
|
|
data = list(date_sales.values())
|
|
data.sort(key=lambda x: x["date"])
|
|
|
|
total_sales = sum(item["total_sales"] for item in data)
|
|
total_orders = sum(item["order_count"] for item in data)
|
|
|
|
return {
|
|
"period": period,
|
|
"data": data,
|
|
"total_sales": total_sales,
|
|
"total_orders": total_orders
|
|
}
|
|
|
|
@staticmethod
|
|
def get_top_categories(db: Session, period: TimePeriod, limit: int = 5) -> dict[str, Any]:
|
|
"""
|
|
Get top selling categories for the given period.
|
|
|
|
Args:
|
|
db: Database session
|
|
period: Time period to get data for
|
|
limit: Number of categories to return
|
|
|
|
Returns:
|
|
Dictionary with top category sales data
|
|
|
|
"""
|
|
start_date, end_date = AdminDashboardService.get_date_range(period)
|
|
|
|
# Get completed orders in the date range
|
|
completed_statuses = [OrderStatus.DELIVERED, OrderStatus.COMPLETED]
|
|
|
|
# This is a complex query that would involve joining multiple tables
|
|
# For simplicity, we'll use a more direct approach
|
|
|
|
# Get all order items from completed orders
|
|
orders = db.query(Order).filter(
|
|
Order.created_at.between(start_date, end_date),
|
|
Order.status.in_(completed_statuses)
|
|
).all()
|
|
|
|
# Collect category sales data
|
|
category_sales = {}
|
|
total_sales = 0
|
|
|
|
for order in orders:
|
|
for item in order.items:
|
|
product = db.query(Product).filter(Product.id == item.product_id).first()
|
|
if product and product.category_id:
|
|
category_id = product.category_id
|
|
category = db.query(Category).filter(Category.id == category_id).first()
|
|
|
|
if category:
|
|
if category_id not in category_sales:
|
|
category_sales[category_id] = {
|
|
"category_id": category_id,
|
|
"category_name": category.name,
|
|
"total_sales": 0
|
|
}
|
|
|
|
item_total = item.unit_price * item.quantity - item.discount
|
|
category_sales[category_id]["total_sales"] += item_total
|
|
total_sales += item_total
|
|
|
|
# Convert to list and sort by total sales
|
|
categories = list(category_sales.values())
|
|
categories.sort(key=lambda x: x["total_sales"], reverse=True)
|
|
|
|
# Calculate percentages and limit results
|
|
for category in categories:
|
|
category["percentage"] = (category["total_sales"] / total_sales * 100) if total_sales > 0 else 0
|
|
|
|
categories = categories[:limit]
|
|
|
|
return {
|
|
"period": period,
|
|
"categories": categories,
|
|
"total_sales": total_sales
|
|
}
|
|
|
|
@staticmethod
|
|
def get_top_products(db: Session, period: TimePeriod, limit: int = 5) -> dict[str, Any]:
|
|
"""
|
|
Get top selling products for the given period.
|
|
|
|
Args:
|
|
db: Database session
|
|
period: Time period to get data for
|
|
limit: Number of products to return
|
|
|
|
Returns:
|
|
Dictionary with top product sales data
|
|
|
|
"""
|
|
start_date, end_date = AdminDashboardService.get_date_range(period)
|
|
|
|
# Get completed orders in the date range
|
|
completed_statuses = [OrderStatus.DELIVERED, OrderStatus.COMPLETED]
|
|
|
|
# Get all order items from completed orders
|
|
orders = db.query(Order).filter(
|
|
Order.created_at.between(start_date, end_date),
|
|
Order.status.in_(completed_statuses)
|
|
).all()
|
|
|
|
# Collect product sales data
|
|
product_sales = {}
|
|
total_sales = 0
|
|
|
|
for order in orders:
|
|
for item in order.items:
|
|
product_id = item.product_id
|
|
|
|
if product_id not in product_sales:
|
|
product = db.query(Product).filter(Product.id == product_id).first()
|
|
product_name = item.product_name if item.product_name else (product.name if product else "Unknown Product")
|
|
|
|
product_sales[product_id] = {
|
|
"product_id": product_id,
|
|
"product_name": product_name,
|
|
"quantity_sold": 0,
|
|
"total_sales": 0
|
|
}
|
|
|
|
product_sales[product_id]["quantity_sold"] += item.quantity
|
|
item_total = item.unit_price * item.quantity - item.discount
|
|
product_sales[product_id]["total_sales"] += item_total
|
|
total_sales += item_total
|
|
|
|
# Convert to list and sort by total sales
|
|
products = list(product_sales.values())
|
|
products.sort(key=lambda x: x["total_sales"], reverse=True)
|
|
|
|
# Limit results
|
|
products = products[:limit]
|
|
|
|
return {
|
|
"period": period,
|
|
"products": products,
|
|
"total_sales": total_sales
|
|
}
|
|
|
|
@staticmethod
|
|
def get_top_customers(db: Session, period: TimePeriod, limit: int = 5) -> dict[str, Any]:
|
|
"""
|
|
Get top customers for the given period.
|
|
|
|
Args:
|
|
db: Database session
|
|
period: Time period to get data for
|
|
limit: Number of customers to return
|
|
|
|
Returns:
|
|
Dictionary with top customer data
|
|
|
|
"""
|
|
start_date, end_date = AdminDashboardService.get_date_range(period)
|
|
|
|
# Get completed orders in the date range
|
|
completed_statuses = [OrderStatus.DELIVERED, OrderStatus.COMPLETED]
|
|
|
|
# Get all orders from the period
|
|
orders = db.query(Order).filter(
|
|
Order.created_at.between(start_date, end_date),
|
|
Order.status.in_(completed_statuses)
|
|
).all()
|
|
|
|
# Collect customer data
|
|
customer_data = {}
|
|
total_sales = 0
|
|
|
|
for order in orders:
|
|
user_id = order.user_id
|
|
|
|
if user_id not in customer_data:
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
user_name = f"{user.first_name} {user.last_name}" if user and user.first_name else f"User {user_id}"
|
|
|
|
customer_data[user_id] = {
|
|
"user_id": user_id,
|
|
"user_name": user_name,
|
|
"order_count": 0,
|
|
"total_spent": 0
|
|
}
|
|
|
|
customer_data[user_id]["order_count"] += 1
|
|
customer_data[user_id]["total_spent"] += order.total_amount
|
|
total_sales += order.total_amount
|
|
|
|
# Convert to list and sort by total spent
|
|
customers = list(customer_data.values())
|
|
customers.sort(key=lambda x: x["total_spent"], reverse=True)
|
|
|
|
# Limit results
|
|
customers = customers[:limit]
|
|
|
|
return {
|
|
"period": period,
|
|
"customers": customers,
|
|
"total_sales": total_sales
|
|
}
|
|
|
|
@staticmethod
|
|
def get_dashboard_summary(db: Session) -> dict[str, Any]:
|
|
"""
|
|
Get a summary of key metrics for the dashboard.
|
|
|
|
Args:
|
|
db: Database session
|
|
|
|
Returns:
|
|
Dictionary with dashboard summary data
|
|
|
|
"""
|
|
# Get sales summary for last 30 days
|
|
sales_summary = AdminDashboardService.get_sales_summary(db, TimePeriod.LAST_30_DAYS)
|
|
|
|
# Count pending orders
|
|
pending_orders = db.query(Order).filter(Order.status == OrderStatus.PENDING).count()
|
|
|
|
# Count low stock products (less than 5 items)
|
|
low_stock_products = db.query(Product).filter(
|
|
Product.stock_quantity <= 5,
|
|
Product.status != ProductStatus.DISCONTINUED
|
|
).count()
|
|
|
|
# Count new customers in the last 30 days
|
|
last_30_days = datetime.now() - timedelta(days=30)
|
|
new_customers = db.query(User).filter(
|
|
User.created_at >= last_30_days,
|
|
User.role == UserRole.CUSTOMER
|
|
).count()
|
|
|
|
# Count total products and customers
|
|
total_products = db.query(Product).count()
|
|
total_customers = db.query(User).filter(User.role == UserRole.CUSTOMER).count()
|
|
|
|
return {
|
|
"sales_summary": sales_summary,
|
|
"pending_orders": pending_orders,
|
|
"low_stock_products": low_stock_products,
|
|
"new_customers": new_customers,
|
|
"total_products": total_products,
|
|
"total_customers": total_customers
|
|
}
|
|
|
|
@staticmethod
|
|
def get_orders_by_status(db: Session) -> list[dict[str, Any]]:
|
|
"""
|
|
Get order counts by status.
|
|
|
|
Args:
|
|
db: Database session
|
|
|
|
Returns:
|
|
List of dictionaries with order status counts
|
|
|
|
"""
|
|
# Count orders by status
|
|
status_counts = db.query(
|
|
Order.status,
|
|
func.count(Order.id).label('count')
|
|
).group_by(Order.status).all()
|
|
|
|
# Calculate total orders
|
|
total_orders = sum(count for _, count in status_counts)
|
|
|
|
# Format results
|
|
result = []
|
|
for status, count in status_counts:
|
|
percentage = (count / total_orders * 100) if total_orders > 0 else 0
|
|
result.append({
|
|
"status": status.value,
|
|
"count": count,
|
|
"percentage": percentage
|
|
})
|
|
|
|
# Sort by count (descending)
|
|
result.sort(key=lambda x: x["count"], reverse=True)
|
|
|
|
return result
|