Files
Hotel-Booking/Frontend/src/features/rooms/contexts/RoomContext.tsx
Iliyan Angelov 4cbcdde369 updates
2025-12-12 16:22:41 +02:00

376 lines
12 KiB
TypeScript

import React, { createContext, useContext, useState, useCallback, useEffect, useRef, useMemo } from 'react';
import roomService, { Room } from '../services/roomService';
import advancedRoomService, { RoomStatusBoardItem } from '../services/advancedRoomService';
import { toast } from 'react-toastify';
import { logger } from '../../../shared/utils/logger';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
interface RoomContextType {
// Room list state
rooms: Room[];
roomsLoading: boolean;
roomsError: string | null;
// Room status board state
statusBoardRooms: RoomStatusBoardItem[];
statusBoardLoading: boolean;
statusBoardError: string | null;
// Actions
refreshRooms: () => Promise<void>;
refreshStatusBoard: (floor?: number) => Promise<void>;
updateRoom: (roomId: number, updates: Partial<Room>) => Promise<void>;
deleteRoom: (roomId: number) => Promise<void>;
createRoom: (roomData: Partial<Room> & { room_number: string; floor: number; room_type_id: number; status: 'available' | 'occupied' | 'maintenance' }) => Promise<void>;
// Filters and pagination
setRoomFilters: (filters: { search?: string; status?: string; type?: string }) => void;
roomFilters: { search: string; status: string; type: string };
setRoomPage: (page: number) => void;
roomPage: number;
// Status board filters
setStatusBoardFloor: (floor: number | null) => void;
statusBoardFloor: number | null;
// Last update timestamp for synchronization
lastUpdate: number;
}
const RoomContext = createContext<RoomContextType | undefined>(undefined);
// eslint-disable-next-line react-refresh/only-export-components
export const useRoomContext = () => {
const context = useContext(RoomContext);
if (!context) {
// Provide a more helpful error message with debugging info
const error = new Error(
'useRoomContext must be used within a RoomProvider. ' +
'Make sure RoomProvider wraps your component tree in App.tsx'
);
if (import.meta.env.DEV) {
console.error('RoomContext Error:', {
context,
stack: error.stack,
// Check if we're in a development environment
isDev: import.meta.env.DEV,
hint: 'This error usually occurs when: 1) Component is rendered outside RoomProvider, 2) Hot module reload issue, 3) Context provider not mounted yet',
});
}
throw error;
}
return context;
};
interface RoomProviderProps {
children: React.ReactNode;
}
export const RoomProvider: React.FC<RoomProviderProps> = ({ children }) => {
// Room list state
const [rooms, setRooms] = useState<Room[]>([]);
const [roomsLoading, setRoomsLoading] = useState(false);
const [roomsError, setRoomsError] = useState<string | null>(null);
// Room status board state
const [statusBoardRooms, setStatusBoardRooms] = useState<RoomStatusBoardItem[]>([]);
const [statusBoardLoading, setStatusBoardLoading] = useState(false);
const [statusBoardError, setStatusBoardError] = useState<string | null>(null);
// Filters and pagination
const [roomFilters, setRoomFiltersState] = useState({ search: '', status: '', type: '' });
const [roomPage, setRoomPageState] = useState(1);
const [statusBoardFloor, setStatusBoardFloorState] = useState<number | null>(null);
// Last update timestamp
const [lastUpdate, setLastUpdate] = useState<number>(Date.now());
// Abort controllers for cleanup
const roomsAbortRef = useRef<AbortController | null>(null);
const statusBoardAbortRef = useRef<AbortController | null>(null);
// Auto-refresh interval (30 seconds)
const autoRefreshIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Refresh rooms list - fetch all rooms for better sync across components
const refreshRooms = useCallback(async () => {
// Cancel previous request
if (roomsAbortRef.current) {
roomsAbortRef.current.abort();
}
// Create new abort controller
roomsAbortRef.current = new AbortController();
try {
setRoomsLoading(true);
setRoomsError(null);
// Fetch rooms with reasonable pagination
// Individual components can handle their own pagination/filtering
const response = await roomService.getRooms({
limit: 100, // Reasonable batch size for sync
page: 1,
}, roomsAbortRef.current.signal);
if (response.data?.rooms) {
setRooms(response.data.rooms);
setLastUpdate(Date.now());
}
} catch (error: unknown) {
// Type guard for abort errors
const isAbortError = (e: unknown): boolean => {
if (typeof e === 'object' && e !== null) {
const err = e as { name?: string; code?: string; isCancelled?: boolean };
return err.name === 'AbortError' || err.code === 'ERR_CANCELED' || err.isCancelled === true;
}
return false;
};
if (isAbortError(error)) {
return;
}
logger.error('Error refreshing rooms', error);
setRoomsError(getUserFriendlyError(error) || 'Failed to refresh rooms');
// Don't show toast on every auto-refresh, only on manual refresh
} finally {
setRoomsLoading(false);
}
}, []);
// Refresh status board
const refreshStatusBoard = useCallback(async (floor?: number) => {
// Cancel previous request
if (statusBoardAbortRef.current) {
statusBoardAbortRef.current.abort();
}
// Create new abort controller
statusBoardAbortRef.current = new AbortController();
try {
setStatusBoardLoading(true);
setStatusBoardError(null);
const response = await advancedRoomService.getRoomStatusBoard(
floor || statusBoardFloor || undefined,
statusBoardAbortRef.current.signal
);
if (response.status === 'success' && response.data?.rooms) {
setStatusBoardRooms(response.data.rooms);
setLastUpdate(Date.now());
}
} catch (error: unknown) {
// Type guard for abort errors
const isAbortError = (e: unknown): boolean => {
if (typeof e === 'object' && e !== null) {
const err = e as { name?: string; code?: string; isCancelled?: boolean };
return err.name === 'AbortError' || err.code === 'ERR_CANCELED' || err.isCancelled === true;
}
return false;
};
if (isAbortError(error)) {
return;
}
// Handle 401 Unauthorized gracefully - user may not have admin/staff role
if (isAxiosError(error) && error.response?.status === 401) {
setStatusBoardError(null); // Don't set error for unauthorized access
setStatusBoardRooms([]); // Clear status board if unauthorized
return; // Silently return without logging
}
logger.error('Error refreshing status board', error);
setStatusBoardError(getUserFriendlyError(error) || 'Failed to refresh status board');
// Don't show toast on every auto-refresh, only on manual refresh
} finally {
setStatusBoardLoading(false);
}
}, [statusBoardFloor]);
// Update room
const updateRoom = useCallback(async (roomId: number, updates: Partial<Room>) => {
try {
await roomService.updateRoom(roomId, updates);
toast.success('Room updated successfully');
// Refresh both views to ensure they're in sync
await Promise.all([
refreshRooms(),
refreshStatusBoard(),
]);
} catch (error: unknown) {
logger.error('Error updating room', error);
toast.error(getUserFriendlyError(error) || 'Failed to update room');
throw error;
}
}, [refreshRooms, refreshStatusBoard]);
// Delete room
const deleteRoom = useCallback(async (roomId: number) => {
try {
await roomService.deleteRoom(roomId);
toast.success('Room deleted successfully');
// Refresh both views
await Promise.all([
refreshRooms(),
refreshStatusBoard(),
]);
} catch (error: unknown) {
logger.error('Error deleting room', error);
toast.error(getUserFriendlyError(error) || 'Failed to delete room');
throw error;
}
}, [refreshRooms, refreshStatusBoard]);
// Create room
const createRoom = useCallback(async (roomData: Partial<Room> & { room_number: string; floor: number; room_type_id: number; status: 'available' | 'occupied' | 'maintenance' }) => {
try {
await roomService.createRoom(roomData as Partial<Room> & { room_number: string; floor: number; room_type_id: number; status: 'available' | 'occupied' | 'maintenance' });
toast.success('Room created successfully');
// Refresh both views
await Promise.all([
refreshRooms(),
refreshStatusBoard(),
]);
} catch (error: unknown) {
logger.error('Error creating room', error);
toast.error(getUserFriendlyError(error) || 'Failed to create room');
throw error;
}
}, [refreshRooms, refreshStatusBoard]);
// Set room filters
const setRoomFilters = useCallback((filters: { search?: string; status?: string; type?: string }) => {
setRoomFiltersState(prev => ({ ...prev, ...filters }));
setRoomPageState(1); // Reset to first page when filters change
}, []);
// Set room page
const setRoomPage = useCallback((page: number) => {
setRoomPageState(page);
}, []);
// Set status board floor
const setStatusBoardFloor = useCallback((floor: number | null) => {
setStatusBoardFloorState(floor);
}, []);
// Initial load - only fetch rooms, status board will be fetched when needed
useEffect(() => {
refreshRooms();
// Don't fetch status board on initial load - it requires admin/staff role
// It will be fetched when the component that needs it mounts
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // refreshRooms is stable (useCallback with empty deps), so this is safe
// Initial load and periodic refresh handled by auto-refresh interval
// Auto-refresh status board when floor changes (only if component using it is mounted)
// This will be called by AdvancedRoomManagementPage when floor filter changes
// Set up auto-refresh interval (every 60 seconds)
// Pause when tab is inactive to save resources
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
const startRefresh = () => {
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
if (!document.hidden) {
refreshRooms();
}
}, 60000); // 60 seconds
};
const handleVisibilityChange = () => {
if (document.hidden) {
// Pause refresh when tab is hidden
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
} else {
// Resume refresh when tab becomes visible
startRefresh();
// Immediate refresh when tab becomes visible
refreshRooms();
}
};
startRefresh();
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
if (intervalId) {
clearInterval(intervalId);
}
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [refreshRooms]);
// Cleanup on unmount
useEffect(() => {
const autoRefreshInterval = autoRefreshIntervalRef.current;
return () => {
if (roomsAbortRef.current) {
roomsAbortRef.current.abort();
}
if (statusBoardAbortRef.current) {
statusBoardAbortRef.current.abort();
}
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
};
}, []);
const value: RoomContextType = useMemo(() => ({
rooms,
roomsLoading,
roomsError,
statusBoardRooms,
statusBoardLoading,
statusBoardError,
refreshRooms,
refreshStatusBoard,
updateRoom,
deleteRoom,
createRoom,
setRoomFilters,
roomFilters,
setRoomPage,
roomPage,
setStatusBoardFloor,
statusBoardFloor,
lastUpdate,
}), [
rooms,
roomsLoading,
roomsError,
statusBoardRooms,
statusBoardLoading,
statusBoardError,
refreshRooms,
refreshStatusBoard,
updateRoom,
deleteRoom,
createRoom,
setRoomFilters,
roomFilters,
setRoomPage,
roomPage,
setStatusBoardFloor,
statusBoardFloor,
lastUpdate,
]);
return <RoomContext.Provider value={value}>{children}</RoomContext.Provider>;
};