import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Hotel, Wrench, Sparkles, ClipboardCheck, Filter, RefreshCw, CheckCircle, AlertTriangle, Users, ChevronDown, ChevronUp, Calendar, MapPin, Search, Plus, Edit, Trash2, X, Image as ImageIcon, Check, } from 'lucide-react'; import { toast } from 'react-toastify'; import Loading from '../../components/common/Loading'; import advancedRoomService, { RoomStatusBoardItem, } from '../../services/api/advancedRoomService'; import { roomService, Room } from '../../services/api'; import MaintenanceManagement from '../../components/shared/MaintenanceManagement'; import HousekeepingManagement from '../../components/shared/HousekeepingManagement'; import InspectionManagement from '../../components/shared/InspectionManagement'; import Pagination from '../../components/common/Pagination'; import apiClient from '../../services/api/apiClient'; import { useFormatCurrency } from '../../hooks/useFormatCurrency'; import { logger } from '../../utils/logger'; type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections' | 'rooms'; const AdvancedRoomManagementPage: React.FC = () => { const { formatCurrency } = useFormatCurrency(); 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, room_type_id: 1, status: 'available' as 'available' | 'occupied' | 'maintenance', featured: false, price: '', description: '', capacity: '', room_size: '', view: '', amenities: [] as string[], }); const [availableAmenities, setAvailableAmenities] = useState([]); const [roomTypes, setRoomTypes] = useState>([]); const [uploadingImages, setUploadingImages] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); useEffect(() => { fetchRoomStatusBoard(); fetchFloors(); }, [selectedFloor]); const fetchRoomStatusBoard = 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 }); 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[]; setFloors(uniqueFloors); } } catch (error) { logger.error('Failed to fetch floors', error); } }; const toggleRoomExpansion = (roomId: number) => { const newExpanded = new Set(expandedRooms); if (newExpanded.has(roomId)) { newExpanded.delete(roomId); } else { newExpanded.add(roomId); } setExpandedRooms(newExpanded); }; // Group rooms by floor const roomsByFloor = useMemo(() => { const grouped: Record = {}; rooms.forEach(room => { if (!grouped[room.floor]) { grouped[room.floor] = []; } grouped[room.floor].push(room); }); return grouped; }, [rooms]); const getStatusColor = (status: string) => { switch (status) { case 'available': return { bg: 'bg-gradient-to-br from-emerald-50 via-green-50 to-emerald-100', border: 'border-emerald-300/50', text: 'text-emerald-800', badge: 'bg-gradient-to-r from-emerald-500 to-green-600 text-white', shadow: 'shadow-emerald-200/50' }; case 'occupied': return { bg: 'bg-gradient-to-br from-blue-50 via-indigo-50 to-blue-100', border: 'border-blue-300/50', text: 'text-blue-800', badge: 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white', shadow: 'shadow-blue-200/50' }; case 'maintenance': return { bg: 'bg-gradient-to-br from-red-50 via-rose-50 to-red-100', border: 'border-red-300/50', text: 'text-red-800', badge: 'bg-gradient-to-r from-red-500 to-rose-600 text-white', shadow: 'shadow-red-200/50' }; case 'cleaning': return { bg: 'bg-gradient-to-br from-amber-50 via-yellow-50 to-amber-100', border: 'border-amber-300/50', text: 'text-amber-800', badge: 'bg-gradient-to-r from-amber-500 to-yellow-600 text-white', shadow: 'shadow-amber-200/50' }; default: return { bg: 'bg-gradient-to-br from-gray-50 via-slate-50 to-gray-100', border: 'border-gray-300/50', text: 'text-gray-800', badge: 'bg-gradient-to-r from-gray-500 to-slate-600 text-white', shadow: 'shadow-gray-200/50' }; } }; const getStatusIcon = (status: string) => { const iconClass = "w-5 h-5"; switch (status) { case 'available': return ; case 'occupied': return ; case 'maintenance': return ; case 'cleaning': return ; default: return ; } }; const getStatusLabel = (status: string) => { switch (status) { case 'available': return 'Available'; case 'occupied': return 'Occupied'; case 'maintenance': return 'Maintenance'; case 'cleaning': return 'Cleaning'; default: return 'Unknown'; } }; // Rooms management functions const fetchAvailableAmenities = useCallback(async () => { try { const response = await roomService.getAmenities(); if (response.data?.amenities) { setAvailableAmenities(response.data.amenities); } } catch (error) { logger.error('Failed to fetch amenities', error); } }, []); const fetchRoomList = 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()); setRoomTypes(roomTypesList); setRoomFormData(prev => { if (!editingRoom && prev.room_type_id === 1 && roomTypesList.length > 0) { return { ...prev, room_type_id: roomTypesList[0].id }; } return prev; }); } catch (error) { logger.error('Failed to fetch room types', error); } }; fetchAllRoomTypes(); }, [activeTab, editingRoom]); const handleRoomSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingRoom) { const updateData = { ...roomFormData, price: roomFormData.price ? parseFloat(roomFormData.price) : undefined, description: roomFormData.description || undefined, capacity: roomFormData.capacity ? parseInt(roomFormData.capacity) : undefined, room_size: roomFormData.room_size || undefined, view: roomFormData.view || undefined, amenities: Array.isArray(roomFormData.amenities) ? roomFormData.amenities : [], }; await roomService.updateRoom(editingRoom.id, updateData); toast.success('Room updated successfully'); await fetchRoomList(); try { const updatedRoom = await roomService.getRoomByNumber(editingRoom.room_number); setEditingRoom(updatedRoom.data.room); } catch (err) { logger.error('Failed to refresh room data', err); } } else { const createData = { ...roomFormData, price: roomFormData.price ? parseFloat(roomFormData.price) : undefined, description: roomFormData.description || undefined, capacity: roomFormData.capacity ? parseInt(roomFormData.capacity) : undefined, room_size: roomFormData.room_size || undefined, view: roomFormData.view || undefined, 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) { try { setUploadingImages(true); const uploadFormData = new FormData(); selectedFiles.forEach(file => { uploadFormData.append('images', file); }); await apiClient.post(`/rooms/${response.data.room.id}/images`, uploadFormData, { headers: { 'Content-Type': 'multipart/form-data', }, }); toast.success('Images uploaded successfully'); setSelectedFiles([]); const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number); setEditingRoom(updatedRoom.data.room); } catch (uploadError: any) { toast.error(uploadError.response?.data?.message || 'Room created but failed to upload images'); const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number); setEditingRoom(updatedRoom.data.room); } finally { setUploadingImages(false); } } else { const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number); setEditingRoom(updatedRoom.data.room); } setRoomFormData({ room_number: response.data.room.room_number, floor: response.data.room.floor, room_type_id: response.data.room.room_type_id, status: response.data.room.status, featured: response.data.room.featured, price: response.data.room.price?.toString() || '', description: response.data.room.description || '', capacity: response.data.room.capacity?.toString() || '', room_size: response.data.room.room_size || '', view: response.data.room.view || '', amenities: response.data.room.amenities || [], }); await fetchRoomList(); return; } } setShowRoomModal(false); resetRoomForm(); fetchRoomList(); } catch (error: any) { toast.error(error.response?.data?.message || 'An error occurred'); } }; const handleEditRoom = async (room: Room) => { setEditingRoom(room); let amenitiesArray: string[] = []; if (room.amenities) { if (Array.isArray(room.amenities)) { amenitiesArray = room.amenities; } else if (typeof room.amenities === 'string') { try { const parsed = JSON.parse(room.amenities); amenitiesArray = Array.isArray(parsed) ? parsed : []; } catch { const amenitiesStr: string = room.amenities; amenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean); } } } setRoomFormData({ room_number: room.room_number, floor: room.floor, room_type_id: room.room_type_id, status: room.status, featured: room.featured, price: room.price?.toString() || '', description: room.description || '', capacity: room.capacity?.toString() || '', room_size: room.room_size || '', view: room.view || '', amenities: amenitiesArray, }); setShowRoomModal(true); try { const fullRoom = await roomService.getRoomByNumber(room.room_number); const roomData = fullRoom.data.room; let updatedAmenitiesArray: string[] = []; if (roomData.amenities) { if (Array.isArray(roomData.amenities)) { updatedAmenitiesArray = roomData.amenities; } else if (typeof roomData.amenities === 'string') { try { const parsed = JSON.parse(roomData.amenities); updatedAmenitiesArray = Array.isArray(parsed) ? parsed : []; } catch { const amenitiesStr: string = roomData.amenities; updatedAmenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean); } } } setRoomFormData({ room_number: roomData.room_number, floor: roomData.floor, room_type_id: roomData.room_type_id, status: roomData.status, featured: roomData.featured, price: roomData.price?.toString() || '', description: roomData.description || '', capacity: roomData.capacity?.toString() || '', room_size: roomData.room_size || '', view: roomData.view || '', amenities: updatedAmenitiesArray, }); setEditingRoom(roomData); } catch (error) { logger.error('Failed to fetch full room details', error); } }; const handleDeleteRoom = async (id: number) => { 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(); } 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)); } }; const resetRoomForm = () => { setEditingRoom(null); setRoomFormData({ room_number: '', floor: 1, room_type_id: 1, status: 'available', featured: false, price: '', description: '', capacity: '', room_size: '', view: '', amenities: [], }); setSelectedFiles([]); setUploadingImages(false); }; const toggleAmenity = (amenity: string) => { setRoomFormData(prev => ({ ...prev, amenities: prev.amenities.includes(amenity) ? prev.amenities.filter(a => a !== amenity) : [...prev.amenities, amenity] })); }; const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { const files = Array.from(e.target.files); setSelectedFiles(files); } }; const handleUploadImages = async () => { if (!editingRoom || selectedFiles.length === 0) return; try { setUploadingImages(true); const formData = new FormData(); selectedFiles.forEach(file => { formData.append('images', file); }); await apiClient.post(`/rooms/${editingRoom.id}/images`, formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); toast.success('Images uploaded successfully'); setSelectedFiles([]); fetchRoomList(); const response = await roomService.getRoomByNumber(editingRoom.room_number); setEditingRoom(response.data.room); } catch (error: any) { toast.error(error.response?.data?.message || 'Unable to upload images'); } finally { setUploadingImages(false); } }; const handleDeleteImage = async (imageUrl: string) => { if (!editingRoom) return; if (!window.confirm('Are you sure you want to delete this image?')) return; try { let imagePath = imageUrl; if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { try { const url = new URL(imageUrl); imagePath = url.pathname; } catch (e) { const match = imageUrl.match(/(\/uploads\/.*)/); imagePath = match ? match[1] : imageUrl; } } await apiClient.delete(`/rooms/${editingRoom.id}/images`, { params: { image_url: imagePath }, }); toast.success('Image deleted successfully'); fetchRoomList(); const response = await roomService.getRoomByNumber(editingRoom.room_number); setEditingRoom(response.data.room); } catch (error: any) { logger.error('Error deleting image', error); toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete image'); } }; const getRoomStatusBadge = (status: string) => { const badges: Record = { available: { bg: 'bg-gradient-to-r from-emerald-50 to-green-50', text: 'text-emerald-800', label: 'Available', border: 'border-emerald-200' }, occupied: { bg: 'bg-gradient-to-r from-blue-50 to-indigo-50', text: 'text-blue-800', label: 'Occupied', border: 'border-blue-200' }, maintenance: { bg: 'bg-gradient-to-r from-amber-50 to-yellow-50', text: 'text-amber-800', label: 'Maintenance', border: 'border-amber-200' }, }; const badge = badges[status] || badges.available; return ( {badge.label} ); }; if (loading && rooms.length === 0 && activeTab !== 'rooms') { return ; } return (

