This commit is contained in:
Iliyan Angelov
2025-12-03 01:31:34 +02:00
parent e32527ae8c
commit 5fb50983a9
37 changed files with 5844 additions and 201 deletions

View File

@@ -7,6 +7,8 @@ import {
X,
CheckCircle,
Clock,
RefreshCw,
Play,
} from 'lucide-react';
import { toast } from 'react-toastify';
import Loading from '../../../shared/components/Loading';
@@ -66,10 +68,23 @@ const HousekeepingManagement: React.FC = () => {
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 };
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;
@@ -176,7 +191,30 @@ const HousekeepingManagement: React.FC = () => {
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');
@@ -192,7 +230,7 @@ const HousekeepingManagement: React.FC = () => {
status: 'completed',
checklist_items: task.checklist_items?.map(item => ({ ...item, completed: true })) || [],
});
toast.success('Task marked as completed successfully');
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');
@@ -322,15 +360,26 @@ 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"
/>
</div>
{(isAdmin || userInfo?.role === 'staff') && (
<div className="flex items-center space-x-2">
<button
onClick={handleCreate}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
onClick={fetchTasks}
disabled={loading}
className="flex items-center space-x-2 px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors disabled:opacity-50"
title="Refresh tasks"
>
<Plus className="w-4 h-4" />
<span>New Task</span>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
<span>Refresh</span>
</button>
)}
{(isAdmin || userInfo?.role === 'staff') && (
<button
onClick={handleCreate}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<Plus className="w-4 h-4" />
<span>New Task</span>
</button>
)}
</div>
</div>
<div className="bg-white rounded-lg shadow overflow-hidden">
@@ -347,18 +396,34 @@ const HousekeepingManagement: React.FC = () => {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{tasks.map((task) => {
{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 (
<tr key={task.id} className="hover:bg-gray-50">
<tr
key={task.id || `room-${task.room_id}-${index}`}
className={`hover:bg-gray-50 ${isCleaningRoom ? 'bg-amber-50/50' : ''}`}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{task.room_number || `Room ${task.room_id}`}</div>
<div className="flex items-center space-x-2">
<div className="text-sm font-medium text-gray-900">
{task.room_number || `Room ${task.room_id}`}
</div>
{isCleaningRoom && (
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-amber-100 text-amber-800 border border-amber-200">
Cleaning
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500 capitalize">{task.task_type}</div>
<div className="text-sm text-gray-500 capitalize">
{isRoomStatusOnly ? 'Room Status' : task.task_type}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(task.status)}`}>
@@ -366,60 +431,107 @@ const HousekeepingManagement: React.FC = () => {
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(task.scheduled_time).toLocaleString()}
{task.scheduled_time ? new Date(task.scheduled_time).toLocaleString() : 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{task.assigned_staff_name || 'Unassigned'}
{task.assigned_staff_name || (isRoomStatusOnly ? 'Not Assigned' : 'Unassigned')}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-16 bg-gray-200 rounded-full h-2 mr-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${progress}%` }}
/>
{totalItems > 0 ? (
<div className="flex items-center">
<div className="w-16 bg-gray-200 rounded-full h-2 mr-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-xs text-gray-600">{progress}%</span>
</div>
<span className="text-xs text-gray-600">{progress}%</span>
</div>
) : (
<span className="text-xs text-gray-400">No checklist</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => setViewingTask(task)}
className="text-blue-600 hover:text-blue-900"
title="View task"
>
<Eye className="w-4 h-4" />
</button>
{isAdmin ? (
{task.id && (
<button
onClick={() => handleEdit(task)}
className="text-indigo-600 hover:text-indigo-900"
title="Edit task"
onClick={() => setViewingTask(task)}
className="text-blue-600 hover:text-blue-900"
title="View task"
>
<Edit className="w-4 h-4" />
<Eye className="w-4 h-4" />
</button>
)}
{isRoomStatusOnly ? (
// For room status entries, allow creating a task
(isAdmin || userInfo?.role === 'staff') && (
<button
onClick={() => {
setFormData({
room_id: task.room_id.toString(),
booking_id: '',
task_type: 'vacant',
scheduled_time: new Date(),
assigned_to: '',
checklist_items: [],
notes: '',
estimated_duration_minutes: '',
});
setEditingTask(null);
setShowModal(true);
}}
className="text-indigo-600 hover:text-indigo-900"
title="Create task for this room"
>
<Plus className="w-4 h-4" />
</button>
)
) : (
// Housekeeping and staff can only edit their own assigned tasks
(isHousekeeping || userInfo?.role === 'staff') &&
task.assigned_to === userInfo?.id &&
task.status !== 'completed' && (
<>
<button
onClick={() => handleEdit(task)}
className="text-indigo-600 hover:text-indigo-900"
title="Update task"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleMarkAsDone(task)}
className="text-green-600 hover:text-green-900"
title="Mark as done"
>
<CheckCircle className="w-4 h-4" />
</button>
</>
// For actual tasks
isAdmin ? (
<button
onClick={() => handleEdit(task)}
className="text-indigo-600 hover:text-indigo-900"
title="Edit task"
>
<Edit className="w-4 h-4" />
</button>
) : (
// Housekeeping and staff actions
(isHousekeeping || userInfo?.role === 'staff') && (
<>
{task.status === 'pending' && !task.assigned_to && (
// Show Start button for unassigned pending tasks
<button
onClick={() => handleStartTask(task)}
className="text-blue-600 hover:text-blue-900"
title="Start cleaning this room"
>
<Play className="w-4 h-4" />
</button>
)}
{task.assigned_to === userInfo?.id && task.status !== 'completed' && (
<>
<button
onClick={() => handleEdit(task)}
className="text-indigo-600 hover:text-indigo-900"
title="Update task"
>
<Edit className="w-4 h-4" />
</button>
{task.status === 'in_progress' && (
<button
onClick={() => handleMarkAsDone(task)}
className="text-green-600 hover:text-green-900"
title="Mark as done - Room ready for check-in"
>
<CheckCircle className="w-4 h-4" />
</button>
)}
</>
)}
</>
)
)
)}
</div>