import React, { useState, useEffect } from 'react'; import { Plus, Edit, Eye, Search, X, CheckCircle, Clock, RefreshCw, Play, } from 'lucide-react'; import { toast } from 'react-toastify'; import Loading from '../../../shared/components/Loading'; import Pagination from '../../../shared/components/Pagination'; import advancedRoomService, { HousekeepingTask, ChecklistItem } from '../../rooms/services/advancedRoomService'; import roomService, { Room } from '../../rooms/services/roomService'; import userService, { User as UserType } from '../../auth/services/userService'; import useAuthStore from '../../../store/useAuthStore'; import DatePicker from 'react-datepicker'; 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([]); const [staff, setStaff] = useState([]); const [showModal, setShowModal] = useState(false); const [editingTask, setEditingTask] = useState(null); const [viewingTask, setViewingTask] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [filters, setFilters] = useState({ room_id: '', status: '', task_type: '', date: '', }); const [formData, setFormData] = useState({ room_id: '', booking_id: '', task_type: 'vacant' as 'checkout' | 'stayover' | 'vacant' | 'inspection' | 'turndown', scheduled_time: new Date(), assigned_to: '', checklist_items: [] as ChecklistItem[], notes: '', estimated_duration_minutes: '', }); const defaultChecklistItems: Record = { checkout: ['Bathroom cleaned', 'Beds made', 'Trash emptied', 'Towels replaced', 'Amenities restocked', 'Floor vacuumed'], stayover: ['Beds made', 'Trash emptied', 'Towels replaced', 'Bathroom cleaned'], vacant: ['Deep clean bathroom', 'Change linens', 'Vacuum and mop', 'Dust surfaces', 'Check amenities'], inspection: ['Check all amenities', 'Test electronics', 'Check for damages', 'Verify cleanliness'], turndown: ['Prepare bed', 'Close curtains', 'Place amenities', 'Adjust lighting'], }; useEffect(() => { fetchRooms(); fetchStaff(); }, []); useEffect(() => { fetchTasks(); }, [currentPage, filters]); // Auto-refresh every 30 seconds for real-time updates useEffect(() => { const interval = setInterval(() => { fetchTasks(); }, 30000); // Refresh every 30 seconds return () => clearInterval(interval); }, [currentPage, filters]); const fetchTasks = async () => { try { setLoading(true); const params: any = { page: currentPage, limit: 10, include_cleaning_rooms: true // Include rooms in cleaning status }; if (filters.room_id) params.room_id = parseInt(filters.room_id); if (filters.status) params.status = filters.status; if (filters.task_type) params.task_type = filters.task_type; if (filters.date) params.date = filters.date; const response = await advancedRoomService.getHousekeepingTasks(params); if (response.status === 'success') { setTasks(response.data.tasks); setTotalPages(response.data.pagination?.total_pages || 1); } } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to fetch housekeeping tasks'); } finally { setLoading(false); } }; const fetchRooms = async () => { try { const allRooms: Room[] = []; let page = 1; let hasMorePages = true; while (hasMorePages) { const response = await roomService.getRooms({ limit: 100, page }); if (response.success || response.status === 'success') { if (response.data?.rooms) { allRooms.push(...response.data.rooms); // Check if there are more pages if (response.data.pagination) { hasMorePages = page < response.data.pagination.totalPages; page++; } else { hasMorePages = false; } } else { hasMorePages = false; } } else { console.error('Failed to fetch rooms: Invalid response structure', response); hasMorePages = false; } } setRooms(allRooms); } catch (error) { console.error('Failed to fetch rooms:', error); toast.error('Failed to load rooms. Please try again.'); } }; const fetchStaff = async () => { try { const response = await userService.getUsers({ role: 'staff', limit: 100 }); if (response.data?.users) { setStaff(response.data.users); } } catch (error) { console.error('Failed to fetch staff:', error); } }; const handleCreate = () => { setEditingTask(null); setFormData({ room_id: '', booking_id: '', task_type: 'vacant', scheduled_time: new Date(), assigned_to: '', checklist_items: [], notes: '', estimated_duration_minutes: '', }); // Ensure rooms are loaded when opening the modal if (rooms.length === 0) { fetchRooms(); } setShowModal(true); }; const handleTaskTypeChange = (type: string) => { const items = defaultChecklistItems[type] || []; setFormData({ ...formData, task_type: type as any, checklist_items: items.map(item => ({ item, completed: false, notes: '' })), }); }; const handleEdit = (task: HousekeepingTask) => { setEditingTask(task); setFormData({ room_id: task.room_id.toString(), booking_id: task.booking_id?.toString() || '', task_type: task.task_type, scheduled_time: new Date(task.scheduled_time), assigned_to: task.assigned_to?.toString() || '', checklist_items: task.checklist_items || [], notes: task.notes || '', estimated_duration_minutes: task.estimated_duration_minutes?.toString() || '', }); setShowModal(true); }; const handleStartTask = async (task: HousekeepingTask) => { if (!task.id) { toast.error('Cannot start task: Invalid task ID'); return; } try { await advancedRoomService.updateHousekeepingTask(task.id, { status: 'in_progress', assigned_to: userInfo?.id, // Assign to current user when starting }); toast.success('Task started successfully'); fetchTasks(); } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to start task'); } }; const handleMarkAsDone = async (task: HousekeepingTask) => { if (!task.id) { toast.error('Cannot complete task: Invalid task ID'); return; } // Double check that the task is assigned to the current user if (!task.assigned_to) { toast.error('Task must be assigned before it can be marked as done'); return; } if (task.assigned_to !== userInfo?.id) { toast.error('Only the assigned staff member can mark this task as done'); return; } try { await advancedRoomService.updateHousekeepingTask(task.id, { status: 'completed', checklist_items: task.checklist_items?.map(item => ({ ...item, completed: true })) || [], }); toast.success('Task marked as completed successfully. Room is now ready for check-in.'); fetchTasks(); } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to mark task as done'); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingTask && editingTask.id) { // For staff, only allow updating status and checklist items if (!isAdmin) { const data = { status: editingTask.status, checklist_items: formData.checklist_items, notes: formData.notes, // Allow staff to add notes }; await advancedRoomService.updateHousekeepingTask(editingTask.id, data); } else { // Admin can update all fields const data = { room_id: parseInt(formData.room_id), booking_id: formData.booking_id ? parseInt(formData.booking_id) : undefined, task_type: formData.task_type, scheduled_time: formData.scheduled_time.toISOString(), assigned_to: formData.assigned_to ? parseInt(formData.assigned_to) : undefined, checklist_items: formData.checklist_items, notes: formData.notes, estimated_duration_minutes: formData.estimated_duration_minutes ? parseInt(formData.estimated_duration_minutes) : undefined, status: editingTask.status, }; await advancedRoomService.updateHousekeepingTask(editingTask.id, data); } toast.success('Housekeeping task updated successfully'); } else { // Only admin and staff can create tasks if (!isAdmin && userInfo?.role !== 'staff') { toast.error('You do not have permission to create tasks'); return; } const data = { room_id: parseInt(formData.room_id), booking_id: formData.booking_id ? parseInt(formData.booking_id) : undefined, task_type: formData.task_type, scheduled_time: formData.scheduled_time.toISOString(), assigned_to: formData.assigned_to ? parseInt(formData.assigned_to) : undefined, checklist_items: formData.checklist_items, notes: formData.notes, estimated_duration_minutes: formData.estimated_duration_minutes ? parseInt(formData.estimated_duration_minutes) : undefined, }; await advancedRoomService.createHousekeepingTask(data); toast.success('Housekeeping task created successfully'); } setShowModal(false); fetchTasks(); } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to save housekeeping task'); } }; const updateChecklistItem = (index: number, field: 'completed' | 'notes', value: any) => { const updated = [...formData.checklist_items]; updated[index] = { ...updated[index], [field]: value }; setFormData({ ...formData, checklist_items: updated }); }; const getStatusColor = (status: string) => { switch (status) { case 'completed': return 'bg-green-100 text-green-800'; case 'in_progress': return 'bg-blue-100 text-blue-800'; case 'pending': return 'bg-yellow-100 text-yellow-800'; case 'skipped': return 'bg-gray-100 text-gray-800'; default: return 'bg-gray-100 text-gray-800'; } }; if (loading && tasks.length === 0) { return ; } return (
setFilters({ ...filters, room_id: e.target.value })} />
setFilters({ ...filters, date: e.target.value })} className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
{(isAdmin || userInfo?.role === 'staff') && ( )}
{tasks.map((task, index) => { const completedItems = task.checklist_items?.filter(item => item.completed).length || 0; const totalItems = task.checklist_items?.length || 0; const progress = totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0; const isRoomStatusOnly = task.is_room_status_only || task.id === null; const isCleaningRoom = task.room_status === 'cleaning' || isRoomStatusOnly; return ( ); })}
Room Type Status Scheduled Assigned Progress Actions
{task.room_number || `Room ${task.room_id}`}
{isCleaningRoom && ( Cleaning )}
{isRoomStatusOnly ? 'Room Status' : task.task_type}
{task.status.replace('_', ' ')} {task.scheduled_time ? new Date(task.scheduled_time).toLocaleString() : 'N/A'} {task.assigned_staff_name || (isRoomStatusOnly ? 'Not Assigned' : 'Unassigned')} {totalItems > 0 ? (
{progress}%
) : ( No checklist )}
{task.id && ( )} {isRoomStatusOnly ? ( // For room status entries, allow creating a task (isAdmin || userInfo?.role === 'staff') && ( ) ) : ( // For actual tasks isAdmin ? ( ) : ( // Housekeeping and staff actions (isHousekeeping || userInfo?.role === 'staff') && ( <> {task.status === 'pending' && !task.assigned_to && ( // Show Start button for unassigned pending tasks )} {task.assigned_to === userInfo?.id && task.status !== 'completed' && ( <> {task.status === 'in_progress' && ( )} )} ) ) )}
{totalPages > 1 && (
)} {/* Create/Edit Modal */} {showModal && (

{editingTask ? (isAdmin ? 'Edit Housekeeping Task' : 'Update Task Status') : 'New Housekeeping Task'}

date && setFormData({ ...formData, scheduled_time: date })} showTimeSelect dateFormat="yyyy-MM-dd HH:mm" disabled={!isAdmin && editingTask !== null} className={`w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${!isAdmin && editingTask !== null ? 'bg-gray-100 cursor-not-allowed' : ''}`} />
{/* Status field - staff can update this */} {editingTask && (
)}
{formData.checklist_items.map((item, index) => (
updateChecklistItem(index, 'completed', e.target.checked)} className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
updateChecklistItem(index, 'notes', e.target.value)} className="mt-1 w-full border border-gray-300 rounded-md px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500" />
))} {formData.checklist_items.length === 0 && (

Select a task type to load checklist items

)}
setFormData({ ...formData, estimated_duration_minutes: e.target.value })} className={`w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${!isAdmin && editingTask !== null ? 'bg-gray-100 cursor-not-allowed' : ''}`} />