Files
Hotel-Booking/Backend/src/routes/report_routes.py
Iliyan Angelov 48353cde9c update
2025-11-16 20:05:08 +02:00

484 lines
19 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import func, and_
from typing import Optional
from datetime import datetime, timedelta
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.booking import Booking, BookingStatus
from ..models.payment import Payment, PaymentStatus
from ..models.room import Room
from ..models.service_usage import ServiceUsage
from ..models.service import Service
router = APIRouter(prefix="/reports", tags=["reports"])
@router.get("")
async def get_reports(
from_date: Optional[str] = Query(None, alias="from"),
to_date: Optional[str] = Query(None, alias="to"),
type: Optional[str] = Query(None),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get comprehensive reports (Admin/Staff only)"""
try:
# Parse dates if provided
start_date = None
end_date = None
if from_date:
try:
start_date = datetime.strptime(from_date, "%Y-%m-%d")
except ValueError:
start_date = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
if to_date:
try:
end_date = datetime.strptime(to_date, "%Y-%m-%d")
# Set to end of day
end_date = end_date.replace(hour=23, minute=59, second=59)
except ValueError:
end_date = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
# Base queries
booking_query = db.query(Booking)
payment_query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
# Apply date filters
if start_date:
booking_query = booking_query.filter(Booking.created_at >= start_date)
payment_query = payment_query.filter(Payment.payment_date >= start_date)
if end_date:
booking_query = booking_query.filter(Booking.created_at <= end_date)
payment_query = payment_query.filter(Payment.payment_date <= end_date)
# Total bookings
total_bookings = booking_query.count()
# Total revenue
total_revenue = payment_query.with_entities(func.sum(Payment.amount)).scalar() or 0.0
# Total customers (unique users with bookings)
total_customers = db.query(func.count(func.distinct(Booking.user_id))).scalar() or 0
if start_date or end_date:
customer_query = db.query(func.count(func.distinct(Booking.user_id)))
if start_date:
customer_query = customer_query.filter(Booking.created_at >= start_date)
if end_date:
customer_query = customer_query.filter(Booking.created_at <= end_date)
total_customers = customer_query.scalar() or 0
# Available rooms
available_rooms = db.query(Room).filter(Room.status == "available").count()
# Occupied rooms (rooms with active bookings)
occupied_rooms = db.query(func.count(func.distinct(Booking.room_id))).filter(
Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in])
).scalar() or 0
# Revenue by date (daily breakdown)
revenue_by_date = []
if start_date and end_date:
daily_revenue_query = db.query(
func.date(Payment.payment_date).label('date'),
func.sum(Payment.amount).label('revenue'),
func.count(func.distinct(Payment.booking_id)).label('bookings')
).filter(Payment.payment_status == PaymentStatus.completed)
if start_date:
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date >= start_date)
if end_date:
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date <= end_date)
daily_revenue_query = daily_revenue_query.group_by(
func.date(Payment.payment_date)
).order_by(func.date(Payment.payment_date))
daily_data = daily_revenue_query.all()
revenue_by_date = [
{
"date": str(date),
"revenue": float(revenue or 0),
"bookings": int(bookings or 0)
}
for date, revenue, bookings in daily_data
]
# Bookings by status
bookings_by_status = {}
for status in BookingStatus:
count = booking_query.filter(Booking.status == status).count()
status_name = status.value if hasattr(status, 'value') else str(status)
bookings_by_status[status_name] = count
# Top rooms (by revenue)
top_rooms_query = db.query(
Room.id,
Room.room_number,
func.count(Booking.id).label('bookings'),
func.sum(Payment.amount).label('revenue')
).join(Booking, Room.id == Booking.room_id).join(
Payment, Booking.id == Payment.booking_id
).filter(Payment.payment_status == PaymentStatus.completed)
if start_date:
top_rooms_query = top_rooms_query.filter(Booking.created_at >= start_date)
if end_date:
top_rooms_query = top_rooms_query.filter(Booking.created_at <= end_date)
top_rooms_data = top_rooms_query.group_by(Room.id, Room.room_number).order_by(
func.sum(Payment.amount).desc()
).limit(10).all()
top_rooms = [
{
"room_id": room_id,
"room_number": room_number,
"bookings": int(bookings or 0),
"revenue": float(revenue or 0)
}
for room_id, room_number, bookings, revenue in top_rooms_data
]
# Service usage statistics
service_usage_query = db.query(
Service.id,
Service.name,
func.count(ServiceUsage.id).label('usage_count'),
func.sum(ServiceUsage.total_price).label('total_revenue')
).join(ServiceUsage, Service.id == ServiceUsage.service_id)
if start_date:
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date >= start_date)
if end_date:
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date <= end_date)
service_usage_data = service_usage_query.group_by(Service.id, Service.name).order_by(
func.sum(ServiceUsage.total_price).desc()
).limit(10).all()
service_usage = [
{
"service_id": service_id,
"service_name": service_name,
"usage_count": int(usage_count or 0),
"total_revenue": float(total_revenue or 0)
}
for service_id, service_name, usage_count, total_revenue in service_usage_data
]
return {
"status": "success",
"success": True,
"data": {
"total_bookings": total_bookings,
"total_revenue": float(total_revenue),
"total_customers": int(total_customers),
"available_rooms": available_rooms,
"occupied_rooms": occupied_rooms,
"revenue_by_date": revenue_by_date if revenue_by_date else None,
"bookings_by_status": bookings_by_status,
"top_rooms": top_rooms if top_rooms else None,
"service_usage": service_usage if service_usage else None,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/dashboard")
async def get_dashboard_stats(
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get dashboard statistics (Admin/Staff only)"""
try:
# Total bookings
total_bookings = db.query(Booking).count()
# Active bookings
active_bookings = db.query(Booking).filter(
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
).count()
# Total revenue (from completed payments)
total_revenue = db.query(func.sum(Payment.amount)).filter(
Payment.payment_status == PaymentStatus.completed
).scalar() or 0.0
# Today's revenue
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
today_revenue = db.query(func.sum(Payment.amount)).filter(
and_(
Payment.payment_status == PaymentStatus.completed,
Payment.payment_date >= today_start
)
).scalar() or 0.0
# Total rooms
total_rooms = db.query(Room).count()
# Available rooms
available_rooms = db.query(Room).filter(Room.status == "available").count()
# Recent bookings (last 7 days)
week_ago = datetime.utcnow() - timedelta(days=7)
recent_bookings = db.query(Booking).filter(
Booking.created_at >= week_ago
).count()
# Pending payments
pending_payments = db.query(Payment).filter(
Payment.payment_status == PaymentStatus.pending
).count()
return {
"status": "success",
"data": {
"total_bookings": total_bookings,
"active_bookings": active_bookings,
"total_revenue": float(total_revenue),
"today_revenue": float(today_revenue),
"total_rooms": total_rooms,
"available_rooms": available_rooms,
"recent_bookings": recent_bookings,
"pending_payments": pending_payments,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/customer/dashboard")
async def get_customer_dashboard_stats(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get customer dashboard statistics"""
try:
from datetime import datetime, timedelta
# Total bookings count for user
total_bookings = db.query(Booking).filter(
Booking.user_id == current_user.id
).count()
# Total spending (sum of completed payments from user's bookings)
user_bookings = db.query(Booking.id).filter(
Booking.user_id == current_user.id
).subquery()
total_spending = db.query(func.sum(Payment.amount)).filter(
and_(
Payment.booking_id.in_(db.query(user_bookings.c.id)),
Payment.payment_status == PaymentStatus.completed
)
).scalar() or 0.0
# Currently staying (checked_in bookings)
now = datetime.utcnow()
currently_staying = db.query(Booking).filter(
and_(
Booking.user_id == current_user.id,
Booking.status == BookingStatus.checked_in,
Booking.check_in_date <= now,
Booking.check_out_date >= now
)
).count()
# Upcoming bookings (confirmed/pending with check_in_date in future)
upcoming_bookings_query = db.query(Booking).filter(
and_(
Booking.user_id == current_user.id,
Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]),
Booking.check_in_date > now
)
).order_by(Booking.check_in_date.asc()).limit(5).all()
upcoming_bookings = []
for booking in upcoming_bookings_query:
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
}
if booking.room:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
"room_type": {
"name": booking.room.room_type.name if booking.room.room_type else None
}
}
upcoming_bookings.append(booking_dict)
# Recent activity (last 5 bookings ordered by created_at)
recent_bookings_query = db.query(Booking).filter(
Booking.user_id == current_user.id
).order_by(Booking.created_at.desc()).limit(5).all()
recent_activity = []
for booking in recent_bookings_query:
activity_type = None
if booking.status == BookingStatus.checked_out:
activity_type = "Check-out"
elif booking.status == BookingStatus.checked_in:
activity_type = "Check-in"
elif booking.status == BookingStatus.confirmed:
activity_type = "Booking Confirmed"
elif booking.status == BookingStatus.pending:
activity_type = "Booking"
else:
activity_type = "Booking"
activity_dict = {
"action": activity_type,
"booking_id": booking.id,
"booking_number": booking.booking_number,
"created_at": booking.created_at.isoformat() if booking.created_at else None,
}
if booking.room:
activity_dict["room"] = {
"room_number": booking.room.room_number,
}
recent_activity.append(activity_dict)
# Calculate percentage change (placeholder - can be enhanced)
# For now, compare last month vs this month
last_month_start = (now - timedelta(days=30)).replace(day=1, hour=0, minute=0, second=0)
last_month_end = now.replace(day=1, hour=0, minute=0, second=0) - timedelta(seconds=1)
last_month_bookings = db.query(Booking).filter(
and_(
Booking.user_id == current_user.id,
Booking.created_at >= last_month_start,
Booking.created_at <= last_month_end
)
).count()
this_month_bookings = db.query(Booking).filter(
and_(
Booking.user_id == current_user.id,
Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0),
Booking.created_at <= now
)
).count()
booking_change_percentage = 0
if last_month_bookings > 0:
booking_change_percentage = ((this_month_bookings - last_month_bookings) / last_month_bookings) * 100
last_month_spending = db.query(func.sum(Payment.amount)).filter(
and_(
Payment.booking_id.in_(db.query(user_bookings.c.id)),
Payment.payment_status == PaymentStatus.completed,
Payment.payment_date >= last_month_start,
Payment.payment_date <= last_month_end
)
).scalar() or 0.0
this_month_spending = db.query(func.sum(Payment.amount)).filter(
and_(
Payment.booking_id.in_(db.query(user_bookings.c.id)),
Payment.payment_status == PaymentStatus.completed,
Payment.payment_date >= now.replace(day=1, hour=0, minute=0, second=0),
Payment.payment_date <= now
)
).scalar() or 0.0
spending_change_percentage = 0
if last_month_spending > 0:
spending_change_percentage = ((this_month_spending - last_month_spending) / last_month_spending) * 100
return {
"status": "success",
"success": True,
"data": {
"total_bookings": total_bookings,
"total_spending": float(total_spending),
"currently_staying": currently_staying,
"upcoming_bookings": upcoming_bookings,
"recent_activity": recent_activity,
"booking_change_percentage": round(booking_change_percentage, 1),
"spending_change_percentage": round(spending_change_percentage, 1),
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/revenue")
async def get_revenue_report(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get revenue report (Admin/Staff only)"""
try:
query = db.query(Payment).filter(
Payment.payment_status == PaymentStatus.completed
)
if start_date:
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
query = query.filter(Payment.payment_date >= start)
if end_date:
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
query = query.filter(Payment.payment_date <= end)
# Total revenue
total_revenue = db.query(func.sum(Payment.amount)).filter(
Payment.payment_status == PaymentStatus.completed
).scalar() or 0.0
# Revenue by payment method
revenue_by_method = db.query(
Payment.payment_method,
func.sum(Payment.amount).label('total')
).filter(
Payment.payment_status == PaymentStatus.completed
).group_by(Payment.payment_method).all()
method_breakdown = {}
for method, total in revenue_by_method:
method_name = method.value if hasattr(method, 'value') else str(method)
method_breakdown[method_name] = float(total or 0)
# Revenue by date (daily breakdown)
daily_revenue = db.query(
func.date(Payment.payment_date).label('date'),
func.sum(Payment.amount).label('total')
).filter(
Payment.payment_status == PaymentStatus.completed
).group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date).desc()).limit(30).all()
daily_breakdown = [
{
"date": date.isoformat() if isinstance(date, datetime) else str(date),
"revenue": float(total or 0)
}
for date, total in daily_revenue
]
return {
"status": "success",
"data": {
"total_revenue": float(total_revenue),
"revenue_by_method": method_breakdown,
"daily_breakdown": daily_breakdown,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))