diff --git a/Backend/seeds_data/__pycache__/add_accountant_role.cpython-312.pyc b/Backend/seeds_data/__pycache__/add_accountant_role.cpython-312.pyc new file mode 100644 index 00000000..788378bc Binary files /dev/null and b/Backend/seeds_data/__pycache__/add_accountant_role.cpython-312.pyc differ diff --git a/Backend/seeds_data/add_housekeeping_role.py b/Backend/seeds_data/add_housekeeping_role.py new file mode 100644 index 00000000..145aa247 --- /dev/null +++ b/Backend/seeds_data/add_housekeeping_role.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Script to add the 'housekeeping' role to the database. +Run this script once to create the housekeeping role if it doesn't exist. +""" + +import sys +import os +from pathlib import Path + +# Add the Backend directory to the path +backend_dir = Path(__file__).parent.parent +sys.path.insert(0, str(backend_dir)) + +from src.shared.config.database import SessionLocal +from src.models import Role # Use the models __init__ which handles all imports + +def add_housekeeping_role(): + """Add the housekeeping role to the database if it doesn't exist.""" + db = SessionLocal() + try: + # Check if housekeeping role already exists + existing_role = db.query(Role).filter(Role.name == 'housekeeping').first() + + if existing_role: + print("✓ Housekeeping role already exists in the database.") + print(f" Role ID: {existing_role.id}") + print(f" Role Name: {existing_role.name}") + return + + # Create the housekeeping role + housekeeping_role = Role( + name='housekeeping', + description='Housekeeping staff role with access to room cleaning tasks and status updates' + ) + db.add(housekeeping_role) + db.commit() + db.refresh(housekeeping_role) + + print("✓ Housekeeping role created successfully!") + print(f" Role ID: {housekeeping_role.id}") + print(f" Role Name: {housekeeping_role.name}") + print(f" Description: {housekeeping_role.description}") + + except Exception as e: + db.rollback() + print(f"✗ Error creating housekeeping role: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + finally: + db.close() + +if __name__ == '__main__': + print("Adding housekeeping role to the database...") + print("=" * 60) + add_housekeeping_role() + print("=" * 60) + print("Done!") diff --git a/Backend/seeds_data/add_housekeeping_user.py b/Backend/seeds_data/add_housekeeping_user.py new file mode 100644 index 00000000..e43da954 --- /dev/null +++ b/Backend/seeds_data/add_housekeeping_user.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +Script to add a housekeeping user to the database. +""" + +import sys +import os +from pathlib import Path + +# Add the Backend directory to the path +backend_dir = Path(__file__).parent.parent +sys.path.insert(0, str(backend_dir)) + +from src.shared.config.database import SessionLocal +from src.models import Role, User +import bcrypt + +def add_housekeeping_user(): + """Add the housekeeping user to the database if it doesn't exist.""" + db = SessionLocal() + try: + # Get housekeeping role + housekeeping_role = db.query(Role).filter(Role.name == 'housekeeping').first() + + if not housekeeping_role: + print("✗ Housekeeping role not found! Please run add_housekeeping_role.py first.") + sys.exit(1) + + # Check if user already exists + existing_user = db.query(User).filter(User.email == 'housekeeping@gnxsoft.com').first() + + if existing_user: + print("✓ Housekeeping user already exists in the database.") + print(f" User ID: {existing_user.id}") + print(f" Email: {existing_user.email}") + print(f" Full Name: {existing_user.full_name}") + print(f" Role: {existing_user.role.name if existing_user.role else 'N/A'}") + return + + # Hash password + password = 'P4eli240453.' + password_bytes = password.encode('utf-8') + salt = bcrypt.gensalt() + hashed_password = bcrypt.hashpw(password_bytes, salt).decode('utf-8') + + # Create the housekeeping user + housekeeping_user = User( + email='housekeeping@gnxsoft.com', + password=hashed_password, + full_name='Housekeeping Staff', + role_id=housekeeping_role.id, + is_active=True, + currency='EUR' + ) + db.add(housekeeping_user) + db.commit() + db.refresh(housekeeping_user) + + print("✓ Housekeeping user created successfully!") + print(f" User ID: {housekeeping_user.id}") + print(f" Email: {housekeeping_user.email}") + print(f" Full Name: {housekeeping_user.full_name}") + print(f" Role: {housekeeping_role.name}") + print(f" Password: P4eli240453.") + + except Exception as e: + db.rollback() + print(f"✗ Error creating housekeeping user: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + finally: + db.close() + +if __name__ == '__main__': + print("Adding housekeeping user to the database...") + print("=" * 60) + add_housekeeping_user() + print("=" * 60) + print("Done!") + diff --git a/Backend/seeds_data/assign_housekeeping_tasks.py b/Backend/seeds_data/assign_housekeeping_tasks.py new file mode 100644 index 00000000..f0069047 --- /dev/null +++ b/Backend/seeds_data/assign_housekeeping_tasks.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Script to assign test housekeeping tasks to the housekeeping user. +This creates sample tasks for testing the housekeeping dashboard. +""" + +import sys +import os +from pathlib import Path +from datetime import datetime, timedelta + +# Add the Backend directory to the path +backend_dir = Path(__file__).parent.parent +sys.path.insert(0, str(backend_dir)) + +from src.shared.config.database import SessionLocal +from src.models import Role, User, Room +from src.hotel_services.models.housekeeping_task import HousekeepingTask, HousekeepingStatus, HousekeepingType + +def assign_housekeeping_tasks(): + """Assign test housekeeping tasks to the housekeeping user.""" + db = SessionLocal() + try: + # Get housekeeping user + housekeeping_user = db.query(User).join(Role).filter( + Role.name == 'housekeeping', + User.email == 'housekeeping@gnxsoft.com' + ).first() + + if not housekeeping_user: + print("✗ Housekeeping user not found! Please run add_housekeeping_user.py first.") + sys.exit(1) + + print(f"✓ Found housekeeping user: {housekeeping_user.email} (ID: {housekeeping_user.id})") + + # Get admin user for created_by + admin_role = db.query(Role).filter(Role.name == 'admin').first() + admin_user = db.query(User).filter(User.role_id == admin_role.id).first() if admin_role else None + + # Get some rooms + rooms = db.query(Room).limit(5).all() + + if not rooms: + print("✗ No rooms found in the database! Please seed rooms first.") + sys.exit(1) + + print(f"✓ Found {len(rooms)} rooms") + + # Default checklist items for different task types + checklists = { + 'checkout': [ + {'item': 'Bathroom cleaned', 'completed': False, 'notes': ''}, + {'item': 'Beds made with fresh linens', 'completed': False, 'notes': ''}, + {'item': 'Trash emptied', 'completed': False, 'notes': ''}, + {'item': 'Towels replaced', 'completed': False, 'notes': ''}, + {'item': 'Amenities restocked', 'completed': False, 'notes': ''}, + {'item': 'Floor vacuumed and mopped', 'completed': False, 'notes': ''}, + {'item': 'Surfaces dusted', 'completed': False, 'notes': ''}, + {'item': 'Windows and mirrors cleaned', 'completed': False, 'notes': ''}, + ], + 'stayover': [ + {'item': 'Beds made', 'completed': False, 'notes': ''}, + {'item': 'Trash emptied', 'completed': False, 'notes': ''}, + {'item': 'Towels replaced', 'completed': False, 'notes': ''}, + {'item': 'Bathroom cleaned', 'completed': False, 'notes': ''}, + ], + 'vacant': [ + {'item': 'Deep clean bathroom', 'completed': False, 'notes': ''}, + {'item': 'Change linens', 'completed': False, 'notes': ''}, + {'item': 'Vacuum and mop', 'completed': False, 'notes': ''}, + {'item': 'Dust surfaces', 'completed': False, 'notes': ''}, + {'item': 'Check amenities', 'completed': False, 'notes': ''}, + ], + 'inspection': [ + {'item': 'Check all amenities', 'completed': False, 'notes': ''}, + {'item': 'Test electronics', 'completed': False, 'notes': ''}, + {'item': 'Check for damages', 'completed': False, 'notes': ''}, + {'item': 'Verify cleanliness', 'completed': False, 'notes': ''}, + ], + } + + # Create tasks for today + today = datetime.utcnow().replace(hour=9, minute=0, second=0, microsecond=0) + task_types = ['checkout', 'stayover', 'vacant', 'inspection'] + + created_count = 0 + skipped_count = 0 + + for i, room in enumerate(rooms): + # Cycle through task types + task_type = task_types[i % len(task_types)] + + # Check if task already exists for this room today + existing_task = db.query(HousekeepingTask).filter( + HousekeepingTask.room_id == room.id, + HousekeepingTask.assigned_to == housekeeping_user.id, + HousekeepingTask.status == HousekeepingStatus.pending, + HousekeepingTask.scheduled_time >= today.replace(hour=0, minute=0), + HousekeepingTask.scheduled_time < today.replace(hour=23, minute=59, second=59) + ).first() + + if existing_task: + print(f" ⚠️ Task already exists for Room {room.room_number}, skipping...") + skipped_count += 1 + continue + + # Schedule tasks at different times throughout the day + scheduled_time = today + timedelta(hours=i) + + task = HousekeepingTask( + room_id=room.id, + booking_id=None, + task_type=HousekeepingType(task_type), + status=HousekeepingStatus.pending, + scheduled_time=scheduled_time, + assigned_to=housekeeping_user.id, + created_by=admin_user.id if admin_user else housekeeping_user.id, + checklist_items=checklists.get(task_type, []), + notes=f'Test task for Room {room.room_number} - {task_type} cleaning', + estimated_duration_minutes=45 if task_type == 'checkout' else 30 + ) + + db.add(task) + created_count += 1 + print(f" ✓ Created {task_type} task for Room {room.room_number} (scheduled: {scheduled_time.strftime('%Y-%m-%d %H:%M')})") + + db.commit() + + print(f"\n✓ Tasks assigned successfully!") + print(f" - Created: {created_count} task(s)") + print(f" - Skipped: {skipped_count} task(s) (already exist)") + print(f" - Assigned to: {housekeeping_user.email}") + + except Exception as e: + db.rollback() + print(f"✗ Error assigning housekeeping tasks: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + finally: + db.close() + +if __name__ == '__main__': + print("Assigning test housekeeping tasks to housekeeping user...") + print("=" * 60) + assign_housekeeping_tasks() + print("=" * 60) + print("Done!") + diff --git a/Backend/seeds_data/seed_initial_data.py b/Backend/seeds_data/seed_initial_data.py index 7abf8c82..5510251f 100644 --- a/Backend/seeds_data/seed_initial_data.py +++ b/Backend/seeds_data/seed_initial_data.py @@ -26,7 +26,8 @@ def seed_roles(db: Session): {'name': 'admin', 'description': 'Administrator with full access'}, {'name': 'staff', 'description': 'Staff member with limited admin access'}, {'name': 'customer', 'description': 'Regular customer'}, - {'name': 'accountant', 'description': 'Accountant role with access to financial data, payments, and invoices'} + {'name': 'accountant', 'description': 'Accountant role with access to financial data, payments, and invoices'}, + {'name': 'housekeeping', 'description': 'Housekeeping staff role with access to room cleaning tasks and status updates'} ] for role_data in roles_data: diff --git a/Backend/src/bookings/routes/__pycache__/booking_routes.cpython-312.pyc b/Backend/src/bookings/routes/__pycache__/booking_routes.cpython-312.pyc index a9c2067e..367ca479 100644 Binary files a/Backend/src/bookings/routes/__pycache__/booking_routes.cpython-312.pyc and b/Backend/src/bookings/routes/__pycache__/booking_routes.cpython-312.pyc differ diff --git a/Backend/src/bookings/routes/booking_routes.py b/Backend/src/bookings/routes/booking_routes.py index 9e92ee74..3dfe052e 100644 --- a/Backend/src/bookings/routes/booking_routes.py +++ b/Backend/src/bookings/routes/booking_routes.py @@ -730,6 +730,46 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us room.status = RoomStatus.maintenance else: room.status = RoomStatus.cleaning + + # Auto-create housekeeping task for checkout cleaning + from ...hotel_services.models.housekeeping_task import HousekeepingTask, HousekeepingStatus, HousekeepingType + + # Check if a pending checkout task already exists for this room + existing_task = db.query(HousekeepingTask).filter( + and_( + HousekeepingTask.room_id == room.id, + HousekeepingTask.booking_id == booking.id, + HousekeepingTask.task_type == HousekeepingType.checkout, + HousekeepingTask.status.in_([HousekeepingStatus.pending, HousekeepingStatus.in_progress]) + ) + ).first() + + if not existing_task: + # Default checklist items for checkout cleaning + checkout_checklist = [ + {'item': 'Bathroom cleaned', 'completed': False, 'notes': ''}, + {'item': 'Beds made with fresh linens', 'completed': False, 'notes': ''}, + {'item': 'Trash emptied', 'completed': False, 'notes': ''}, + {'item': 'Towels replaced', 'completed': False, 'notes': ''}, + {'item': 'Amenities restocked', 'completed': False, 'notes': ''}, + {'item': 'Floor vacuumed and mopped', 'completed': False, 'notes': ''}, + {'item': 'Surfaces dusted', 'completed': False, 'notes': ''}, + {'item': 'Windows and mirrors cleaned', 'completed': False, 'notes': ''}, + ] + + housekeeping_task = HousekeepingTask( + room_id=room.id, + booking_id=booking.id, + task_type=HousekeepingType.checkout, + status=HousekeepingStatus.pending, + scheduled_time=datetime.utcnow(), # Schedule immediately + created_by=current_user.id, + checklist_items=checkout_checklist, + notes=f'Auto-created on checkout for booking {booking.booking_number}', + estimated_duration_minutes=45 # Default 45 minutes for checkout cleaning + ) + db.add(housekeeping_task) + db.flush() # Flush to get the task ID if needed for notifications elif new_status == BookingStatus.cancelled: # Update room status when booking is cancelled if booking.payments: diff --git a/Backend/src/rooms/routes/__pycache__/advanced_room_routes.cpython-312.pyc b/Backend/src/rooms/routes/__pycache__/advanced_room_routes.cpython-312.pyc index 15b8a060..e921a20c 100644 Binary files a/Backend/src/rooms/routes/__pycache__/advanced_room_routes.cpython-312.pyc and b/Backend/src/rooms/routes/__pycache__/advanced_room_routes.cpython-312.pyc differ diff --git a/Backend/src/rooms/routes/__pycache__/room_routes.cpython-312.pyc b/Backend/src/rooms/routes/__pycache__/room_routes.cpython-312.pyc index 1987291f..3ae29ecf 100644 Binary files a/Backend/src/rooms/routes/__pycache__/room_routes.cpython-312.pyc and b/Backend/src/rooms/routes/__pycache__/room_routes.cpython-312.pyc differ diff --git a/Backend/src/rooms/routes/advanced_room_routes.py b/Backend/src/rooms/routes/advanced_room_routes.py index 2f663808..5350cf25 100644 --- a/Backend/src/rooms/routes/advanced_room_routes.py +++ b/Backend/src/rooms/routes/advanced_room_routes.py @@ -346,19 +346,20 @@ async def get_housekeeping_tasks( 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')), + current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')), 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 + # Check user role - housekeeping and staff users 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' + is_admin = role and role.name == 'admin' + is_housekeeping_or_staff = role and role.name in ('housekeeping', 'staff') query = db.query(HousekeepingTask) - # Filter by assigned_to for staff users - if is_staff: + # Filter by assigned_to for housekeeping and staff users (not admin) + if is_housekeeping_or_staff: query = query.filter(HousekeepingTask.assigned_to == current_user.id) if room_id: @@ -488,7 +489,7 @@ async def create_housekeeping_task( async def update_housekeeping_task( task_id: int, task_data: dict, - current_user: User = Depends(authorize_roles('admin', 'staff')), + current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')), db: Session = Depends(get_db) ): """Update a housekeeping task""" @@ -497,22 +498,23 @@ async def update_housekeeping_task( 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 + # Check user role - housekeeping and staff users 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' + is_admin = role and role.name == 'admin' + is_housekeeping_or_staff = role and role.name in ('housekeeping', 'staff') - if is_staff: - # Staff can only update tasks assigned to them + if is_housekeeping_or_staff: + # Housekeeping and 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 + # Housekeeping and 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: + if 'assigned_to' in task_data and is_admin: new_assigned_to = task_data.get('assigned_to') if new_assigned_to != old_assigned_to: task.assigned_to = new_assigned_to @@ -537,6 +539,37 @@ async def update_housekeeping_task( if task.started_at: duration = (task.completed_at - task.started_at).total_seconds() / 60 task.actual_duration_minutes = int(duration) + + # Update room status when housekeeping task is completed + room = db.query(Room).filter(Room.id == task.room_id).first() + if room: + # Check if there are other pending housekeeping tasks for this room + pending_tasks = db.query(HousekeepingTask).filter( + and_( + HousekeepingTask.room_id == room.id, + HousekeepingTask.id != task.id, + HousekeepingTask.status.in_([HousekeepingStatus.pending, HousekeepingStatus.in_progress]) + ) + ).count() + + # Check if there's active maintenance + from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus + 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() + + if active_maintenance: + room.status = RoomStatus.maintenance + elif pending_tasks > 0: + # Keep room as cleaning if there are other pending tasks + room.status = RoomStatus.cleaning + else: + # No pending tasks and no maintenance - room is ready + room.status = RoomStatus.available if 'checklist_items' in task_data: task.checklist_items = task_data['checklist_items'] diff --git a/Backend/src/rooms/routes/room_routes.py b/Backend/src/rooms/routes/room_routes.py index 83849fce..0eae485f 100644 --- a/Backend/src/rooms/routes/room_routes.py +++ b/Backend/src/rooms/routes/room_routes.py @@ -72,6 +72,26 @@ async def get_amenities(db: Session=Depends(get_db)): logger.error(f'Error fetching amenities: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) +@router.get('/room-types') +async def get_room_types(db: Session=Depends(get_db)): + """Get all room types for dropdowns and forms.""" + try: + room_types = db.query(RoomType).order_by(RoomType.name).all() + room_types_list = [ + { + 'id': rt.id, + 'name': rt.name, + 'description': rt.description, + 'base_price': float(rt.base_price) if rt.base_price else 0.0, + 'capacity': rt.capacity, + } + for rt in room_types + ] + return {'status': 'success', 'data': {'room_types': room_types_list}} + except Exception as e: + logger.error(f'Error fetching room types: {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: @@ -215,19 +235,17 @@ async def get_room_by_number(room_number: str, request: Request, db: Session=Dep @router.post('/', dependencies=[Depends(authorize_roles('admin'))]) async def create_room(room_data: CreateRoomRequest, request: Request, current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)): """Create a new room with validated input using Pydantic schema.""" - # Start transaction - transaction = db.begin() try: # Lock room type to prevent race conditions room_type = db.query(RoomType).filter(RoomType.id == room_data.room_type_id).with_for_update().first() if not room_type: - transaction.rollback() + db.rollback() raise HTTPException(status_code=404, detail='Room type not found') # Check for duplicate room number with locking existing = db.query(Room).filter(Room.room_number == room_data.room_number).with_for_update().first() if existing: - transaction.rollback() + db.rollback() raise HTTPException(status_code=400, detail='Room number already exists') # Use price from request or default to room type base price @@ -250,7 +268,7 @@ async def create_room(room_data: CreateRoomRequest, request: Request, current_us db.flush() # Commit transaction - transaction.commit() + 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} @@ -262,36 +280,31 @@ async def create_room(room_data: CreateRoomRequest, request: Request, current_us 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 success_response(data={'room': room_dict}, message='Room created successfully') except HTTPException: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() raise except IntegrityError as e: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() logger.error(f'Database integrity error during room creation: {str(e)}') raise HTTPException(status_code=409, detail='Room conflict detected. Please check room number.') except Exception as e: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() logger.error(f'Error creating room: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail='An error occurred while creating the room') @router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))]) async def update_room(id: int, room_data: UpdateRoomRequest, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): """Update a room with validated input using Pydantic schema.""" - # Start transaction - transaction = db.begin() try: # Lock room row to prevent race conditions room = db.query(Room).filter(Room.id == id).with_for_update().first() if not room: - transaction.rollback() + db.rollback() raise HTTPException(status_code=404, detail='Room not found') if room_data.room_type_id: room_type = db.query(RoomType).filter(RoomType.id == room_data.room_type_id).first() if not room_type: - transaction.rollback() + db.rollback() raise HTTPException(status_code=404, detail='Room type not found') room.room_type_id = room_data.room_type_id @@ -299,7 +312,7 @@ async def update_room(id: int, room_data: UpdateRoomRequest, request: Request, c # Check for duplicate room number existing = db.query(Room).filter(Room.room_number == room_data.room_number, Room.id != id).first() if existing: - transaction.rollback() + db.rollback() raise HTTPException(status_code=400, detail='Room number already exists') room.room_number = room_data.room_number @@ -323,7 +336,7 @@ async def update_room(id: int, room_data: UpdateRoomRequest, request: Request, c room.amenities = room_data.amenities or [] # Commit transaction - transaction.commit() + db.commit() db.refresh(room) base_url = get_base_url(request) @@ -359,17 +372,14 @@ async def update_room(id: int, room_data: UpdateRoomRequest, request: Request, c } return success_response(data={'room': room_dict}, message='Room updated successfully') except HTTPException: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() raise except IntegrityError as e: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() logger.error(f'Database integrity error during room update: {str(e)}') raise HTTPException(status_code=409, detail='Room conflict detected. Please check room number.') except Exception as e: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() logger.error(f'Error updating room: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail='An error occurred while updating the room') @@ -391,8 +401,6 @@ async def delete_room(id: int, current_user: User=Depends(authorize_roles('admin @router.post('/bulk-delete', dependencies=[Depends(authorize_roles('admin'))]) async def bulk_delete_rooms(room_ids: BulkDeleteRoomsRequest, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): """Bulk delete rooms with validated input using Pydantic schema.""" - # Start transaction - transaction = db.begin() try: ids = room_ids.room_ids @@ -401,30 +409,27 @@ async def bulk_delete_rooms(room_ids: BulkDeleteRoomsRequest, current_user: User 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: - transaction.rollback() + db.rollback() raise HTTPException(status_code=404, detail=f'Rooms with IDs {not_found_ids} not found') # Delete rooms deleted_count = db.query(Room).filter(Room.id.in_(ids)).delete(synchronize_session=False) # Commit transaction - transaction.commit() + db.commit() return success_response( data={'deleted_count': deleted_count, 'deleted_ids': ids}, message=f'Successfully deleted {deleted_count} room(s)' ) except HTTPException: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() raise except IntegrityError as e: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() logger.error(f'Database integrity error during bulk room deletion: {str(e)}') raise HTTPException(status_code=409, detail='Cannot delete rooms due to existing relationships (bookings, etc.)') except Exception as e: - if 'transaction' in locals(): - transaction.rollback() + db.rollback() logger.error(f'Error bulk deleting rooms: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail='An error occurred while deleting rooms') diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 5a41cb5d..cbe8f85e 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -14,6 +14,7 @@ import { CurrencyProvider } from './features/payments/contexts/CurrencyContext'; import { CompanySettingsProvider } from './shared/contexts/CompanySettingsContext'; import { AuthModalProvider } from './features/auth/contexts/AuthModalContext'; import { AntibotProvider } from './features/auth/contexts/AntibotContext'; +import { RoomProvider } from './features/rooms/contexts/RoomContext'; import { logDebug } from './shared/utils/errorReporter'; import OfflineIndicator from './shared/components/OfflineIndicator'; import CookieConsentBanner from './shared/components/CookieConsentBanner'; @@ -37,7 +38,8 @@ import { AdminRoute, StaffRoute, AccountantRoute, - CustomerRoute + CustomerRoute, + HousekeepingRoute } from './features/auth/components'; const HomePage = lazy(() => import('./features/content/pages/HomePage')); @@ -122,6 +124,11 @@ const AccountantPaymentManagementPage = lazy(() => import('./pages/accountant/Pa const AccountantInvoiceManagementPage = lazy(() => import('./pages/accountant/InvoiceManagementPage')); const AccountantAnalyticsDashboardPage = lazy(() => import('./pages/accountant/AnalyticsDashboardPage')); const AccountantLayout = lazy(() => import('./pages/AccountantLayout')); + +const HousekeepingDashboardPage = lazy(() => import('./pages/housekeeping/DashboardPage')); +const HousekeepingTasksPage = lazy(() => import('./pages/housekeeping/TasksPage')); +const HousekeepingLayout = lazy(() => import('./pages/HousekeepingLayout')); + const AdminProfilePage = lazy(() => import('./pages/admin/ProfilePage')); const StaffProfilePage = lazy(() => import('./pages/staff/ProfilePage')); const AccountantProfilePage = lazy(() => import('./pages/accountant/ProfilePage')); @@ -210,6 +217,7 @@ function App() { + + {/* Housekeeping Routes */} + + + + + + } + > + } + /> + } /> + + {} + diff --git a/Frontend/src/features/auth/components/AdminRoute.tsx b/Frontend/src/features/auth/components/AdminRoute.tsx index d51570b8..a3556cc4 100644 --- a/Frontend/src/features/auth/components/AdminRoute.tsx +++ b/Frontend/src/features/auth/components/AdminRoute.tsx @@ -62,6 +62,8 @@ const AdminRoute: React.FC = ({ return ; } else if (userInfo?.role === 'accountant') { return ; + } else if (userInfo?.role === 'housekeeping') { + return ; } return ; } diff --git a/Frontend/src/features/auth/components/HousekeepingRoute.tsx b/Frontend/src/features/auth/components/HousekeepingRoute.tsx new file mode 100644 index 00000000..fbb51410 --- /dev/null +++ b/Frontend/src/features/auth/components/HousekeepingRoute.tsx @@ -0,0 +1,52 @@ +import React, { useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; +import useAuthStore from '../../../store/useAuthStore'; +import { useAuthModal } from '../contexts/AuthModalContext'; + +interface HousekeepingRouteProps { + children: React.ReactNode; +} + +const HousekeepingRoute: React.FC = ({ children }) => { + const { isAuthenticated, userInfo, isLoading } = useAuthStore(); + const { openModal } = useAuthModal(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + openModal('login'); + } + }, [isLoading, isAuthenticated, openModal]); + + if (isLoading) { + return ( +
+
+
+

