update
This commit is contained in:
0
Backend/src/rooms/routes/__init__.py
Normal file
0
Backend/src/rooms/routes/__init__.py
Normal file
861
Backend/src/rooms/routes/advanced_room_routes.py
Normal file
861
Backend/src/rooms/routes/advanced_room_routes.py
Normal file
@@ -0,0 +1,861 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
|
||||
from sqlalchemy.orm import Session, joinedload, load_only
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceType, MaintenanceStatus
|
||||
from ...hotel_services.models.housekeeping_task import HousekeepingTask, HousekeepingStatus, HousekeepingType
|
||||
from ..models.room_inspection import RoomInspection, InspectionType, InspectionStatus
|
||||
from ..models.room_attribute import RoomAttribute
|
||||
from ..services.room_assignment_service import RoomAssignmentService
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/advanced-rooms', tags=['advanced-room-management'])
|
||||
|
||||
|
||||
# ==================== Room Assignment Optimization ====================
|
||||
|
||||
@router.post('/assign-optimal-room')
|
||||
async def assign_optimal_room(
|
||||
request_data: dict,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Find the best available room for a booking based on preferences"""
|
||||
try:
|
||||
room_type_id = request_data.get('room_type_id')
|
||||
check_in_str = request_data.get('check_in')
|
||||
check_out_str = request_data.get('check_out')
|
||||
num_guests = request_data.get('num_guests', 1)
|
||||
guest_preferences = request_data.get('guest_preferences', {})
|
||||
exclude_room_ids = request_data.get('exclude_room_ids', [])
|
||||
|
||||
if not room_type_id or not check_in_str or not check_out_str:
|
||||
raise HTTPException(status_code=400, detail='Missing required fields')
|
||||
|
||||
check_in = datetime.fromisoformat(check_in_str.replace('Z', '+00:00'))
|
||||
check_out = datetime.fromisoformat(check_out_str.replace('Z', '+00:00'))
|
||||
|
||||
best_room = RoomAssignmentService.find_best_room(
|
||||
db=db,
|
||||
room_type_id=room_type_id,
|
||||
check_in=check_in,
|
||||
check_out=check_out,
|
||||
num_guests=num_guests,
|
||||
guest_preferences=guest_preferences,
|
||||
exclude_room_ids=exclude_room_ids
|
||||
)
|
||||
|
||||
if not best_room:
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {'room': None, 'message': 'No suitable room available'}
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'room': {
|
||||
'id': best_room.id,
|
||||
'room_number': best_room.room_number,
|
||||
'floor': best_room.floor,
|
||||
'view': best_room.view,
|
||||
'status': best_room.status.value
|
||||
}
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get('/{room_id}/availability-calendar')
|
||||
async def get_room_availability_calendar(
|
||||
room_id: int,
|
||||
start_date: str = Query(...),
|
||||
end_date: str = Query(...),
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get detailed availability calendar for a room"""
|
||||
try:
|
||||
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||||
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||||
|
||||
calendar = RoomAssignmentService.get_room_availability_calendar(
|
||||
db=db,
|
||||
room_id=room_id,
|
||||
start_date=start,
|
||||
end_date=end
|
||||
)
|
||||
|
||||
if not calendar:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
|
||||
return {'status': 'success', 'data': calendar}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ==================== Room Maintenance ====================
|
||||
|
||||
@router.get('/maintenance')
|
||||
async def get_maintenance_records(
|
||||
room_id: Optional[int] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
maintenance_type: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get maintenance records with filtering"""
|
||||
try:
|
||||
# Check if user is staff (not admin) - staff should only see their assigned records
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
is_staff = role and role.name == 'staff'
|
||||
|
||||
query = db.query(RoomMaintenance)
|
||||
|
||||
# Filter by assigned_to for staff users
|
||||
if is_staff:
|
||||
query = query.filter(RoomMaintenance.assigned_to == current_user.id)
|
||||
|
||||
if room_id:
|
||||
query = query.filter(RoomMaintenance.room_id == room_id)
|
||||
if status:
|
||||
query = query.filter(RoomMaintenance.status == MaintenanceStatus(status))
|
||||
if maintenance_type:
|
||||
query = query.filter(RoomMaintenance.maintenance_type == MaintenanceType(maintenance_type))
|
||||
|
||||
total = query.count()
|
||||
query = query.order_by(desc(RoomMaintenance.scheduled_start))
|
||||
|
||||
offset = (page - 1) * limit
|
||||
records = query.offset(offset).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for record in records:
|
||||
result.append({
|
||||
'id': record.id,
|
||||
'room_id': record.room_id,
|
||||
'room_number': record.room.room_number if record.room else None,
|
||||
'maintenance_type': record.maintenance_type.value,
|
||||
'status': record.status.value,
|
||||
'title': record.title,
|
||||
'description': record.description,
|
||||
'scheduled_start': record.scheduled_start.isoformat() if record.scheduled_start else None,
|
||||
'scheduled_end': record.scheduled_end.isoformat() if record.scheduled_end else None,
|
||||
'actual_start': record.actual_start.isoformat() if record.actual_start else None,
|
||||
'actual_end': record.actual_end.isoformat() if record.actual_end else None,
|
||||
'assigned_to': record.assigned_to,
|
||||
'assigned_staff_name': record.assigned_staff.full_name if record.assigned_staff else None,
|
||||
'priority': record.priority,
|
||||
'blocks_room': record.blocks_room,
|
||||
'estimated_cost': float(record.estimated_cost) if record.estimated_cost else None,
|
||||
'actual_cost': float(record.actual_cost) if record.actual_cost else None,
|
||||
'created_at': record.created_at.isoformat() if record.created_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'maintenance_records': result,
|
||||
'pagination': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'total_pages': (total + limit - 1) // limit
|
||||
}
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/maintenance')
|
||||
async def create_maintenance_record(
|
||||
maintenance_data: dict,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new maintenance record"""
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == maintenance_data.get('room_id')).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
|
||||
scheduled_start = datetime.fromisoformat(maintenance_data['scheduled_start'].replace('Z', '+00:00'))
|
||||
scheduled_end = None
|
||||
if maintenance_data.get('scheduled_end'):
|
||||
scheduled_end = datetime.fromisoformat(maintenance_data['scheduled_end'].replace('Z', '+00:00'))
|
||||
|
||||
block_start = None
|
||||
block_end = None
|
||||
if maintenance_data.get('block_start'):
|
||||
block_start = datetime.fromisoformat(maintenance_data['block_start'].replace('Z', '+00:00'))
|
||||
if maintenance_data.get('block_end'):
|
||||
block_end = datetime.fromisoformat(maintenance_data['block_end'].replace('Z', '+00:00'))
|
||||
|
||||
maintenance = RoomMaintenance(
|
||||
room_id=maintenance_data['room_id'],
|
||||
maintenance_type=MaintenanceType(maintenance_data.get('maintenance_type', 'preventive')),
|
||||
status=MaintenanceStatus(maintenance_data.get('status', 'scheduled')),
|
||||
title=maintenance_data.get('title', 'Maintenance'),
|
||||
description=maintenance_data.get('description'),
|
||||
scheduled_start=scheduled_start,
|
||||
scheduled_end=scheduled_end,
|
||||
assigned_to=maintenance_data.get('assigned_to'),
|
||||
reported_by=current_user.id,
|
||||
estimated_cost=maintenance_data.get('estimated_cost'),
|
||||
blocks_room=maintenance_data.get('blocks_room', True),
|
||||
block_start=block_start,
|
||||
block_end=block_end,
|
||||
priority=maintenance_data.get('priority', 'medium'),
|
||||
notes=maintenance_data.get('notes')
|
||||
)
|
||||
|
||||
# Update room status if blocking and maintenance is active
|
||||
if maintenance.blocks_room and maintenance.status in [MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]:
|
||||
# Only update if room is currently available
|
||||
if room.status == RoomStatus.available:
|
||||
room.status = RoomStatus.maintenance
|
||||
|
||||
db.add(maintenance)
|
||||
db.commit()
|
||||
db.refresh(maintenance)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Maintenance record created successfully',
|
||||
'data': {'maintenance_id': maintenance.id}
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put('/maintenance/{maintenance_id}')
|
||||
async def update_maintenance_record(
|
||||
maintenance_id: int,
|
||||
maintenance_data: dict,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a maintenance record"""
|
||||
try:
|
||||
maintenance = db.query(RoomMaintenance).filter(RoomMaintenance.id == maintenance_id).first()
|
||||
if not maintenance:
|
||||
raise HTTPException(status_code=404, detail='Maintenance record not found')
|
||||
|
||||
# Check if user is staff (not admin) - staff can only update their own assigned records
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
is_staff = role and role.name == 'staff'
|
||||
|
||||
if is_staff:
|
||||
# Staff can only update records assigned to them
|
||||
if maintenance.assigned_to != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='You can only update maintenance assigned to you')
|
||||
# Staff can only update status and completion fields
|
||||
allowed_fields = {'status', 'actual_start', 'actual_end', 'completion_notes', 'actual_cost'}
|
||||
if any(key not in allowed_fields for key in maintenance_data.keys()):
|
||||
raise HTTPException(status_code=403, detail='You can only update status and completion information')
|
||||
|
||||
# Update fields
|
||||
if 'status' in maintenance_data:
|
||||
new_status = MaintenanceStatus(maintenance_data['status'])
|
||||
|
||||
# Only the assigned user can mark the maintenance as completed
|
||||
if new_status == MaintenanceStatus.completed:
|
||||
if not maintenance.assigned_to:
|
||||
raise HTTPException(status_code=400, detail='Maintenance must be assigned before it can be marked as completed')
|
||||
if maintenance.assigned_to != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Only the assigned staff member can mark this maintenance as completed')
|
||||
|
||||
old_status = maintenance.status
|
||||
maintenance.status = new_status
|
||||
|
||||
# Update room status based on maintenance status
|
||||
if maintenance.status == MaintenanceStatus.completed and maintenance.blocks_room:
|
||||
# Check if room has other active maintenance
|
||||
other_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == maintenance.room_id,
|
||||
RoomMaintenance.id != maintenance_id,
|
||||
RoomMaintenance.blocks_room == True,
|
||||
RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress])
|
||||
)
|
||||
).first()
|
||||
|
||||
if not other_maintenance:
|
||||
# Check if room has active bookings
|
||||
from datetime import datetime
|
||||
active_booking = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.room_id == maintenance.room_id,
|
||||
Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]),
|
||||
Booking.check_in_date <= datetime.utcnow(),
|
||||
Booking.check_out_date > datetime.utcnow()
|
||||
)
|
||||
).first()
|
||||
|
||||
if active_booking:
|
||||
maintenance.room.status = RoomStatus.occupied
|
||||
else:
|
||||
maintenance.room.status = RoomStatus.available
|
||||
elif maintenance.status in [MaintenanceStatus.scheduled, MaintenanceStatus.in_progress] and maintenance.blocks_room:
|
||||
# Set room to maintenance if it's not occupied
|
||||
if maintenance.room.status == RoomStatus.available:
|
||||
maintenance.room.status = RoomStatus.maintenance
|
||||
|
||||
if 'actual_start' in maintenance_data:
|
||||
maintenance.actual_start = datetime.fromisoformat(maintenance_data['actual_start'].replace('Z', '+00:00'))
|
||||
if 'actual_end' in maintenance_data:
|
||||
maintenance.actual_end = datetime.fromisoformat(maintenance_data['actual_end'].replace('Z', '+00:00'))
|
||||
if 'completion_notes' in maintenance_data:
|
||||
maintenance.completion_notes = maintenance_data['completion_notes']
|
||||
if 'actual_cost' in maintenance_data:
|
||||
maintenance.actual_cost = maintenance_data['actual_cost']
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Maintenance record updated successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ==================== Housekeeping Tasks ====================
|
||||
|
||||
@router.get('/housekeeping')
|
||||
async def get_housekeeping_tasks(
|
||||
room_id: Optional[int] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
task_type: Optional[str] = Query(None),
|
||||
date: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get housekeeping tasks with filtering"""
|
||||
try:
|
||||
# Check if user is staff (not admin) - staff should only see their assigned tasks
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
is_staff = role and role.name == 'staff'
|
||||
|
||||
query = db.query(HousekeepingTask)
|
||||
|
||||
# Filter by assigned_to for staff users
|
||||
if is_staff:
|
||||
query = query.filter(HousekeepingTask.assigned_to == current_user.id)
|
||||
|
||||
if room_id:
|
||||
query = query.filter(HousekeepingTask.room_id == room_id)
|
||||
if status:
|
||||
query = query.filter(HousekeepingTask.status == HousekeepingStatus(status))
|
||||
if task_type:
|
||||
query = query.filter(HousekeepingTask.task_type == HousekeepingType(task_type))
|
||||
if date:
|
||||
date_obj = datetime.fromisoformat(date.replace('Z', '+00:00')).date()
|
||||
query = query.filter(func.date(HousekeepingTask.scheduled_time) == date_obj)
|
||||
|
||||
total = query.count()
|
||||
query = query.order_by(HousekeepingTask.scheduled_time)
|
||||
|
||||
offset = (page - 1) * limit
|
||||
tasks = query.offset(offset).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for task in tasks:
|
||||
result.append({
|
||||
'id': task.id,
|
||||
'room_id': task.room_id,
|
||||
'room_number': task.room.room_number if task.room else None,
|
||||
'booking_id': task.booking_id,
|
||||
'task_type': task.task_type.value,
|
||||
'status': task.status.value,
|
||||
'scheduled_time': task.scheduled_time.isoformat() if task.scheduled_time else None,
|
||||
'started_at': task.started_at.isoformat() if task.started_at else None,
|
||||
'completed_at': task.completed_at.isoformat() if task.completed_at else None,
|
||||
'assigned_to': task.assigned_to,
|
||||
'assigned_staff_name': task.assigned_staff.full_name if task.assigned_staff else None,
|
||||
'checklist_items': task.checklist_items,
|
||||
'notes': task.notes,
|
||||
'quality_score': task.quality_score,
|
||||
'estimated_duration_minutes': task.estimated_duration_minutes,
|
||||
'actual_duration_minutes': task.actual_duration_minutes
|
||||
})
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'tasks': result,
|
||||
'pagination': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'total_pages': (total + limit - 1) // limit
|
||||
}
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/housekeeping')
|
||||
async def create_housekeeping_task(
|
||||
task_data: dict,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new housekeeping task"""
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == task_data.get('room_id')).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
|
||||
scheduled_time = datetime.fromisoformat(task_data['scheduled_time'].replace('Z', '+00:00'))
|
||||
assigned_to = task_data.get('assigned_to')
|
||||
|
||||
task = HousekeepingTask(
|
||||
room_id=task_data['room_id'],
|
||||
booking_id=task_data.get('booking_id'),
|
||||
task_type=HousekeepingType(task_data.get('task_type', 'vacant')),
|
||||
status=HousekeepingStatus(task_data.get('status', 'pending')),
|
||||
scheduled_time=scheduled_time,
|
||||
assigned_to=assigned_to,
|
||||
created_by=current_user.id,
|
||||
checklist_items=task_data.get('checklist_items', []),
|
||||
notes=task_data.get('notes'),
|
||||
estimated_duration_minutes=task_data.get('estimated_duration_minutes')
|
||||
)
|
||||
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
|
||||
# Send notification to assigned staff member if task is assigned
|
||||
if assigned_to:
|
||||
try:
|
||||
from ..routes.chat_routes import manager
|
||||
assigned_staff = db.query(User).filter(User.id == assigned_to).first()
|
||||
task_data_notification = {
|
||||
'id': task.id,
|
||||
'room_id': task.room_id,
|
||||
'room_number': room.room_number,
|
||||
'task_type': task.task_type.value,
|
||||
'status': task.status.value,
|
||||
'scheduled_time': task.scheduled_time.isoformat() if task.scheduled_time else None,
|
||||
'assigned_to': task.assigned_to,
|
||||
'created_at': task.created_at.isoformat() if task.created_at else None
|
||||
}
|
||||
notification_data = {
|
||||
'type': 'housekeeping_task_assigned',
|
||||
'data': task_data_notification
|
||||
}
|
||||
# Send notification to the specific staff member
|
||||
if assigned_to in manager.staff_connections:
|
||||
try:
|
||||
await manager.staff_connections[assigned_to].send_json(notification_data)
|
||||
except Exception as e:
|
||||
logger.error(f'Error sending housekeeping task notification to staff {assigned_to}: {str(e)}', exc_info=True, extra={'staff_id': assigned_to})
|
||||
except Exception as e:
|
||||
logger.error(f'Error setting up housekeeping task notification: {str(e)}', exc_info=True)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Housekeeping task created successfully',
|
||||
'data': {'task_id': task.id}
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put('/housekeeping/{task_id}')
|
||||
async def update_housekeeping_task(
|
||||
task_id: int,
|
||||
task_data: dict,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a housekeeping task"""
|
||||
try:
|
||||
task = db.query(HousekeepingTask).filter(HousekeepingTask.id == task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail='Housekeeping task not found')
|
||||
|
||||
# Check if user is staff (not admin) - staff can only update their own assigned tasks
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
is_staff = role and role.name == 'staff'
|
||||
|
||||
if is_staff:
|
||||
# Staff can only update tasks assigned to them
|
||||
if task.assigned_to != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='You can only update tasks assigned to you')
|
||||
# Staff cannot change assignment
|
||||
if 'assigned_to' in task_data and task_data.get('assigned_to') != task.assigned_to:
|
||||
raise HTTPException(status_code=403, detail='You cannot change task assignment')
|
||||
|
||||
old_assigned_to = task.assigned_to
|
||||
assigned_to_changed = False
|
||||
|
||||
if 'assigned_to' in task_data and not is_staff:
|
||||
new_assigned_to = task_data.get('assigned_to')
|
||||
if new_assigned_to != old_assigned_to:
|
||||
task.assigned_to = new_assigned_to
|
||||
assigned_to_changed = True
|
||||
|
||||
if 'status' in task_data:
|
||||
new_status = HousekeepingStatus(task_data['status'])
|
||||
|
||||
# Only the assigned user can mark the task as completed
|
||||
if new_status == HousekeepingStatus.completed:
|
||||
if not task.assigned_to:
|
||||
raise HTTPException(status_code=400, detail='Task must be assigned before it can be marked as completed')
|
||||
if task.assigned_to != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Only the assigned staff member can mark this task as completed')
|
||||
|
||||
task.status = new_status
|
||||
|
||||
if new_status == HousekeepingStatus.in_progress and not task.started_at:
|
||||
task.started_at = datetime.utcnow()
|
||||
elif new_status == HousekeepingStatus.completed and not task.completed_at:
|
||||
task.completed_at = datetime.utcnow()
|
||||
if task.started_at:
|
||||
duration = (task.completed_at - task.started_at).total_seconds() / 60
|
||||
task.actual_duration_minutes = int(duration)
|
||||
|
||||
if 'checklist_items' in task_data:
|
||||
task.checklist_items = task_data['checklist_items']
|
||||
if 'notes' in task_data:
|
||||
task.notes = task_data['notes']
|
||||
if 'issues_found' in task_data:
|
||||
task.issues_found = task_data['issues_found']
|
||||
if 'quality_score' in task_data:
|
||||
task.quality_score = task_data['quality_score']
|
||||
if 'inspected_by' in task_data:
|
||||
task.inspected_by = task_data['inspected_by']
|
||||
task.inspected_at = datetime.utcnow()
|
||||
if 'inspection_notes' in task_data:
|
||||
task.inspection_notes = task_data['inspection_notes']
|
||||
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
|
||||
# Send notification if assignment changed
|
||||
if assigned_to_changed and task.assigned_to:
|
||||
try:
|
||||
from ..routes.chat_routes import manager
|
||||
room = db.query(Room).filter(Room.id == task.room_id).first()
|
||||
task_data_notification = {
|
||||
'id': task.id,
|
||||
'room_id': task.room_id,
|
||||
'room_number': room.room_number if room else None,
|
||||
'task_type': task.task_type.value,
|
||||
'status': task.status.value,
|
||||
'scheduled_time': task.scheduled_time.isoformat() if task.scheduled_time else None,
|
||||
'assigned_to': task.assigned_to,
|
||||
'updated_at': task.updated_at.isoformat() if task.updated_at else None
|
||||
}
|
||||
notification_data = {
|
||||
'type': 'housekeeping_task_assigned',
|
||||
'data': task_data_notification
|
||||
}
|
||||
# Send notification to the newly assigned staff member
|
||||
if task.assigned_to in manager.staff_connections:
|
||||
try:
|
||||
await manager.staff_connections[task.assigned_to].send_json(notification_data)
|
||||
except Exception as e:
|
||||
logger.error(f'Error sending housekeeping task notification to staff {task.assigned_to}: {str(e)}', exc_info=True, extra={'staff_id': task.assigned_to})
|
||||
except Exception as e:
|
||||
logger.error(f'Error setting up housekeeping task notification: {str(e)}', exc_info=True)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Housekeeping task updated successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ==================== Room Inspections ====================
|
||||
|
||||
@router.get('/inspections')
|
||||
async def get_room_inspections(
|
||||
room_id: Optional[int] = Query(None),
|
||||
inspection_type: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get room inspections with filtering"""
|
||||
try:
|
||||
# Check if user is staff (not admin) - staff should only see their assigned inspections
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
is_staff = role and role.name == 'staff'
|
||||
|
||||
query = db.query(RoomInspection)
|
||||
|
||||
# Filter by inspected_by for staff users
|
||||
if is_staff:
|
||||
query = query.filter(RoomInspection.inspected_by == current_user.id)
|
||||
|
||||
if room_id:
|
||||
query = query.filter(RoomInspection.room_id == room_id)
|
||||
if inspection_type:
|
||||
query = query.filter(RoomInspection.inspection_type == InspectionType(inspection_type))
|
||||
if status:
|
||||
query = query.filter(RoomInspection.status == InspectionStatus(status))
|
||||
|
||||
total = query.count()
|
||||
query = query.order_by(desc(RoomInspection.scheduled_at))
|
||||
|
||||
offset = (page - 1) * limit
|
||||
inspections = query.offset(offset).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for inspection in inspections:
|
||||
result.append({
|
||||
'id': inspection.id,
|
||||
'room_id': inspection.room_id,
|
||||
'room_number': inspection.room.room_number if inspection.room else None,
|
||||
'booking_id': inspection.booking_id,
|
||||
'inspection_type': inspection.inspection_type.value,
|
||||
'status': inspection.status.value,
|
||||
'scheduled_at': inspection.scheduled_at.isoformat() if inspection.scheduled_at else None,
|
||||
'started_at': inspection.started_at.isoformat() if inspection.started_at else None,
|
||||
'completed_at': inspection.completed_at.isoformat() if inspection.completed_at else None,
|
||||
'inspected_by': inspection.inspected_by,
|
||||
'inspector_name': inspection.inspector.full_name if inspection.inspector else None,
|
||||
'checklist_items': inspection.checklist_items,
|
||||
'overall_score': float(inspection.overall_score) if inspection.overall_score else None,
|
||||
'overall_notes': inspection.overall_notes,
|
||||
'issues_found': inspection.issues_found,
|
||||
'requires_followup': inspection.requires_followup,
|
||||
'created_at': inspection.created_at.isoformat() if inspection.created_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'inspections': result,
|
||||
'pagination': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'total_pages': (total + limit - 1) // limit
|
||||
}
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/inspections')
|
||||
async def create_room_inspection(
|
||||
inspection_data: dict,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new room inspection"""
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == inspection_data.get('room_id')).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
|
||||
scheduled_at = datetime.fromisoformat(inspection_data['scheduled_at'].replace('Z', '+00:00'))
|
||||
|
||||
inspection = RoomInspection(
|
||||
room_id=inspection_data['room_id'],
|
||||
booking_id=inspection_data.get('booking_id'),
|
||||
inspection_type=InspectionType(inspection_data.get('inspection_type', 'routine')),
|
||||
status=InspectionStatus(inspection_data.get('status', 'pending')),
|
||||
scheduled_at=scheduled_at,
|
||||
inspected_by=inspection_data.get('inspected_by'),
|
||||
created_by=current_user.id,
|
||||
checklist_items=inspection_data.get('checklist_items', []),
|
||||
checklist_template_id=inspection_data.get('checklist_template_id')
|
||||
)
|
||||
|
||||
db.add(inspection)
|
||||
db.commit()
|
||||
db.refresh(inspection)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Room inspection created successfully',
|
||||
'data': {'inspection_id': inspection.id}
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put('/inspections/{inspection_id}')
|
||||
async def update_room_inspection(
|
||||
inspection_id: int,
|
||||
inspection_data: dict,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a room inspection"""
|
||||
try:
|
||||
inspection = db.query(RoomInspection).filter(RoomInspection.id == inspection_id).first()
|
||||
if not inspection:
|
||||
raise HTTPException(status_code=404, detail='Room inspection not found')
|
||||
|
||||
# Check if user is staff (not admin) - staff can only update their own assigned inspections
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
is_staff = role and role.name == 'staff'
|
||||
|
||||
if is_staff:
|
||||
# Staff can only update inspections assigned to them
|
||||
if inspection.inspected_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='You can only update inspections assigned to you')
|
||||
# Staff can only update status and inspection results
|
||||
allowed_fields = {'status', 'checklist_items', 'overall_score', 'overall_notes', 'issues_found', 'requires_followup', 'followup_notes'}
|
||||
if any(key not in allowed_fields for key in inspection_data.keys()):
|
||||
raise HTTPException(status_code=403, detail='You can only update status and inspection results')
|
||||
|
||||
if 'status' in inspection_data:
|
||||
new_status = InspectionStatus(inspection_data['status'])
|
||||
|
||||
# Only the assigned user can mark the inspection as completed
|
||||
if new_status == InspectionStatus.completed:
|
||||
if not inspection.inspected_by:
|
||||
raise HTTPException(status_code=400, detail='Inspection must be assigned before it can be marked as completed')
|
||||
if inspection.inspected_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Only the assigned inspector can mark this inspection as completed')
|
||||
|
||||
inspection.status = new_status
|
||||
|
||||
if new_status == InspectionStatus.in_progress and not inspection.started_at:
|
||||
inspection.started_at = datetime.utcnow()
|
||||
elif new_status == InspectionStatus.completed and not inspection.completed_at:
|
||||
inspection.completed_at = datetime.utcnow()
|
||||
|
||||
if 'checklist_items' in inspection_data:
|
||||
inspection.checklist_items = inspection_data['checklist_items']
|
||||
if 'overall_score' in inspection_data:
|
||||
inspection.overall_score = inspection_data['overall_score']
|
||||
if 'overall_notes' in inspection_data:
|
||||
inspection.overall_notes = inspection_data['overall_notes']
|
||||
if 'issues_found' in inspection_data:
|
||||
inspection.issues_found = inspection_data['issues_found']
|
||||
if 'photos' in inspection_data:
|
||||
inspection.photos = inspection_data['photos']
|
||||
if 'requires_followup' in inspection_data:
|
||||
inspection.requires_followup = inspection_data['requires_followup']
|
||||
if 'followup_notes' in inspection_data:
|
||||
inspection.followup_notes = inspection_data['followup_notes']
|
||||
if 'maintenance_request_id' in inspection_data:
|
||||
inspection.maintenance_request_id = inspection_data['maintenance_request_id']
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Room inspection updated successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ==================== Room Status Board ====================
|
||||
|
||||
@router.get('/status-board')
|
||||
async def get_room_status_board(
|
||||
floor: Optional[int] = Query(None),
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get visual room status board with all rooms and their current status"""
|
||||
try:
|
||||
query = db.query(Room).options(joinedload(Room.room_type))
|
||||
if floor:
|
||||
query = query.filter(Room.floor == floor)
|
||||
|
||||
rooms = query.order_by(Room.floor, Room.room_number).all()
|
||||
|
||||
result = []
|
||||
for room in rooms:
|
||||
# Get current booking if any
|
||||
# Use load_only to avoid querying columns that don't exist in the database (rate_plan_id, group_booking_id)
|
||||
current_booking = db.query(Booking).options(
|
||||
joinedload(Booking.user),
|
||||
load_only(Booking.id, Booking.user_id, Booking.room_id, Booking.check_in_date, Booking.check_out_date, Booking.status)
|
||||
).filter(
|
||||
and_(
|
||||
Booking.room_id == room.id,
|
||||
Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]),
|
||||
Booking.check_in_date <= datetime.utcnow(),
|
||||
Booking.check_out_date > datetime.utcnow()
|
||||
)
|
||||
).first()
|
||||
|
||||
# Get active maintenance
|
||||
active_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == room.id,
|
||||
RoomMaintenance.blocks_room == True,
|
||||
RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress])
|
||||
)
|
||||
).first()
|
||||
|
||||
# Get pending housekeeping tasks
|
||||
pending_housekeeping = db.query(HousekeepingTask).filter(
|
||||
and_(
|
||||
HousekeepingTask.room_id == room.id,
|
||||
HousekeepingTask.status == HousekeepingStatus.pending,
|
||||
func.date(HousekeepingTask.scheduled_time) == datetime.utcnow().date()
|
||||
)
|
||||
).count()
|
||||
|
||||
result.append({
|
||||
'id': room.id,
|
||||
'room_number': room.room_number,
|
||||
'floor': room.floor,
|
||||
'status': room.status.value,
|
||||
'room_type': room.room_type.name if room.room_type else None,
|
||||
'current_booking': {
|
||||
'id': current_booking.id,
|
||||
'guest_name': current_booking.user.full_name if current_booking.user else 'Unknown',
|
||||
'check_out': current_booking.check_out_date.isoformat()
|
||||
} if current_booking else None,
|
||||
'active_maintenance': {
|
||||
'id': active_maintenance.id,
|
||||
'title': active_maintenance.title,
|
||||
'type': active_maintenance.maintenance_type.value
|
||||
} if active_maintenance else None,
|
||||
'pending_housekeeping_count': pending_housekeeping
|
||||
})
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {'rooms': result}
|
||||
}
|
||||
except Exception as e:
|
||||
import logging
|
||||
import traceback
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f'Error in get_room_status_board: {str(e)}')
|
||||
logger.error(f'Traceback: {traceback.format_exc()}')
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
496
Backend/src/rooms/routes/rate_plan_routes.py
Normal file
496
Backend/src/rooms/routes/rate_plan_routes.py
Normal file
@@ -0,0 +1,496 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, and_
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.rate_plan import RatePlan, RatePlanRule, RatePlanType, RatePlanStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ...bookings.models.booking import Booking
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional as Opt
|
||||
|
||||
router = APIRouter(prefix='/rate-plans', tags=['rate-plans'])
|
||||
|
||||
# Pydantic models for request/response
|
||||
class RatePlanRuleCreate(BaseModel):
|
||||
rule_type: str
|
||||
rule_key: str
|
||||
rule_value: Optional[dict] = None
|
||||
price_modifier: Optional[float] = None
|
||||
discount_percentage: Optional[float] = None
|
||||
fixed_adjustment: Optional[float] = None
|
||||
priority: int = 100
|
||||
|
||||
class RatePlanCreate(BaseModel):
|
||||
name: str
|
||||
code: str
|
||||
description: Optional[str] = None
|
||||
plan_type: str
|
||||
status: str = 'active'
|
||||
base_price_modifier: float = 1.0
|
||||
discount_percentage: Optional[float] = None
|
||||
fixed_discount: Optional[float] = None
|
||||
room_type_id: Optional[int] = None
|
||||
min_nights: Optional[int] = None
|
||||
max_nights: Optional[int] = None
|
||||
advance_days_required: Optional[int] = None
|
||||
valid_from: Optional[str] = None
|
||||
valid_to: Optional[str] = None
|
||||
is_refundable: bool = True
|
||||
requires_deposit: bool = False
|
||||
deposit_percentage: Optional[float] = None
|
||||
cancellation_hours: Optional[int] = None
|
||||
corporate_code: Optional[str] = None
|
||||
requires_verification: bool = False
|
||||
verification_type: Optional[str] = None
|
||||
long_stay_nights: Optional[int] = None
|
||||
is_package: bool = False
|
||||
package_id: Optional[int] = None
|
||||
priority: int = 100
|
||||
extra_data: Optional[dict] = None
|
||||
rules: Optional[List[RatePlanRuleCreate]] = []
|
||||
|
||||
class RatePlanUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
base_price_modifier: Optional[float] = None
|
||||
discount_percentage: Optional[float] = None
|
||||
fixed_discount: Optional[float] = None
|
||||
room_type_id: Optional[int] = None
|
||||
min_nights: Optional[int] = None
|
||||
max_nights: Optional[int] = None
|
||||
advance_days_required: Optional[int] = None
|
||||
valid_from: Optional[str] = None
|
||||
valid_to: Optional[str] = None
|
||||
is_refundable: Optional[bool] = None
|
||||
requires_deposit: Optional[bool] = None
|
||||
deposit_percentage: Optional[float] = None
|
||||
cancellation_hours: Optional[int] = None
|
||||
corporate_code: Optional[str] = None
|
||||
requires_verification: Optional[bool] = None
|
||||
verification_type: Optional[str] = None
|
||||
long_stay_nights: Optional[int] = None
|
||||
package_id: Optional[int] = None
|
||||
priority: Optional[int] = None
|
||||
extra_data: Optional[dict] = None
|
||||
|
||||
@router.get('/')
|
||||
async def get_rate_plans(
|
||||
search: Optional[str] = Query(None),
|
||||
status_filter: Optional[str] = Query(None, alias='status'),
|
||||
plan_type: Optional[str] = Query(None),
|
||||
room_type_id: Optional[int] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
try:
|
||||
query = db.query(RatePlan)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
RatePlan.name.like(f'%{search}%'),
|
||||
RatePlan.code.like(f'%{search}%'),
|
||||
RatePlan.description.like(f'%{search}%')
|
||||
)
|
||||
)
|
||||
|
||||
if status_filter:
|
||||
try:
|
||||
query = query.filter(RatePlan.status == RatePlanStatus(status_filter))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if plan_type:
|
||||
try:
|
||||
query = query.filter(RatePlan.plan_type == RatePlanType(plan_type))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if room_type_id:
|
||||
query = query.filter(
|
||||
or_(
|
||||
RatePlan.room_type_id == room_type_id,
|
||||
RatePlan.room_type_id.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
offset = (page - 1) * limit
|
||||
rate_plans = query.order_by(RatePlan.priority.asc(), RatePlan.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for plan in rate_plans:
|
||||
plan_dict = {
|
||||
'id': plan.id,
|
||||
'name': plan.name,
|
||||
'code': plan.code,
|
||||
'description': plan.description,
|
||||
'plan_type': plan.plan_type.value if isinstance(plan.plan_type, RatePlanType) else plan.plan_type,
|
||||
'status': plan.status.value if isinstance(plan.status, RatePlanStatus) else plan.status,
|
||||
'base_price_modifier': float(plan.base_price_modifier) if plan.base_price_modifier else 1.0,
|
||||
'discount_percentage': float(plan.discount_percentage) if plan.discount_percentage else None,
|
||||
'fixed_discount': float(plan.fixed_discount) if plan.fixed_discount else None,
|
||||
'room_type_id': plan.room_type_id,
|
||||
'room_type_name': plan.room_type.name if plan.room_type else None,
|
||||
'min_nights': plan.min_nights,
|
||||
'max_nights': plan.max_nights,
|
||||
'advance_days_required': plan.advance_days_required,
|
||||
'valid_from': plan.valid_from.isoformat() if plan.valid_from else None,
|
||||
'valid_to': plan.valid_to.isoformat() if plan.valid_to else None,
|
||||
'is_refundable': plan.is_refundable,
|
||||
'requires_deposit': plan.requires_deposit,
|
||||
'deposit_percentage': float(plan.deposit_percentage) if plan.deposit_percentage else None,
|
||||
'cancellation_hours': plan.cancellation_hours,
|
||||
'corporate_code': plan.corporate_code,
|
||||
'requires_verification': plan.requires_verification,
|
||||
'verification_type': plan.verification_type,
|
||||
'long_stay_nights': plan.long_stay_nights,
|
||||
'is_package': plan.is_package,
|
||||
'package_id': plan.package_id,
|
||||
'priority': plan.priority,
|
||||
'extra_data': plan.extra_data,
|
||||
'created_at': plan.created_at.isoformat() if plan.created_at else None,
|
||||
'updated_at': plan.updated_at.isoformat() if plan.updated_at else None,
|
||||
}
|
||||
result.append(plan_dict)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'rate_plans': result,
|
||||
'pagination': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'totalPages': (total + limit - 1) // limit
|
||||
}
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/{id}')
|
||||
async def get_rate_plan(id: int, db: Session = Depends(get_db)):
|
||||
try:
|
||||
plan = db.query(RatePlan).filter(RatePlan.id == id).first()
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail='Rate plan not found')
|
||||
|
||||
rules = db.query(RatePlanRule).filter(RatePlanRule.rate_plan_id == id).order_by(RatePlanRule.priority.asc()).all()
|
||||
|
||||
plan_dict = {
|
||||
'id': plan.id,
|
||||
'name': plan.name,
|
||||
'code': plan.code,
|
||||
'description': plan.description,
|
||||
'plan_type': plan.plan_type.value if isinstance(plan.plan_type, RatePlanType) else plan.plan_type,
|
||||
'status': plan.status.value if isinstance(plan.status, RatePlanStatus) else plan.status,
|
||||
'base_price_modifier': float(plan.base_price_modifier) if plan.base_price_modifier else 1.0,
|
||||
'discount_percentage': float(plan.discount_percentage) if plan.discount_percentage else None,
|
||||
'fixed_discount': float(plan.fixed_discount) if plan.fixed_discount else None,
|
||||
'room_type_id': plan.room_type_id,
|
||||
'room_type_name': plan.room_type.name if plan.room_type else None,
|
||||
'min_nights': plan.min_nights,
|
||||
'max_nights': plan.max_nights,
|
||||
'advance_days_required': plan.advance_days_required,
|
||||
'valid_from': plan.valid_from.isoformat() if plan.valid_from else None,
|
||||
'valid_to': plan.valid_to.isoformat() if plan.valid_to else None,
|
||||
'is_refundable': plan.is_refundable,
|
||||
'requires_deposit': plan.requires_deposit,
|
||||
'deposit_percentage': float(plan.deposit_percentage) if plan.deposit_percentage else None,
|
||||
'cancellation_hours': plan.cancellation_hours,
|
||||
'corporate_code': plan.corporate_code,
|
||||
'requires_verification': plan.requires_verification,
|
||||
'verification_type': plan.verification_type,
|
||||
'long_stay_nights': plan.long_stay_nights,
|
||||
'is_package': plan.is_package,
|
||||
'package_id': plan.package_id,
|
||||
'priority': plan.priority,
|
||||
'extra_data': plan.extra_data,
|
||||
'rules': [
|
||||
{
|
||||
'id': rule.id,
|
||||
'rule_type': rule.rule_type,
|
||||
'rule_key': rule.rule_key,
|
||||
'rule_value': rule.rule_value,
|
||||
'price_modifier': float(rule.price_modifier) if rule.price_modifier else None,
|
||||
'discount_percentage': float(rule.discount_percentage) if rule.discount_percentage else None,
|
||||
'fixed_adjustment': float(rule.fixed_adjustment) if rule.fixed_adjustment else None,
|
||||
'priority': rule.priority,
|
||||
}
|
||||
for rule in rules
|
||||
],
|
||||
'created_at': plan.created_at.isoformat() if plan.created_at else None,
|
||||
'updated_at': plan.updated_at.isoformat() if plan.updated_at else None,
|
||||
}
|
||||
|
||||
return {'status': 'success', 'data': {'rate_plan': plan_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def create_rate_plan(plan_data: RatePlanCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
try:
|
||||
# Check if code already exists
|
||||
existing = db.query(RatePlan).filter(RatePlan.code == plan_data.code).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail='Rate plan code already exists')
|
||||
|
||||
# Validate room_type_id if provided
|
||||
if plan_data.room_type_id:
|
||||
room_type = db.query(RoomType).filter(RoomType.id == plan_data.room_type_id).first()
|
||||
if not room_type:
|
||||
raise HTTPException(status_code=404, detail='Room type not found')
|
||||
|
||||
# Create rate plan
|
||||
rate_plan = RatePlan(
|
||||
name=plan_data.name,
|
||||
code=plan_data.code,
|
||||
description=plan_data.description,
|
||||
plan_type=RatePlanType(plan_data.plan_type),
|
||||
status=RatePlanStatus(plan_data.status),
|
||||
base_price_modifier=Decimal(str(plan_data.base_price_modifier)),
|
||||
discount_percentage=Decimal(str(plan_data.discount_percentage)) if plan_data.discount_percentage else None,
|
||||
fixed_discount=Decimal(str(plan_data.fixed_discount)) if plan_data.fixed_discount else None,
|
||||
room_type_id=plan_data.room_type_id,
|
||||
min_nights=plan_data.min_nights,
|
||||
max_nights=plan_data.max_nights,
|
||||
advance_days_required=plan_data.advance_days_required,
|
||||
valid_from=datetime.strptime(plan_data.valid_from, '%Y-%m-%d').date() if plan_data.valid_from else None,
|
||||
valid_to=datetime.strptime(plan_data.valid_to, '%Y-%m-%d').date() if plan_data.valid_to else None,
|
||||
is_refundable=plan_data.is_refundable,
|
||||
requires_deposit=plan_data.requires_deposit,
|
||||
deposit_percentage=Decimal(str(plan_data.deposit_percentage)) if plan_data.deposit_percentage else None,
|
||||
cancellation_hours=plan_data.cancellation_hours,
|
||||
corporate_code=plan_data.corporate_code,
|
||||
requires_verification=plan_data.requires_verification,
|
||||
verification_type=plan_data.verification_type,
|
||||
long_stay_nights=plan_data.long_stay_nights,
|
||||
is_package=plan_data.is_package,
|
||||
package_id=plan_data.package_id,
|
||||
priority=plan_data.priority,
|
||||
extra_data=plan_data.extra_data,
|
||||
)
|
||||
|
||||
db.add(rate_plan)
|
||||
db.flush()
|
||||
|
||||
# Create rules
|
||||
if plan_data.rules:
|
||||
for rule_data in plan_data.rules:
|
||||
rule = RatePlanRule(
|
||||
rate_plan_id=rate_plan.id,
|
||||
rule_type=rule_data.rule_type,
|
||||
rule_key=rule_data.rule_key,
|
||||
rule_value=rule_data.rule_value,
|
||||
price_modifier=Decimal(str(rule_data.price_modifier)) if rule_data.price_modifier else None,
|
||||
discount_percentage=Decimal(str(rule_data.discount_percentage)) if rule_data.discount_percentage else None,
|
||||
fixed_adjustment=Decimal(str(rule_data.fixed_adjustment)) if rule_data.fixed_adjustment else None,
|
||||
priority=rule_data.priority,
|
||||
)
|
||||
db.add(rule)
|
||||
|
||||
db.commit()
|
||||
db.refresh(rate_plan)
|
||||
|
||||
return {'status': 'success', 'message': 'Rate plan created successfully', 'data': {'rate_plan_id': rate_plan.id}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}')
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def update_rate_plan(id: int, plan_data: RatePlanUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
try:
|
||||
rate_plan = db.query(RatePlan).filter(RatePlan.id == id).first()
|
||||
if not rate_plan:
|
||||
raise HTTPException(status_code=404, detail='Rate plan not found')
|
||||
|
||||
# Update fields
|
||||
if plan_data.name is not None:
|
||||
rate_plan.name = plan_data.name
|
||||
if plan_data.description is not None:
|
||||
rate_plan.description = plan_data.description
|
||||
if plan_data.status is not None:
|
||||
rate_plan.status = RatePlanStatus(plan_data.status)
|
||||
if plan_data.base_price_modifier is not None:
|
||||
rate_plan.base_price_modifier = Decimal(str(plan_data.base_price_modifier))
|
||||
if plan_data.discount_percentage is not None:
|
||||
rate_plan.discount_percentage = Decimal(str(plan_data.discount_percentage))
|
||||
if plan_data.fixed_discount is not None:
|
||||
rate_plan.fixed_discount = Decimal(str(plan_data.fixed_discount))
|
||||
if plan_data.room_type_id is not None:
|
||||
if plan_data.room_type_id:
|
||||
room_type = db.query(RoomType).filter(RoomType.id == plan_data.room_type_id).first()
|
||||
if not room_type:
|
||||
raise HTTPException(status_code=404, detail='Room type not found')
|
||||
rate_plan.room_type_id = plan_data.room_type_id
|
||||
if plan_data.min_nights is not None:
|
||||
rate_plan.min_nights = plan_data.min_nights
|
||||
if plan_data.max_nights is not None:
|
||||
rate_plan.max_nights = plan_data.max_nights
|
||||
if plan_data.advance_days_required is not None:
|
||||
rate_plan.advance_days_required = plan_data.advance_days_required
|
||||
if plan_data.valid_from is not None:
|
||||
rate_plan.valid_from = datetime.strptime(plan_data.valid_from, '%Y-%m-%d').date() if plan_data.valid_from else None
|
||||
if plan_data.valid_to is not None:
|
||||
rate_plan.valid_to = datetime.strptime(plan_data.valid_to, '%Y-%m-%d').date() if plan_data.valid_to else None
|
||||
if plan_data.is_refundable is not None:
|
||||
rate_plan.is_refundable = plan_data.is_refundable
|
||||
if plan_data.requires_deposit is not None:
|
||||
rate_plan.requires_deposit = plan_data.requires_deposit
|
||||
if plan_data.deposit_percentage is not None:
|
||||
rate_plan.deposit_percentage = Decimal(str(plan_data.deposit_percentage)) if plan_data.deposit_percentage else None
|
||||
if plan_data.cancellation_hours is not None:
|
||||
rate_plan.cancellation_hours = plan_data.cancellation_hours
|
||||
if plan_data.corporate_code is not None:
|
||||
rate_plan.corporate_code = plan_data.corporate_code
|
||||
if plan_data.requires_verification is not None:
|
||||
rate_plan.requires_verification = plan_data.requires_verification
|
||||
if plan_data.verification_type is not None:
|
||||
rate_plan.verification_type = plan_data.verification_type
|
||||
if plan_data.long_stay_nights is not None:
|
||||
rate_plan.long_stay_nights = plan_data.long_stay_nights
|
||||
if plan_data.package_id is not None:
|
||||
rate_plan.package_id = plan_data.package_id
|
||||
if plan_data.priority is not None:
|
||||
rate_plan.priority = plan_data.priority
|
||||
if plan_data.extra_data is not None:
|
||||
rate_plan.extra_data = plan_data.extra_data
|
||||
|
||||
db.commit()
|
||||
|
||||
return {'status': 'success', 'message': 'Rate plan updated successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}')
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def delete_rate_plan(id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
try:
|
||||
rate_plan = db.query(RatePlan).filter(RatePlan.id == id).first()
|
||||
if not rate_plan:
|
||||
raise HTTPException(status_code=404, detail='Rate plan not found')
|
||||
|
||||
# Check if rate plan is used in bookings
|
||||
booking_count = db.query(Booking).filter(Booking.rate_plan_id == id).count()
|
||||
if booking_count > 0:
|
||||
raise HTTPException(status_code=400, detail=f'Cannot delete rate plan. It is used in {booking_count} booking(s)')
|
||||
|
||||
# Delete rules first
|
||||
db.query(RatePlanRule).filter(RatePlanRule.rate_plan_id == id).delete()
|
||||
|
||||
db.delete(rate_plan)
|
||||
db.commit()
|
||||
|
||||
return {'status': 'success', 'message': 'Rate plan deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/available/{room_type_id}')
|
||||
async def get_available_rate_plans(
|
||||
room_type_id: int,
|
||||
check_in: str = Query(...),
|
||||
check_out: str = Query(...),
|
||||
num_nights: Optional[int] = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get available rate plans for a room type and date range"""
|
||||
try:
|
||||
check_in_date = datetime.strptime(check_in, '%Y-%m-%d').date()
|
||||
check_out_date = datetime.strptime(check_out, '%Y-%m-%d').date()
|
||||
|
||||
if num_nights is None:
|
||||
num_nights = (check_out_date - check_in_date).days
|
||||
|
||||
today = date.today()
|
||||
advance_days = (check_in_date - today).days
|
||||
|
||||
# Query rate plans
|
||||
query = db.query(RatePlan).filter(
|
||||
RatePlan.status == RatePlanStatus.active,
|
||||
or_(
|
||||
RatePlan.room_type_id == room_type_id,
|
||||
RatePlan.room_type_id.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
# Filter by date range
|
||||
query = query.filter(
|
||||
or_(
|
||||
RatePlan.valid_from.is_(None),
|
||||
RatePlan.valid_from <= check_in_date
|
||||
),
|
||||
or_(
|
||||
RatePlan.valid_to.is_(None),
|
||||
RatePlan.valid_to >= check_out_date
|
||||
)
|
||||
)
|
||||
|
||||
# Filter by advance days
|
||||
query = query.filter(
|
||||
or_(
|
||||
RatePlan.advance_days_required.is_(None),
|
||||
RatePlan.advance_days_required <= advance_days
|
||||
)
|
||||
)
|
||||
|
||||
# Filter by nights
|
||||
query = query.filter(
|
||||
or_(
|
||||
RatePlan.min_nights.is_(None),
|
||||
RatePlan.min_nights <= num_nights
|
||||
),
|
||||
or_(
|
||||
RatePlan.max_nights.is_(None),
|
||||
RatePlan.max_nights >= num_nights
|
||||
)
|
||||
)
|
||||
|
||||
rate_plans = query.order_by(RatePlan.priority.asc()).all()
|
||||
|
||||
result = []
|
||||
for plan in rate_plans:
|
||||
plan_dict = {
|
||||
'id': plan.id,
|
||||
'name': plan.name,
|
||||
'code': plan.code,
|
||||
'description': plan.description,
|
||||
'plan_type': plan.plan_type.value if isinstance(plan.plan_type, RatePlanType) else plan.plan_type,
|
||||
'base_price_modifier': float(plan.base_price_modifier) if plan.base_price_modifier else 1.0,
|
||||
'discount_percentage': float(plan.discount_percentage) if plan.discount_percentage else None,
|
||||
'fixed_discount': float(plan.fixed_discount) if plan.fixed_discount else None,
|
||||
'is_refundable': plan.is_refundable,
|
||||
'requires_deposit': plan.requires_deposit,
|
||||
'deposit_percentage': float(plan.deposit_percentage) if plan.deposit_percentage else None,
|
||||
'cancellation_hours': plan.cancellation_hours,
|
||||
'requires_verification': plan.requires_verification,
|
||||
'verification_type': plan.verification_type,
|
||||
}
|
||||
result.append(plan_dict)
|
||||
|
||||
return {'status': 'success', 'data': {'rate_plans': result}}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid date format: {str(e)}')
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
454
Backend/src/rooms/routes/room_routes.py
Normal file
454
Backend/src/rooms/routes/room_routes.py
Normal file
@@ -0,0 +1,454 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Request, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ...reviews.models.review import Review, ReviewStatus
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ..services.room_service import get_rooms_with_ratings, get_amenities_list, normalize_images, get_base_url
|
||||
import os
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/rooms', tags=['rooms'])
|
||||
|
||||
@router.get('/')
|
||||
async def get_rooms(request: Request, type: Optional[str]=Query(None), minPrice: Optional[float]=Query(None), maxPrice: Optional[float]=Query(None), capacity: Optional[int]=Query(None), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=1000), sort: Optional[str]=Query(None), featured: Optional[bool]=Query(None), db: Session=Depends(get_db)):
|
||||
try:
|
||||
where_clause = {}
|
||||
room_type_where = {}
|
||||
if featured is not None:
|
||||
where_clause['featured'] = featured
|
||||
if type:
|
||||
room_type_where['name'] = f'%{type}%'
|
||||
if capacity:
|
||||
room_type_where['capacity'] = capacity
|
||||
if minPrice or maxPrice:
|
||||
if minPrice:
|
||||
room_type_where['base_price_min'] = minPrice
|
||||
if maxPrice:
|
||||
room_type_where['base_price_max'] = maxPrice
|
||||
query = db.query(Room).join(RoomType)
|
||||
if where_clause.get('featured') is not None:
|
||||
query = query.filter(Room.featured == where_clause['featured'])
|
||||
if room_type_where.get('name'):
|
||||
query = query.filter(RoomType.name.like(room_type_where['name']))
|
||||
if room_type_where.get('capacity'):
|
||||
query = query.filter(RoomType.capacity >= room_type_where['capacity'])
|
||||
if room_type_where.get('base_price_min'):
|
||||
query = query.filter(RoomType.base_price >= room_type_where['base_price_min'])
|
||||
if room_type_where.get('base_price_max'):
|
||||
query = query.filter(RoomType.base_price <= room_type_where['base_price_max'])
|
||||
total = query.count()
|
||||
if sort == 'newest' or sort == 'created_at':
|
||||
query = query.order_by(Room.created_at.desc())
|
||||
else:
|
||||
query = query.order_by(Room.featured.desc(), Room.created_at.desc())
|
||||
offset = (page - 1) * limit
|
||||
rooms = query.offset(offset).limit(limit).all()
|
||||
base_url = get_base_url(request)
|
||||
rooms_with_ratings = await get_rooms_with_ratings(db, rooms, base_url)
|
||||
return {'status': 'success', 'data': {'rooms': rooms_with_ratings, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching rooms: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/amenities')
|
||||
async def get_amenities(db: Session=Depends(get_db)):
|
||||
try:
|
||||
amenities = await get_amenities_list(db)
|
||||
return {'status': 'success', 'data': {'amenities': amenities}}
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching amenities: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/available')
|
||||
async def search_available_rooms(request: Request, from_date: str=Query(..., alias='from'), to_date: str=Query(..., alias='to'), roomId: Optional[int]=Query(None, alias='roomId'), type: Optional[str]=Query(None), capacity: Optional[int]=Query(None), page: int=Query(1, ge=1), limit: int=Query(12, ge=1, le=100), db: Session=Depends(get_db)):
|
||||
try:
|
||||
try:
|
||||
if 'T' in from_date or 'Z' in from_date or '+' in from_date:
|
||||
check_in = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
|
||||
else:
|
||||
check_in = datetime.strptime(from_date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid from date format: {from_date}')
|
||||
try:
|
||||
if 'T' in to_date or 'Z' in to_date or '+' in to_date:
|
||||
check_out = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
|
||||
else:
|
||||
check_out = datetime.strptime(to_date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f'Invalid to date format: {to_date}')
|
||||
if roomId:
|
||||
room = db.query(Room).filter(Room.id == roomId).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
if room.status != RoomStatus.available:
|
||||
return {'status': 'success', 'data': {'available': False, 'message': 'Room is not available', 'room_id': roomId}}
|
||||
overlapping = db.query(Booking).filter(and_(Booking.room_id == roomId, Booking.status != BookingStatus.cancelled, Booking.check_in_date < check_out, Booking.check_out_date > check_in)).first()
|
||||
if overlapping:
|
||||
return {'status': 'success', 'data': {'available': False, 'message': 'Room is already booked for the selected dates', 'room_id': roomId}}
|
||||
|
||||
# Check for maintenance blocks
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
maintenance_block = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == roomId,
|
||||
RoomMaintenance.blocks_room == True,
|
||||
RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]),
|
||||
or_(
|
||||
and_(
|
||||
RoomMaintenance.block_start.isnot(None),
|
||||
RoomMaintenance.block_end.isnot(None),
|
||||
RoomMaintenance.block_start < check_out,
|
||||
RoomMaintenance.block_end > check_in
|
||||
),
|
||||
and_(
|
||||
RoomMaintenance.scheduled_start < check_out,
|
||||
RoomMaintenance.scheduled_end.isnot(None),
|
||||
RoomMaintenance.scheduled_end > check_in
|
||||
)
|
||||
)
|
||||
)
|
||||
).first()
|
||||
if maintenance_block:
|
||||
return {'status': 'success', 'data': {'available': False, 'message': f'Room is blocked for maintenance: {maintenance_block.title}', 'room_id': roomId}}
|
||||
|
||||
return {'status': 'success', 'data': {'available': True, 'message': 'Room is available', 'room_id': roomId}}
|
||||
if check_in >= check_out:
|
||||
raise HTTPException(status_code=400, detail='Check-out date must be after check-in date')
|
||||
query = db.query(Room).join(RoomType).filter(Room.status == RoomStatus.available)
|
||||
if type:
|
||||
query = query.filter(RoomType.name.like(f'%{type}%'))
|
||||
if capacity:
|
||||
query = query.filter(RoomType.capacity >= capacity)
|
||||
overlapping_rooms = db.query(Booking.room_id).filter(and_(Booking.status != BookingStatus.cancelled, Booking.check_in_date < check_out, Booking.check_out_date > check_in)).subquery()
|
||||
query = query.filter(~Room.id.in_(db.query(overlapping_rooms.c.room_id)))
|
||||
|
||||
# Exclude rooms blocked by maintenance
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
blocked_rooms = db.query(RoomMaintenance.room_id).filter(
|
||||
and_(
|
||||
RoomMaintenance.blocks_room == True,
|
||||
RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]),
|
||||
or_(
|
||||
and_(
|
||||
RoomMaintenance.block_start.isnot(None),
|
||||
RoomMaintenance.block_end.isnot(None),
|
||||
RoomMaintenance.block_start < check_out,
|
||||
RoomMaintenance.block_end > check_in
|
||||
),
|
||||
and_(
|
||||
RoomMaintenance.scheduled_start < check_out,
|
||||
RoomMaintenance.scheduled_end.isnot(None),
|
||||
RoomMaintenance.scheduled_end > check_in
|
||||
)
|
||||
)
|
||||
)
|
||||
).subquery()
|
||||
query = query.filter(~Room.id.in_(db.query(blocked_rooms.c.room_id)))
|
||||
total = query.count()
|
||||
query = query.order_by(Room.featured.desc(), Room.created_at.desc())
|
||||
offset = (page - 1) * limit
|
||||
rooms = query.offset(offset).limit(limit).all()
|
||||
base_url = get_base_url(request)
|
||||
rooms_with_ratings = await get_rooms_with_ratings(db, rooms, base_url)
|
||||
return {'status': 'success', 'data': {'rooms': rooms_with_ratings, 'search': {'from': from_date, 'to': to_date, 'type': type, 'capacity': capacity}, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f'Error searching available rooms: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/id/{id}')
|
||||
async def get_room_by_id(id: int, request: Request, db: Session=Depends(get_db)):
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
review_stats = db.query(func.avg(Review.rating).label('average_rating'), func.count(Review.id).label('total_reviews')).filter(and_(Review.room_id == room.id, Review.status == ReviewStatus.approved)).first()
|
||||
base_url = get_base_url(request)
|
||||
room_dict = {'id': room.id, 'room_type_id': room.room_type_id, 'room_number': room.room_number, 'floor': room.floor, 'status': room.status.value if isinstance(room.status, RoomStatus) else room.status, 'price': float(room.price) if room.price is not None and room.price > 0 else None, 'featured': room.featured, 'description': room.description, 'capacity': room.capacity, 'room_size': room.room_size, 'view': room.view, 'amenities': room.amenities, 'created_at': room.created_at.isoformat() if room.created_at else None, 'updated_at': room.updated_at.isoformat() if room.updated_at else None, 'average_rating': round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None, 'total_reviews': review_stats.total_reviews or 0 if review_stats else 0}
|
||||
try:
|
||||
room_dict['images'] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict['images'] = []
|
||||
if room.room_type:
|
||||
room_dict['room_type'] = {'id': room.room_type.id, 'name': room.room_type.name, 'description': room.room_type.description, 'base_price': float(room.room_type.base_price) if room.room_type.base_price else 0.0, 'capacity': room.room_type.capacity, 'amenities': room.room_type.amenities, 'images': []}
|
||||
return {'status': 'success', 'data': {'room': room_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/{room_number}')
|
||||
async def get_room_by_number(room_number: str, request: Request, db: Session=Depends(get_db)):
|
||||
try:
|
||||
room = db.query(Room).filter(Room.room_number == room_number).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
review_stats = db.query(func.avg(Review.rating).label('average_rating'), func.count(Review.id).label('total_reviews')).filter(and_(Review.room_id == room.id, Review.status == ReviewStatus.approved)).first()
|
||||
base_url = get_base_url(request)
|
||||
room_dict = {'id': room.id, 'room_type_id': room.room_type_id, 'room_number': room.room_number, 'floor': room.floor, 'status': room.status.value if isinstance(room.status, RoomStatus) else room.status, 'price': float(room.price) if room.price is not None and room.price > 0 else None, 'featured': room.featured, 'description': room.description, 'capacity': room.capacity, 'room_size': room.room_size, 'view': room.view, 'amenities': room.amenities, 'created_at': room.created_at.isoformat() if room.created_at else None, 'updated_at': room.updated_at.isoformat() if room.updated_at else None, 'average_rating': round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None, 'total_reviews': review_stats.total_reviews or 0 if review_stats else 0}
|
||||
try:
|
||||
room_dict['images'] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict['images'] = []
|
||||
if room.room_type:
|
||||
room_dict['room_type'] = {'id': room.room_type.id, 'name': room.room_type.name, 'description': room.room_type.description, 'base_price': float(room.room_type.base_price) if room.room_type.base_price else 0.0, 'capacity': room.room_type.capacity, 'amenities': room.room_type.amenities, 'images': []}
|
||||
return {'status': 'success', 'data': {'room': room_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def create_room(room_data: dict, request: Request, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
room_type = db.query(RoomType).filter(RoomType.id == room_data.get('room_type_id')).first()
|
||||
if not room_type:
|
||||
raise HTTPException(status_code=404, detail='Room type not found')
|
||||
existing = db.query(Room).filter(Room.room_number == room_data.get('room_number')).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail='Room number already exists')
|
||||
amenities_value = room_data.get('amenities', [])
|
||||
if amenities_value is None:
|
||||
amenities_value = []
|
||||
elif not isinstance(amenities_value, list):
|
||||
amenities_value = []
|
||||
room = Room(room_type_id=room_data.get('room_type_id'), room_number=room_data.get('room_number'), floor=room_data.get('floor'), status=RoomStatus(room_data.get('status', 'available')), featured=room_data.get('featured', False), price=room_data.get('price', room_type.base_price), description=room_data.get('description'), capacity=room_data.get('capacity'), room_size=room_data.get('room_size'), view=room_data.get('view'), amenities=amenities_value)
|
||||
db.add(room)
|
||||
db.commit()
|
||||
db.refresh(room)
|
||||
base_url = get_base_url(request)
|
||||
room_dict = {'id': room.id, 'room_type_id': room.room_type_id, 'room_number': room.room_number, 'floor': room.floor, 'status': room.status.value if isinstance(room.status, RoomStatus) else room.status, 'price': float(room.price) if room.price is not None and room.price > 0 else None, 'featured': room.featured, 'description': room.description, 'capacity': room.capacity, 'room_size': room.room_size, 'view': room.view, 'amenities': room.amenities if room.amenities else [], 'created_at': room.created_at.isoformat() if room.created_at else None, 'updated_at': room.updated_at.isoformat() if room.updated_at else None}
|
||||
try:
|
||||
room_dict['images'] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict['images'] = []
|
||||
if room.room_type:
|
||||
room_dict['room_type'] = {'id': room.room_type.id, 'name': room.room_type.name, 'description': room.room_type.description, 'base_price': float(room.room_type.base_price) if room.room_type.base_price else 0.0, 'capacity': room.room_type.capacity, 'amenities': room.room_type.amenities if room.room_type.amenities else [], 'images': []}
|
||||
return {'status': 'success', 'message': 'Room created successfully', 'data': {'room': room_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def update_room(id: int, room_data: dict, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
if room_data.get('room_type_id'):
|
||||
room_type = db.query(RoomType).filter(RoomType.id == room_data['room_type_id']).first()
|
||||
if not room_type:
|
||||
raise HTTPException(status_code=404, detail='Room type not found')
|
||||
if 'room_type_id' in room_data:
|
||||
room.room_type_id = room_data['room_type_id']
|
||||
if 'room_number' in room_data:
|
||||
room.room_number = room_data['room_number']
|
||||
if 'floor' in room_data:
|
||||
room.floor = room_data['floor']
|
||||
if 'status' in room_data:
|
||||
room.status = RoomStatus(room_data['status'])
|
||||
if 'featured' in room_data:
|
||||
room.featured = room_data['featured']
|
||||
if 'price' in room_data:
|
||||
room.price = room_data['price']
|
||||
if 'description' in room_data:
|
||||
room.description = room_data['description']
|
||||
if 'capacity' in room_data:
|
||||
room.capacity = room_data['capacity']
|
||||
if 'room_size' in room_data:
|
||||
room.room_size = room_data['room_size']
|
||||
if 'view' in room_data:
|
||||
room.view = room_data['view']
|
||||
if 'amenities' in room_data:
|
||||
amenities_value = room_data['amenities']
|
||||
if amenities_value is None:
|
||||
room.amenities = []
|
||||
elif isinstance(amenities_value, list):
|
||||
room.amenities = amenities_value
|
||||
else:
|
||||
room.amenities = []
|
||||
db.commit()
|
||||
db.refresh(room)
|
||||
base_url = get_base_url(request)
|
||||
room_dict = {'id': room.id, 'room_type_id': room.room_type_id, 'room_number': room.room_number, 'floor': room.floor, 'status': room.status.value if isinstance(room.status, RoomStatus) else room.status, 'price': float(room.price) if room.price is not None and room.price > 0 else None, 'featured': room.featured, 'description': room.description, 'capacity': room.capacity, 'room_size': room.room_size, 'view': room.view, 'amenities': room.amenities if room.amenities else [], 'created_at': room.created_at.isoformat() if room.created_at else None, 'updated_at': room.updated_at.isoformat() if room.updated_at else None}
|
||||
try:
|
||||
room_dict['images'] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict['images'] = []
|
||||
if room.room_type:
|
||||
room_dict['room_type'] = {'id': room.room_type.id, 'name': room.room_type.name, 'description': room.room_type.description, 'base_price': float(room.room_type.base_price) if room.room_type.base_price else 0.0, 'capacity': room.room_type.capacity, 'amenities': room.room_type.amenities if room.room_type.amenities else [], 'images': []}
|
||||
return {'status': 'success', 'message': 'Room updated successfully', 'data': {'room': room_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def delete_room(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
db.delete(room)
|
||||
db.commit()
|
||||
return {'status': 'success', 'message': 'Room deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/bulk-delete', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def bulk_delete_rooms(room_ids: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
ids = room_ids.get('ids', [])
|
||||
if not ids or not isinstance(ids, list):
|
||||
raise HTTPException(status_code=400, detail='Invalid room IDs provided')
|
||||
if len(ids) == 0:
|
||||
raise HTTPException(status_code=400, detail='No room IDs provided')
|
||||
try:
|
||||
ids = [int(id) for id in ids]
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail='All room IDs must be integers')
|
||||
rooms = db.query(Room).filter(Room.id.in_(ids)).all()
|
||||
found_ids = [room.id for room in rooms]
|
||||
not_found_ids = [id for id in ids if id not in found_ids]
|
||||
if not_found_ids:
|
||||
raise HTTPException(status_code=404, detail=f'Rooms with IDs {not_found_ids} not found')
|
||||
deleted_count = db.query(Room).filter(Room.id.in_(ids)).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
return {'status': 'success', 'message': f'Successfully deleted {deleted_count} room(s)', 'data': {'deleted_count': deleted_count, 'deleted_ids': ids}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/{id}/images', dependencies=[Depends(authorize_roles('admin', 'staff'))])
|
||||
async def upload_room_images(id: int, images: List[UploadFile]=File(...), current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
upload_dir = Path(__file__).parent.parent.parent / 'uploads' / 'rooms'
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
image_urls = []
|
||||
for image in images:
|
||||
if not image.content_type or not image.content_type.startswith('image/'):
|
||||
continue
|
||||
if not image.filename:
|
||||
continue
|
||||
import uuid
|
||||
ext = Path(image.filename).suffix or '.jpg'
|
||||
filename = f'room-{uuid.uuid4()}{ext}'
|
||||
file_path = upload_dir / filename
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
content = await image.read()
|
||||
if not content:
|
||||
continue
|
||||
await f.write(content)
|
||||
image_urls.append(f'/uploads/rooms/{filename}')
|
||||
existing_images = room.images or []
|
||||
updated_images = existing_images + image_urls
|
||||
room.images = updated_images
|
||||
db.commit()
|
||||
return {'success': True, 'status': 'success', 'message': 'Images uploaded successfully', 'data': {'images': updated_images}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f'Error uploading room images: {str(e)}', exc_info=True, extra={'room_id': id})
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.delete('/{id}/images', dependencies=[Depends(authorize_roles('admin', 'staff'))])
|
||||
async def delete_room_images(id: int, image_url: str=Query(..., description='Image URL or path to delete'), current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
normalized_url = image_url
|
||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(image_url)
|
||||
normalized_url = parsed.path
|
||||
if not normalized_url.startswith('/'):
|
||||
normalized_url = f'/{normalized_url}'
|
||||
filename = Path(normalized_url).name
|
||||
existing_images = room.images or []
|
||||
updated_images = []
|
||||
for img in existing_images:
|
||||
stored_path = img if img.startswith('/') else f'/{img}'
|
||||
stored_filename = Path(stored_path).name
|
||||
if img != normalized_url and stored_path != normalized_url and (stored_filename != filename):
|
||||
updated_images.append(img)
|
||||
file_path = Path(__file__).parent.parent.parent / 'uploads' / 'rooms' / filename
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
room.images = updated_images
|
||||
db.commit()
|
||||
return {'status': 'success', 'message': 'Image deleted successfully', 'data': {'images': updated_images}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/{id}/booked-dates')
|
||||
async def get_room_booked_dates(id: int, db: Session=Depends(get_db)):
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
bookings = db.query(Booking).filter(and_(Booking.room_id == id, Booking.status != BookingStatus.cancelled)).all()
|
||||
booked_dates = []
|
||||
for booking in bookings:
|
||||
check_in = booking.check_in_date
|
||||
check_out = booking.check_out_date
|
||||
current_date = check_in.date()
|
||||
end_date = check_out.date()
|
||||
while current_date < end_date:
|
||||
booked_dates.append(current_date.isoformat())
|
||||
from datetime import timedelta
|
||||
current_date += timedelta(days=1)
|
||||
booked_dates = sorted(list(set(booked_dates)))
|
||||
return {'status': 'success', 'data': {'room_id': id, 'booked_dates': booked_dates}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching booked dates: {str(e)}', exc_info=True, extra={'room_id': id})
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get('/{id}/reviews')
|
||||
async def get_room_reviews_route(id: int, db: Session=Depends(get_db)):
|
||||
from ...reviews.models.review import Review, ReviewStatus
|
||||
try:
|
||||
room = db.query(Room).filter(Room.id == id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
reviews = db.query(Review).filter(Review.room_id == id, Review.status == ReviewStatus.approved).order_by(Review.created_at.desc()).all()
|
||||
result = []
|
||||
for review in reviews:
|
||||
review_dict = {'id': review.id, 'user_id': review.user_id, 'room_id': review.room_id, 'rating': review.rating, 'comment': review.comment, 'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status, 'created_at': review.created_at.isoformat() if review.created_at else None}
|
||||
if review.user:
|
||||
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email}
|
||||
result.append(review_dict)
|
||||
return {'status': 'success', 'data': {'reviews': result}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching room reviews: {str(e)}', exc_info=True, extra={'room_id': id})
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user