updates
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface MaintenanceRecord {
|
||||
}
|
||||
|
||||
export interface HousekeepingTask {
|
||||
id: number;
|
||||
id: number | null; // null for room status entries without tasks
|
||||
room_id: number;
|
||||
room_number?: string;
|
||||
booking_id?: number;
|
||||
@@ -42,6 +42,8 @@ export interface HousekeepingTask {
|
||||
quality_score?: number;
|
||||
estimated_duration_minutes?: number;
|
||||
actual_duration_minutes?: number;
|
||||
room_status?: 'available' | 'occupied' | 'maintenance' | 'cleaning';
|
||||
is_room_status_only?: boolean; // Flag to indicate this is from room status, not a task
|
||||
}
|
||||
|
||||
export interface ChecklistItem {
|
||||
|
||||
@@ -526,6 +526,7 @@ const IPWhitelistTab: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop Table */}
|
||||
@@ -701,6 +702,7 @@ const IPBlacklistTab: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop Table */}
|
||||
@@ -1012,6 +1014,7 @@ const OAuthProvidersTab: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop Table */}
|
||||
|
||||
@@ -356,23 +356,29 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
date: today,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
include_cleaning_rooms: true, // Include rooms in cleaning status
|
||||
});
|
||||
|
||||
if (response.status === 'success' && response.data?.tasks) {
|
||||
const userTasks = response.data.tasks.filter(
|
||||
(task: HousekeepingTask) => task.assigned_to === userInfo?.id
|
||||
// Backend already filters to show assigned tasks and unassigned tasks
|
||||
// Just filter out completed tasks that aren't assigned to current user
|
||||
const allTasks = response.data.tasks.filter(
|
||||
(task: HousekeepingTask) =>
|
||||
task.assigned_to === userInfo?.id || // Assigned to current user
|
||||
!task.assigned_to || // Unassigned tasks (can be picked up)
|
||||
task.is_room_status_only // Room status entries
|
||||
);
|
||||
setTasks(userTasks);
|
||||
setTasks(allTasks);
|
||||
|
||||
const pending = userTasks.filter((t: HousekeepingTask) => t.status === 'pending').length;
|
||||
const in_progress = userTasks.filter((t: HousekeepingTask) => t.status === 'in_progress').length;
|
||||
const completed = userTasks.filter((t: HousekeepingTask) => t.status === 'completed').length;
|
||||
const pending = allTasks.filter((t: HousekeepingTask) => t.status === 'pending').length;
|
||||
const in_progress = allTasks.filter((t: HousekeepingTask) => t.status === 'in_progress').length;
|
||||
const completed = allTasks.filter((t: HousekeepingTask) => t.status === 'completed').length;
|
||||
|
||||
setStats({
|
||||
pending,
|
||||
in_progress,
|
||||
completed,
|
||||
total: userTasks.length,
|
||||
total: allTasks.length,
|
||||
});
|
||||
} else {
|
||||
setTasks([]);
|
||||
@@ -406,6 +412,8 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!userInfo?.id) return;
|
||||
|
||||
fetchTasks();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
@@ -414,11 +422,168 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// WebSocket connection for real-time notifications
|
||||
let websocket: WebSocket | null = null;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
let pingInterval: NodeJS.Timeout | null = null;
|
||||
let isIntentionallyClosing = false;
|
||||
|
||||
const connectWebSocket = () => {
|
||||
// Don't connect if already connected or intentionally closing
|
||||
if (websocket && (websocket.readyState === WebSocket.CONNECTING || websocket.readyState === WebSocket.OPEN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
const normalizedBase = baseUrl.replace(/\/$/, '');
|
||||
const wsProtocol = normalizedBase.startsWith('https') ? 'wss' : 'ws';
|
||||
const wsBase = normalizedBase.replace(/^https?/, wsProtocol);
|
||||
const wsUrl = `${wsBase}/api/notifications/ws`;
|
||||
|
||||
logger.debug('Attempting to connect WebSocket...', { url: wsUrl });
|
||||
websocket = new WebSocket(wsUrl);
|
||||
|
||||
websocket.onopen = () => {
|
||||
logger.debug('Housekeeping notification WebSocket connected');
|
||||
isIntentionallyClosing = false;
|
||||
|
||||
// Clear any pending reconnect
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
|
||||
// Set up ping interval to keep connection alive (every 30 seconds)
|
||||
pingInterval = setInterval(() => {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
websocket.send(JSON.stringify({ type: 'ping' }));
|
||||
} catch (error) {
|
||||
logger.error('Error sending ping', error);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle pong responses
|
||||
if (data.type === 'pong' || data.type === 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'housekeeping_task_available') {
|
||||
const taskData = data.data;
|
||||
toast.info(
|
||||
`New cleaning task available: Room ${taskData.room_number || taskData.room_id}`,
|
||||
{
|
||||
onClick: () => {
|
||||
fetchTasks();
|
||||
},
|
||||
autoClose: 10000,
|
||||
}
|
||||
);
|
||||
// Refresh tasks to show the new one
|
||||
fetchTasks();
|
||||
} else if (data.type === 'housekeeping_task_assigned') {
|
||||
const taskData = data.data;
|
||||
if (taskData.assigned_to === userInfo?.id) {
|
||||
toast.success(
|
||||
`You've been assigned to clean Room ${taskData.room_number || taskData.room_id}`,
|
||||
{
|
||||
onClick: () => {
|
||||
fetchTasks();
|
||||
},
|
||||
autoClose: 8000,
|
||||
}
|
||||
);
|
||||
fetchTasks();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error parsing WebSocket message', error);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
logger.error('Housekeeping notification WebSocket error', error);
|
||||
};
|
||||
|
||||
websocket.onclose = (event) => {
|
||||
logger.debug('Housekeeping notification WebSocket disconnected', {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean
|
||||
});
|
||||
|
||||
// Clear ping interval
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
pingInterval = null;
|
||||
}
|
||||
|
||||
// Only attempt to reconnect if:
|
||||
// 1. It wasn't an intentional close (code 1000)
|
||||
// 2. It wasn't a normal navigation away (code 1001)
|
||||
// 3. We're not already trying to reconnect
|
||||
if (!isIntentionallyClosing && event.code !== 1000 && event.code !== 1001 && !reconnectTimeout) {
|
||||
logger.debug('Scheduling WebSocket reconnection...');
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectTimeout = null;
|
||||
logger.debug('Attempting to reconnect WebSocket...');
|
||||
connectWebSocket();
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error creating WebSocket connection', error);
|
||||
// Schedule retry on error
|
||||
if (!reconnectTimeout) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectTimeout = null;
|
||||
connectWebSocket();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Delay connection slightly to ensure cookies are available
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
if (tasksAbortRef.current) {
|
||||
tasksAbortRef.current.abort();
|
||||
}
|
||||
clearInterval(interval);
|
||||
clearTimeout(connectionTimeout);
|
||||
|
||||
isIntentionallyClosing = true;
|
||||
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
pingInterval = null;
|
||||
}
|
||||
|
||||
if (websocket) {
|
||||
try {
|
||||
if (websocket.readyState === WebSocket.OPEN || websocket.readyState === WebSocket.CONNECTING) {
|
||||
websocket.close(1000, 'Component unmounting');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error closing WebSocket', error);
|
||||
}
|
||||
websocket = null;
|
||||
}
|
||||
};
|
||||
}, [userInfo?.id]);
|
||||
|
||||
@@ -513,17 +678,69 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
}, [currentFloorTasks]);
|
||||
|
||||
const handleStartTask = async (task: HousekeepingTask) => {
|
||||
// If task has no ID, it's a room status entry - create a task first
|
||||
if (!task.id) {
|
||||
if (updatingTasks.has(task.room_id)) return;
|
||||
|
||||
setUpdatingTasks(prev => new Set(prev).add(task.room_id));
|
||||
try {
|
||||
// Create a new task for this room
|
||||
const defaultChecklist = [
|
||||
{ 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: '' }
|
||||
];
|
||||
|
||||
const createResponse = await advancedRoomService.createHousekeepingTask({
|
||||
room_id: task.room_id,
|
||||
task_type: task.task_type || 'vacant',
|
||||
scheduled_time: new Date().toISOString(),
|
||||
assigned_to: userInfo?.id,
|
||||
checklist_items: defaultChecklist,
|
||||
notes: 'Task created by housekeeping staff',
|
||||
estimated_duration_minutes: 45
|
||||
});
|
||||
|
||||
if (createResponse.status === 'success' && createResponse.data?.task_id) {
|
||||
// Now update the newly created task to in_progress
|
||||
await advancedRoomService.updateHousekeepingTask(createResponse.data.task_id, {
|
||||
status: 'in_progress',
|
||||
assigned_to: userInfo?.id,
|
||||
});
|
||||
|
||||
toast.success('Task created and started successfully! Room cleaning in progress.');
|
||||
await fetchTasks();
|
||||
} else {
|
||||
toast.error('Failed to create task');
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error creating and starting task', error);
|
||||
toast.error(error.response?.data?.detail || 'Failed to create and start task');
|
||||
} finally {
|
||||
setUpdatingTasks(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(task.room_id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle existing task
|
||||
if (updatingTasks.has(task.id)) return;
|
||||
|
||||
setUpdatingTasks(prev => new Set(prev).add(task.id));
|
||||
try {
|
||||
await advancedRoomService.updateHousekeepingTask(task.id, {
|
||||
status: 'in_progress',
|
||||
assigned_to: userInfo?.id, // Assign to current user when starting
|
||||
});
|
||||
toast.success('Task started successfully!');
|
||||
toast.success('Task started successfully! Room cleaning in progress.');
|
||||
await fetchTasks();
|
||||
if (selectedTask?.id === task.id) {
|
||||
setSelectedTask({ ...task, status: 'in_progress' });
|
||||
setSelectedTask({ ...task, status: 'in_progress', assigned_to: userInfo?.id });
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error starting task', error);
|
||||
@@ -538,6 +755,10 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleUpdateChecklist = async (task: HousekeepingTask, itemIndex: number, checked: boolean) => {
|
||||
if (!task.id) {
|
||||
toast.error('Cannot update checklist: Invalid task ID');
|
||||
return;
|
||||
}
|
||||
if (updatingTasks.has(task.id)) return;
|
||||
|
||||
if (!task.checklist_items) return;
|
||||
@@ -586,6 +807,10 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleCompleteTask = async (task: HousekeepingTask) => {
|
||||
if (!task.id) {
|
||||
toast.error('Cannot complete task: Invalid task ID');
|
||||
return;
|
||||
}
|
||||
if (updatingTasks.has(task.id)) return;
|
||||
|
||||
const allCompleted = task.checklist_items?.every(item => item.completed) ?? true;
|
||||
@@ -608,7 +833,7 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
status: 'completed',
|
||||
checklist_items: updatedChecklist,
|
||||
});
|
||||
toast.success('Task completed successfully! 🎉');
|
||||
toast.success('Task completed successfully! Room is now ready for check-in. 🎉');
|
||||
await fetchTasks();
|
||||
closeTaskModal();
|
||||
} catch (error: any) {
|
||||
@@ -854,17 +1079,17 @@ const HousekeepingDashboardPage: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5">
|
||||
{currentFloorTasks.map((task) => {
|
||||
{currentFloorTasks.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 isUpdating = updatingTasks.has(task.id);
|
||||
const canStart = task.status === 'pending';
|
||||
const canComplete = task.status === 'in_progress' || task.status === 'pending';
|
||||
const isUpdating = task.id ? updatingTasks.has(task.id) : updatingTasks.has(task.room_id);
|
||||
const canStart = task.status === 'pending' && (!task.assigned_to || task.assigned_to === userInfo?.id);
|
||||
const canComplete = task.id && (task.status === 'in_progress' || task.status === 'pending') && task.assigned_to === userInfo?.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
key={task.id || `room-${task.room_id}-${index}`}
|
||||
className="enterprise-card p-4 sm:p-5 hover:shadow-2xl transition-all duration-300 cursor-pointer group border border-gray-200/50"
|
||||
onClick={() => openTaskModal(task)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user