Authenticating...

+
+
+ ); + } + + if (!isAuthenticated) { + return null; // Modal will be shown by AuthModalManager + } + + // Only allow housekeeping role - no admin or staff access + if (userInfo?.role !== 'housekeeping') { + // Redirect to appropriate dashboard based on role + if (userInfo?.role === 'admin') { + return ; + } else if (userInfo?.role === 'staff') { + return ; + } else if (userInfo?.role === 'accountant') { + return ; + } + return ; + } + + return <>{children}; +}; + +export default HousekeepingRoute; + diff --git a/Frontend/src/features/auth/components/LoginModal.tsx b/Frontend/src/features/auth/components/LoginModal.tsx index 0a5b31c0..af75f25a 100644 --- a/Frontend/src/features/auth/components/LoginModal.tsx +++ b/Frontend/src/features/auth/components/LoginModal.tsx @@ -77,6 +77,8 @@ const LoginModal: React.FC = () => { navigate('/staff/dashboard', { replace: true }); } else if (role === 'accountant') { navigate('/accountant/dashboard', { replace: true }); + } else if (role === 'housekeeping') { + navigate('/housekeeping/dashboard', { replace: true }); } else { // Customer or default - go to customer dashboard navigate('/dashboard', { replace: true }); diff --git a/Frontend/src/features/auth/components/StaffRoute.tsx b/Frontend/src/features/auth/components/StaffRoute.tsx index 3d1ef361..76f3680f 100644 --- a/Frontend/src/features/auth/components/StaffRoute.tsx +++ b/Frontend/src/features/auth/components/StaffRoute.tsx @@ -52,6 +52,8 @@ const StaffRoute: React.FC = ({ return ; } else if (userInfo?.role === 'accountant') { return ; + } else if (userInfo?.role === 'housekeeping') { + return ; } return ; } diff --git a/Frontend/src/features/auth/components/index.ts b/Frontend/src/features/auth/components/index.ts index df5bec05..c60fdc76 100644 --- a/Frontend/src/features/auth/components/index.ts +++ b/Frontend/src/features/auth/components/index.ts @@ -3,4 +3,5 @@ export { default as AdminRoute } from './AdminRoute'; export { default as StaffRoute } from './StaffRoute'; export { default as AccountantRoute } from './AccountantRoute'; export { default as CustomerRoute } from './CustomerRoute'; +export { default as HousekeepingRoute } from './HousekeepingRoute'; export { default as ResetPasswordRouteHandler } from './ResetPasswordRouteHandler'; diff --git a/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx b/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx index 340d3b0c..eefac3b2 100644 --- a/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx +++ b/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx @@ -21,6 +21,7 @@ import 'react-datepicker/dist/react-datepicker.css'; const HousekeepingManagement: React.FC = () => { const { userInfo } = useAuthStore(); const isAdmin = userInfo?.role === 'admin'; + const isHousekeeping = userInfo?.role === 'housekeeping'; const [loading, setLoading] = useState(true); const [tasks, setTasks] = useState([]); const [rooms, setRooms] = useState([]); @@ -227,8 +228,8 @@ const HousekeepingManagement: React.FC = () => { } toast.success('Housekeeping task updated successfully'); } else { - // Only admin can create tasks - if (!isAdmin) { + // Only admin and staff can create tasks + if (!isAdmin && userInfo?.role !== 'staff') { toast.error('You do not have permission to create tasks'); return; } @@ -321,7 +322,7 @@ const HousekeepingManagement: React.FC = () => { className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
- {isAdmin && ( + {(isAdmin || userInfo?.role === 'staff') && ( ) : ( - // Staff can only edit their own assigned tasks - task.assigned_to === userInfo?.id && task.status !== 'completed' && ( + // Housekeeping and staff can only edit their own assigned tasks + (isHousekeeping || userInfo?.role === 'staff') && + task.assigned_to === userInfo?.id && + task.status !== 'completed' && ( <> + )} + + {/* Sidebar */} +
+
+ {/* Logo/Brand */} +
+
+ + Housekeeping +
+
+ + {/* Navigation */} + + + {/* User info and logout */} +
+
+

{userInfo?.name || userInfo?.email || 'User'}

+

{userInfo?.role || 'housekeeping'}

+
+ +
+
+
+ + {/* Overlay for mobile */} + {isMobile && sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Main content */} +
+
+ +
+
+
+ ); +}; + +export default HousekeepingLayout; + diff --git a/Frontend/src/pages/admin/AdvancedRoomManagementPage.tsx b/Frontend/src/pages/admin/AdvancedRoomManagementPage.tsx index 55a7e0a5..ce6d4a2c 100644 --- a/Frontend/src/pages/admin/AdvancedRoomManagementPage.tsx +++ b/Frontend/src/pages/admin/AdvancedRoomManagementPage.tsx @@ -13,54 +13,49 @@ import { ChevronUp, Calendar, MapPin, - Search, Plus, Edit, - Trash2, X, Image as ImageIcon, Check, } from 'lucide-react'; import { toast } from 'react-toastify'; import Loading from '../../shared/components/Loading'; -import advancedRoomService, { +import { RoomStatusBoardItem, } from '../../features/rooms/services/advancedRoomService'; import roomService, { Room } from '../../features/rooms/services/roomService'; import MaintenanceManagement from '../../features/hotel_services/components/MaintenanceManagement'; import HousekeepingManagement from '../../features/hotel_services/components/HousekeepingManagement'; import InspectionManagement from '../../features/hotel_services/components/InspectionManagement'; -import Pagination from '../../shared/components/Pagination'; import apiClient from '../../shared/services/apiClient'; -import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { logger } from '../../shared/utils/logger'; +import { useRoomContext } from '../../features/rooms/contexts/RoomContext'; -type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections' | 'rooms'; +type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections'; const AdvancedRoomManagementPage: React.FC = () => { - const { formatCurrency } = useFormatCurrency(); + const { + statusBoardRooms, + statusBoardLoading, + rooms: contextRooms, + refreshStatusBoard, + refreshRooms, + updateRoom: contextUpdateRoom, + deleteRoom: contextDeleteRoom, + createRoom: contextCreateRoom, + setStatusBoardFloor, + statusBoardFloor, + } = useRoomContext(); + const [activeTab, setActiveTab] = useState('status-board'); - const [loading, setLoading] = useState(true); - const [rooms, setRooms] = useState([]); const [selectedFloor, setSelectedFloor] = useState(null); const [floors, setFloors] = useState([]); const [expandedRooms, setExpandedRooms] = useState>(new Set()); // Rooms management state - const [roomList, setRoomList] = useState([]); - const [roomsLoading, setRoomsLoading] = useState(true); const [showRoomModal, setShowRoomModal] = useState(false); const [editingRoom, setEditingRoom] = useState(null); - const [selectedRooms, setSelectedRooms] = useState([]); - const [roomFilters, setRoomFilters] = useState({ - search: '', - status: '', - type: '', - }); - const [roomCurrentPage, setRoomCurrentPage] = useState(1); - const [roomTotalPages, setRoomTotalPages] = useState(1); - const [roomTotalItems, setRoomTotalItems] = useState(0); - const roomItemsPerPage = 5; const [roomFormData, setRoomFormData] = useState({ room_number: '', floor: 1, @@ -79,38 +74,79 @@ const AdvancedRoomManagementPage: React.FC = () => { const [uploadingImages, setUploadingImages] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); - useEffect(() => { - fetchRoomStatusBoard(); - fetchFloors(); - }, [selectedFloor]); - - const fetchRoomStatusBoard = async () => { + // Define fetchFloors before using it in useEffect + const fetchFloors = useCallback(async () => { try { - setLoading(true); - const response = await advancedRoomService.getRoomStatusBoard(selectedFloor || undefined); - if (response.status === 'success') { - setRooms(response.data.rooms); - } - } catch (error: any) { - toast.error(error.response?.data?.detail || 'Failed to fetch room status board'); - } finally { - setLoading(false); - } - }; - - const fetchFloors = async () => { - try { - const response = await roomService.getRooms({ limit: 1000, page: 1 }); + const response = await roomService.getRooms({ limit: 100, page: 1 }); if (response.data?.rooms) { const uniqueFloors = Array.from( - new Set(response.data.rooms.map((r: any) => r.floor).filter((f: any) => f != null)) - ).sort((a: any, b: any) => a - b) as number[]; + new Set(response.data.rooms.map((r: Room) => r.floor).filter((f: number | undefined) => f != null)) + ).sort((a: number, b: number) => a - b) as number[]; setFloors(uniqueFloors); } } catch (error) { logger.error('Failed to fetch floors', error); + toast.error('Failed to load floor information'); } - }; + }, []); + + // Sync selectedFloor with context + useEffect(() => { + setStatusBoardFloor(selectedFloor); + }, [selectedFloor, setStatusBoardFloor]); + + // Use rooms directly from context - no need for local state + + // Update selectedFloor when context changes + useEffect(() => { + if (statusBoardFloor !== selectedFloor) { + setSelectedFloor(statusBoardFloor); + } + }, [statusBoardFloor, selectedFloor]); + + // Refresh status board when floor filter changes + useEffect(() => { + let isMounted = true; + const abortController = new AbortController(); + + const refresh = async () => { + if (isMounted && !abortController.signal.aborted) { + await refreshStatusBoard(selectedFloor || undefined); + } + }; + + refresh(); + + return () => { + isMounted = false; + abortController.abort(); + }; + }, [selectedFloor, refreshStatusBoard]); + + useEffect(() => { + let isMounted = true; + const abortController = new AbortController(); + + const initializeData = async () => { + try { + await Promise.all([ + fetchFloors(), + refreshStatusBoard(), + ]); + } catch (error) { + if (isMounted && !abortController.signal.aborted) { + logger.error('Error initializing data', error); + } + } + }; + + initializeData(); + + return () => { + isMounted = false; + abortController.abort(); + }; + }, [refreshStatusBoard, fetchFloors]); const toggleRoomExpansion = (roomId: number) => { const newExpanded = new Set(expandedRooms); @@ -125,14 +161,14 @@ const AdvancedRoomManagementPage: React.FC = () => { // Group rooms by floor const roomsByFloor = useMemo(() => { const grouped: Record = {}; - rooms.forEach(room => { + statusBoardRooms.forEach(room => { if (!grouped[room.floor]) { grouped[room.floor] = []; } grouped[room.floor].push(room); }); return grouped; - }, [rooms]); + }, [statusBoardRooms]); const getStatusColor = (status: string) => { switch (status) { @@ -219,88 +255,20 @@ const AdvancedRoomManagementPage: React.FC = () => { } } catch (error) { logger.error('Failed to fetch amenities', error); + toast.error('Failed to load amenities'); } }, []); - const fetchRoomList = useCallback(async () => { + + // Fetch room types using dedicated endpoint + const fetchRoomTypes = useCallback(async () => { try { - setRoomsLoading(true); - const response = await roomService.getRooms({ - ...roomFilters, - page: roomCurrentPage, - limit: roomItemsPerPage, - }); - setRoomList(response.data.rooms); - if (response.data.pagination) { - setRoomTotalPages(response.data.pagination.totalPages); - setRoomTotalItems(response.data.pagination.total); - } - - const uniqueRoomTypes = new Map(); - response.data.rooms.forEach((room: Room) => { - if (room.room_type && !uniqueRoomTypes.has(room.room_type.id)) { - uniqueRoomTypes.set(room.room_type.id, { - id: room.room_type.id, - name: room.room_type.name, - }); - } - }); - setRoomTypes(Array.from(uniqueRoomTypes.values())); - } catch (error: any) { - toast.error(error.response?.data?.message || 'Unable to load rooms list'); - } finally { - setRoomsLoading(false); - } - }, [roomFilters.search, roomFilters.status, roomFilters.type, roomCurrentPage]); - - useEffect(() => { - setRoomCurrentPage(1); - setSelectedRooms([]); - }, [roomFilters.search, roomFilters.status, roomFilters.type]); - - useEffect(() => { - if (activeTab === 'rooms') { - fetchRoomList(); - fetchAvailableAmenities(); - } - }, [activeTab, fetchRoomList, fetchAvailableAmenities]); - - useEffect(() => { - if (activeTab !== 'rooms') return; - - const fetchAllRoomTypes = async () => { - try { - const response = await roomService.getRooms({ limit: 100, page: 1 }); - const allUniqueRoomTypes = new Map(); - response.data.rooms.forEach((room: Room) => { - if (room.room_type && !allUniqueRoomTypes.has(room.room_type.id)) { - allUniqueRoomTypes.set(room.room_type.id, { - id: room.room_type.id, - name: room.room_type.name, - }); - } - }); - - if (response.data.pagination && response.data.pagination.totalPages > 1) { - const totalPages = response.data.pagination.totalPages; - for (let page = 2; page <= totalPages; page++) { - try { - const pageResponse = await roomService.getRooms({ limit: 100, page }); - pageResponse.data.rooms.forEach((room: Room) => { - if (room.room_type && !allUniqueRoomTypes.has(room.room_type.id)) { - allUniqueRoomTypes.set(room.room_type.id, { - id: room.room_type.id, - name: room.room_type.name, - }); - } - }); - } catch (err) { - logger.error(`Failed to fetch page ${page}`, err); - } - } - } - - const roomTypesList = Array.from(allUniqueRoomTypes.values()); + const response = await roomService.getRoomTypes(); + if (response.data?.room_types) { + const roomTypesList = response.data.room_types.map((rt: { id: number; name: string }) => ({ + id: rt.id, + name: rt.name, + })); setRoomTypes(roomTypesList); setRoomFormData(prev => { if (!editingRoom && prev.room_type_id === 1 && roomTypesList.length > 0) { @@ -308,16 +276,68 @@ const AdvancedRoomManagementPage: React.FC = () => { } return prev; }); + } + } catch (error) { + logger.error('Failed to fetch room types', error); + toast.error('Failed to load room types'); + } + }, [editingRoom]); + + useEffect(() => { + let isMounted = true; + const abortController = new AbortController(); + + const fetchData = async () => { + try { + await Promise.all([ + fetchAvailableAmenities(), + fetchRoomTypes(), + ]); } catch (error) { - logger.error('Failed to fetch room types', error); + if (isMounted && !abortController.signal.aborted) { + logger.error('Error fetching initial data', error); + } } }; - - fetchAllRoomTypes(); - }, [activeTab, editingRoom]); + + fetchData(); + + return () => { + isMounted = false; + abortController.abort(); + }; + }, [fetchAvailableAmenities, fetchRoomTypes]); + + // Frontend validation + const validateRoomForm = (): string | null => { + if (!roomFormData.room_number.trim()) { + return 'Room number is required'; + } + if (roomFormData.floor < 1) { + return 'Floor must be at least 1'; + } + if (roomFormData.room_type_id < 1) { + return 'Room type is required'; + } + if (roomFormData.price && parseFloat(roomFormData.price) < 0) { + return 'Price cannot be negative'; + } + if (roomFormData.capacity && parseInt(roomFormData.capacity) < 1) { + return 'Capacity must be at least 1'; + } + return null; + }; const handleRoomSubmit = async (e: React.FormEvent) => { e.preventDefault(); + + // Frontend validation + const validationError = validateRoomForm(); + if (validationError) { + toast.error(validationError); + return; + } + try { if (editingRoom) { const updateData = { @@ -329,14 +349,15 @@ const AdvancedRoomManagementPage: React.FC = () => { view: roomFormData.view || undefined, amenities: Array.isArray(roomFormData.amenities) ? roomFormData.amenities : [], }; - await roomService.updateRoom(editingRoom.id, updateData); - toast.success('Room updated successfully'); - await fetchRoomList(); + await contextUpdateRoom(editingRoom.id, updateData); + + // Refresh room details for editing try { const updatedRoom = await roomService.getRoomByNumber(editingRoom.room_number); setEditingRoom(updatedRoom.data.room); } catch (err) { logger.error('Failed to refresh room data', err); + toast.error('Room updated but failed to refresh details'); } } else { const createData = { @@ -349,7 +370,6 @@ const AdvancedRoomManagementPage: React.FC = () => { amenities: Array.isArray(roomFormData.amenities) ? roomFormData.amenities : [], }; const response = await roomService.createRoom(createData); - toast.success('Room added successfully'); if (response.data?.room) { if (selectedFiles.length > 0) { @@ -396,18 +416,41 @@ const AdvancedRoomManagementPage: React.FC = () => { amenities: response.data.room.amenities || [], }); - await fetchRoomList(); + await contextCreateRoom(createData); return; } } setShowRoomModal(false); resetRoomForm(); - fetchRoomList(); + await refreshRooms(); } catch (error: any) { toast.error(error.response?.data?.message || 'An error occurred'); } }; + // Handle edit from status board + const handleEditRoomFromStatusBoard = async (roomId: number) => { + try { + // Find the room in context rooms + const room = contextRooms.find(r => r.id === roomId); + if (room) { + await handleEditRoom(room); + } else { + // If not found in context, fetch it + const response = await roomService.getRoomById(roomId); + const foundRoom = response.data?.room; + if (foundRoom) { + await handleEditRoom(foundRoom); + } else { + toast.error('Room not found'); + } + } + } catch (error: any) { + logger.error('Error fetching room for edit', error); + toast.error('Failed to load room details'); + } + }; + const handleEditRoom = async (room: Room) => { setEditingRoom(room); @@ -478,6 +521,7 @@ const AdvancedRoomManagementPage: React.FC = () => { setEditingRoom(roomData); } catch (error) { logger.error('Failed to fetch full room details', error); + toast.error('Failed to load complete room details'); } }; @@ -485,46 +529,9 @@ const AdvancedRoomManagementPage: React.FC = () => { if (!window.confirm('Are you sure you want to delete this room?')) return; try { - await roomService.deleteRoom(id); - toast.success('Room deleted successfully'); - setSelectedRooms(selectedRooms.filter(roomId => roomId !== id)); - fetchRoomList(); + await contextDeleteRoom(id); } catch (error: any) { - toast.error(error.response?.data?.message || 'Unable to delete room'); - } - }; - - const handleBulkDeleteRooms = async () => { - if (selectedRooms.length === 0) { - toast.warning('Please select at least one room to delete'); - return; - } - - if (!window.confirm(`Are you sure you want to delete ${selectedRooms.length} room(s)?`)) return; - - try { - await roomService.bulkDeleteRooms(selectedRooms); - toast.success(`Successfully deleted ${selectedRooms.length} room(s)`); - setSelectedRooms([]); - fetchRoomList(); - } catch (error: any) { - toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete rooms'); - } - }; - - const handleSelectRoom = (roomId: number) => { - setSelectedRooms(prev => - prev.includes(roomId) - ? prev.filter(id => id !== roomId) - : [...prev, roomId] - ); - }; - - const handleSelectAllRooms = () => { - if (selectedRooms.length === roomList.length) { - setSelectedRooms([]); - } else { - setSelectedRooms(roomList.map(room => room.id)); + // Error already handled in context } }; @@ -581,7 +588,10 @@ const AdvancedRoomManagementPage: React.FC = () => { toast.success('Images uploaded successfully'); setSelectedFiles([]); - fetchRoomList(); + await Promise.all([ + refreshRooms(), + refreshStatusBoard(), + ]); const response = await roomService.getRoomByNumber(editingRoom.room_number); setEditingRoom(response.data.room); @@ -613,7 +623,10 @@ const AdvancedRoomManagementPage: React.FC = () => { }); toast.success('Image deleted successfully'); - fetchRoomList(); + await Promise.all([ + refreshRooms(), + refreshStatusBoard(), + ]); const response = await roomService.getRoomByNumber(editingRoom.room_number); setEditingRoom(response.data.room); @@ -652,7 +665,7 @@ const AdvancedRoomManagementPage: React.FC = () => { ); }; - if (loading && rooms.length === 0 && activeTab !== 'rooms') { + if (statusBoardLoading && statusBoardRooms.length === 0 && activeTab === 'status-board') { return ; } @@ -667,8 +680,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
- {rooms.length} rooms + {statusBoardRooms.length} rooms
- +
+ + +
{/* Floors Display */} @@ -761,26 +785,41 @@ const AdvancedRoomManagementPage: React.FC = () => { {/* Rooms Grid */}
{floorRooms.map((room) => { - const statusColors = getStatusColor(room.status); + // Determine effective status: if there are pending housekeeping tasks and room is available, show as cleaning + const effectiveStatus = room.pending_housekeeping_count > 0 && room.status === 'available' + ? 'cleaning' + : room.status; + const statusColors = getStatusColor(effectiveStatus); return (
toggleRoomExpansion(room.id)} > {/* Status Badge */} -
- {getStatusIcon(room.status)} - {getStatusLabel(room.status)} +
+ {getStatusIcon(effectiveStatus)} + {getStatusLabel(effectiveStatus)}
+ {/* Edit Button */} + + {/* Room Content */} -
+
toggleRoomExpansion(room.id)}>

{room.room_number}

@@ -794,16 +833,16 @@ const AdvancedRoomManagementPage: React.FC = () => { {/* Expanded Details */} {expandedRooms.has(room.id) && ( -
+
{room.current_booking && (
- + Guest
-

{room.current_booking.guest_name}

+

{room.current_booking.guest_name}

- + Check-out: {new Date(room.current_booking.check_out).toLocaleDateString()}
@@ -812,10 +851,10 @@ const AdvancedRoomManagementPage: React.FC = () => { {room.active_maintenance && (
- + Maintenance
-

{room.active_maintenance.title}

+

{room.active_maintenance.title}

{room.active_maintenance.type}

)} @@ -823,7 +862,7 @@ const AdvancedRoomManagementPage: React.FC = () => { {room.pending_housekeeping_count > 0 && (
- + Housekeeping

@@ -873,438 +912,266 @@ const AdvancedRoomManagementPage: React.FC = () => { {/* Inspections Tab */} {activeTab === 'inspections' && } - {/* Rooms Tab */} - {activeTab === 'rooms' && ( -

- {roomsLoading && } - -
-
-
-
-
- -
-

Room Management

-
-

- Manage hotel room information and availability + {/* Room Modal */} + {showRoomModal && ( +

+
+
+
+

+ {editingRoom ? 'Update Room' : 'Add New Room'} +

+

+ {editingRoom ? 'Modify room details and amenities' : 'Create a new luxurious room'}

-
- {selectedRooms.length > 0 && ( - - )} - -
-
-
- -
-
-
- - setRoomFilters({ ...roomFilters, search: e.target.value })} - className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md" - /> -
- - + +
-
- -
-
- - - - - - - - - - - - - - - {roomList.map((room) => ( - - - - - - - - - - - ))} - -
- 0 && selectedRooms.length === roomList.length} - onChange={handleSelectAllRooms} - title="Select all rooms" - className="w-4 h-4 sm:w-5 sm:h-5 text-amber-600 bg-slate-700 border-slate-600 rounded focus:ring-amber-500 cursor-pointer" - /> - Room NumberRoom TypeFloorPriceStatusFeaturedActions
- handleSelectRoom(room.id)} - className="w-4 h-4 sm:w-5 sm:h-5 text-amber-600 bg-white border-slate-300 rounded focus:ring-amber-500 cursor-pointer" - title={`Select room ${room.room_number}`} - /> - -
{room.room_number}
-
-
{room.room_type?.name || 'N/A'}
-
-
Floor {room.floor}
-
-
- {formatCurrency(room.price || room.room_type?.base_price || 0)} -
-
- {getRoomStatusBadge(room.status)} - - {room.featured ? ( - - ) : ( - - - )} - -
- - -
-
-
- -
- - {showRoomModal && ( -
-
-
+ +
+
+

+
+ Basic Information +

+ +
-

- {editingRoom ? 'Update Room' : 'Add New Room'} -

-

- {editingRoom ? 'Modify room details and amenities' : 'Create a new luxurious room'} -

+ + setRoomFormData({ ...roomFormData, room_number: e.target.value })} + className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300" + placeholder="e.g., 1001" + required + /> +
+
+ + setRoomFormData({ ...roomFormData, floor: parseInt(e.target.value) })} + className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300" + required + min="1" + />
-
- -
-

-
- Basic Information -

- -
-
- - setRoomFormData({ ...roomFormData, room_number: e.target.value })} - className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300" - placeholder="e.g., 1001" - required - /> -
-
- - setRoomFormData({ ...roomFormData, floor: parseInt(e.target.value) })} - className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300" - required - min="1" - /> -
-
- -
- - -
- -
- - -
- -
- setRoomFormData({ ...roomFormData, featured: e.target.checked })} - className="w-5 h-5 text-[#d4af37] bg-[#1a1a1a] border-[#d4af37]/30 rounded focus:ring-[#d4af37]/50 focus:ring-2 cursor-pointer transition-all" - /> - -
- -
- - setRoomFormData({ ...roomFormData, price: e.target.value })} - className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300" - placeholder="e.g., 150.00" - /> -
- -
- -