Advanced Room Management

Manage room status, maintenance, housekeeping, and inspections

{/* Tabs */}
{/* Status Board Tab */} {activeTab === 'status-board' && (
{/* Header Controls */}
{rooms.length} rooms
{/* Floors Display */} {Object.keys(roomsByFloor).length === 0 ? (

No rooms found

) : (
{Object.entries(roomsByFloor) .sort(([a], [b]) => parseInt(b) - parseInt(a)) .map(([floor, floorRooms]) => (
{/* Floor Header */}

Floor {floor}

{floorRooms.length} {floorRooms.length === 1 ? 'room' : 'rooms'}

{/* Rooms Grid */}
{floorRooms.map((room) => { const statusColors = getStatusColor(room.status); return (
toggleRoomExpansion(room.id)} > {/* Status Badge */}
{getStatusIcon(room.status)} {getStatusLabel(room.status)}
{/* Room Content */}

{room.room_number}

{room.room_type && ( {room.room_type} )}
{/* Expanded Details */} {expandedRooms.has(room.id) && (
{room.current_booking && (
Guest

{room.current_booking.guest_name}

Check-out: {new Date(room.current_booking.check_out).toLocaleDateString()}
)} {room.active_maintenance && (
Maintenance

{room.active_maintenance.title}

{room.active_maintenance.type}

)} {room.pending_housekeeping_count > 0 && (
Housekeeping

{room.pending_housekeeping_count} pending {room.pending_housekeeping_count === 1 ? 'task' : 'tasks'}

)} {!room.current_booking && !room.active_maintenance && room.pending_housekeeping_count === 0 && (

All Clear

)}
)} {/* Collapse Indicator */}
{expandedRooms.has(room.id) ? ( ) : ( )}
{/* Decorative Corner */}
); })}
))}
)}
)} {/* Maintenance Tab */} {activeTab === 'maintenance' && } {/* Housekeeping Tab */} {activeTab === 'housekeeping' && } {/* Inspections Tab */} {activeTab === 'inspections' && } {/* Rooms Tab */} {activeTab === 'rooms' && (
{roomsLoading && }

Room Management

Manage hotel room information and availability

{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 Number Room Type Floor Price Status Featured Actions
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 && (

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

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

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" />