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))