This commit is contained in:
Iliyan Angelov
2025-12-01 15:34:45 +02:00
parent 49181cf48c
commit f7d6f24e49
28 changed files with 2121 additions and 832 deletions

View File

@@ -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!")

View File

@@ -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!")

View File

@@ -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!")

View File

@@ -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:

View File

@@ -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:

View File

@@ -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']

View File

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