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

@@ -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 */}

View File

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