484 lines
19 KiB
Python
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))
|