-
-
- Basic Information
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setRoomFormData({ ...roomFormData, featured: e.target.checked })}
- className="w-5 h-5 text-[#d4af37] bg-[#1a1a1a] border-[#d4af37]/30 rounded focus:ring-[#d4af37]/50 focus:ring-2 cursor-pointer transition-all"
- />
-
-
-
-
-
- setRoomFormData({ ...roomFormData, price: e.target.value })}
- className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
- placeholder="e.g., 150.00"
- />
-
-
-
-
-
-
-
-
-
-
- setRoomFormData({ ...roomFormData, view: e.target.value })}
- className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
- placeholder="e.g., City View, Ocean View, Mountain View"
- />
-
-
-
-
-
-
- Amenities & Features
-
-
-
- {availableAmenities.length === 0 ? (
-
Loading amenities...
- ) : (
-
- {availableAmenities.map((amenity) => {
- const isSelected = roomFormData.amenities.includes(amenity);
- return (
-
- );
- })}
-
- )}
-
- {roomFormData.amenities.length > 0 && (
-
- {roomFormData.amenities.length} amenit{roomFormData.amenities.length === 1 ? 'y' : 'ies'} selected
-
+
+
+
+
+
+
+
+
+
+
+
+ setRoomFormData({ ...roomFormData, featured: e.target.checked })}
+ className="w-5 h-5 text-[#d4af37] bg-[#1a1a1a] border-[#d4af37]/30 rounded focus:ring-[#d4af37]/50 focus:ring-2 cursor-pointer transition-all"
+ />
+
+
+
+
+
+ setRoomFormData({ ...roomFormData, price: e.target.value })}
+ className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
+ placeholder="e.g., 150.00"
+ />
+
+
+
+
+
+
+
+
+
+ setRoomFormData({ ...roomFormData, capacity: e.target.value })}
+ className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
+ placeholder="e.g., 4"
+ />
-
-
-
+
+
+ setRoomFormData({ ...roomFormData, room_size: e.target.value })}
+ className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
+ placeholder="e.g., 1 Room, 50 sqm"
+ />
-
+
+
+
+
+ setRoomFormData({ ...roomFormData, view: e.target.value })}
+ className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white placeholder-gray-500 focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
+ placeholder="e.g., City View, Ocean View, Mountain View"
+ />
+
+
- {editingRoom && (
-
-
-
-
- Room Images
-
-
- {(() => {
+
+
+
+ Amenities & Features
+
+
+
+ {availableAmenities.length === 0 ? (
+
Loading amenities...
+ ) : (
+
+ {availableAmenities.map((amenity) => {
+ const isSelected = roomFormData.amenities.includes(amenity);
+ return (
+
+ );
+ })}
+
+ )}
+
+ {roomFormData.amenities.length > 0 && (
+
+ {roomFormData.amenities.length} amenit{roomFormData.amenities.length === 1 ? 'y' : 'ies'} selected
+
+ )}
+
+
+
+
+
+
+
+
+ {editingRoom && (
+
+
+
+
+ Room Images
+
+
+ {(() => {
const apiBaseUrl = import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000';
const normalizeImageUrl = (img: string): string => {
@@ -1408,64 +1275,62 @@ const AdvancedRoomManagementPage: React.FC = () => {
)}
>
);
- })()}
+ })()}
-
-
-
- {selectedFiles.length > 0 && (
-
-
- {selectedFiles.length} file(s) selected
-
-
-
- )}
+
+
+
- )}
-
-
-
+ {selectedFiles.length > 0 && (
+
+
+ {selectedFiles.length} file(s) selected
+
+
+
+ )}
+ )}
+
+
+
- )}
+
)}
diff --git a/Frontend/src/pages/admin/RoomManagementPage.tsx b/Frontend/src/pages/admin/RoomManagementPage.tsx
index 2ec8b330..cb66c91c 100644
--- a/Frontend/src/pages/admin/RoomManagementPage.tsx
+++ b/Frontend/src/pages/admin/RoomManagementPage.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useEffect, useState } from 'react';
import { Plus, Search, Edit, Trash2, X, Upload, Image as ImageIcon, Check } from 'lucide-react';
import roomService, { Room } from '../../features/rooms/services/roomService';
import { toast } from 'react-toastify';
@@ -7,9 +7,22 @@ import Pagination from '../../shared/components/Pagination';
import apiClient from '../../shared/services/apiClient';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { logger } from '../../shared/utils/logger';
+import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
const RoomManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
+ const {
+ rooms: contextRooms,
+ roomsLoading,
+ refreshRooms,
+ updateRoom: contextUpdateRoom,
+ deleteRoom: contextDeleteRoom,
+ createRoom: contextCreateRoom,
+ setRoomFilters,
+ setRoomPage,
+ } = useRoomContext();
+
+ // Use context rooms, but filter/paginate locally for this page's display
const [rooms, setRooms] = useState
([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
@@ -43,7 +56,45 @@ const RoomManagementPage: React.FC = () => {
const [roomTypes, setRoomTypes] = useState>([]);
const [uploadingImages, setUploadingImages] = useState(false);
const [selectedFiles, setSelectedFiles] = useState([]);
- const abortControllerRef = useRef(null);
+
+ // Sync local filters with context
+ useEffect(() => {
+ setRoomFilters(filters);
+ }, [filters, setRoomFilters]);
+
+ // Sync local page with context
+ useEffect(() => {
+ setRoomPage(currentPage);
+ }, [currentPage, setRoomPage]);
+
+ // Update local rooms from context and apply local pagination
+ useEffect(() => {
+ if (contextRooms.length > 0) {
+ // Apply local filters
+ let filteredRooms = contextRooms.filter((room) => {
+ const matchesSearch = !filters.search ||
+ room.room_number.toLowerCase().includes(filters.search.toLowerCase()) ||
+ room.room_type?.name.toLowerCase().includes(filters.search.toLowerCase());
+ const matchesStatus = !filters.status || room.status === filters.status;
+ const matchesType = !filters.type || room.room_type?.name === filters.type;
+ return matchesSearch && matchesStatus && matchesType;
+ });
+
+ // Apply pagination
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ const paginatedRooms = filteredRooms.slice(startIndex, endIndex);
+
+ setRooms(paginatedRooms);
+ setTotalPages(Math.ceil(filteredRooms.length / itemsPerPage));
+ setTotalItems(filteredRooms.length);
+ setLoading(false);
+ } else if (!roomsLoading) {
+ setLoading(false);
+ } else {
+ setLoading(roomsLoading);
+ }
+ }, [contextRooms, filters, currentPage, itemsPerPage, roomsLoading]);
useEffect(() => {
setCurrentPage(1);
@@ -51,24 +102,8 @@ const RoomManagementPage: React.FC = () => {
}, [filters]);
useEffect(() => {
- // Cancel previous request if exists
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- }
-
- // Create new abort controller
- abortControllerRef.current = new AbortController();
-
- fetchRooms();
fetchAvailableAmenities();
-
- // Cleanup: abort request on unmount
- return () => {
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- }
- };
- }, [filters, currentPage]);
+ }, []);
useEffect(() => {
@@ -132,23 +167,11 @@ const RoomManagementPage: React.FC = () => {
}
};
- const fetchRooms = async () => {
- try {
- setLoading(true);
- const response = await roomService.getRooms({
- ...filters,
- page: currentPage,
- limit: itemsPerPage,
- });
- setRooms(response.data.rooms);
- if (response.data.pagination) {
- setTotalPages(response.data.pagination.totalPages);
- setTotalItems(response.data.pagination.total);
- }
-
-
+ // Extract unique room types from context rooms
+ useEffect(() => {
+ if (contextRooms.length > 0) {
const uniqueRoomTypes = new Map();
- response.data.rooms.forEach((room: Room) => {
+ contextRooms.forEach((room: Room) => {
if (room.room_type && !uniqueRoomTypes.has(room.room_type.id)) {
uniqueRoomTypes.set(room.room_type.id, {
id: room.room_type.id,
@@ -157,60 +180,8 @@ const RoomManagementPage: React.FC = () => {
}
});
setRoomTypes(Array.from(uniqueRoomTypes.values()));
-
-
- if (roomTypes.length === 0 && response.data.pagination) {
- try {
-
- const allRoomsResponse = await roomService.getRooms({ limit: 100, page: 1 });
- const allUniqueRoomTypes = new Map();
- allRoomsResponse.data.rooms.forEach((room: Room) => {
- if (room.room_type && !allUniqueRoomTypes.has(room.room_type.id)) {
- allUniqueRoomTypes.set(room.room_type.id, {
- id: room.room_type.id,
- name: room.room_type.name,
- });
- }
- });
-
-
- if (allRoomsResponse.data.pagination && allRoomsResponse.data.pagination.totalPages > 1) {
- const totalPages = allRoomsResponse.data.pagination.totalPages;
- for (let page = 2; page <= Math.min(totalPages, 10); page++) {
- try {
- const pageResponse = await roomService.getRooms({ limit: 100, page });
- pageResponse.data.rooms.forEach((room: Room) => {
- if (room.room_type && !allUniqueRoomTypes.has(room.room_type.id)) {
- allUniqueRoomTypes.set(room.room_type.id, {
- id: room.room_type.id,
- name: room.room_type.name,
- });
- }
- });
- } catch (err) {
-
- }
- }
- }
-
- if (allUniqueRoomTypes.size > 0) {
- setRoomTypes(Array.from(allUniqueRoomTypes.values()));
- }
- } catch (err) {
-
- }
- }
- } catch (error: any) {
- // Handle AbortError silently
- if (error.name === 'AbortError') {
- return;
- }
- logger.error('Error fetching rooms', error);
- toast.error(error.response?.data?.message || 'Unable to load rooms list');
- } finally {
- setLoading(false);
}
- };
+ }, [contextRooms]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -226,13 +197,9 @@ const RoomManagementPage: React.FC = () => {
view: formData.view || undefined,
amenities: Array.isArray(formData.amenities) ? formData.amenities : [],
};
- await roomService.updateRoom(editingRoom.id, updateData);
- toast.success('Room updated successfully');
-
-
- await fetchRooms();
-
+ await contextUpdateRoom(editingRoom.id, updateData);
+ // Refresh room details for editing
try {
const updatedRoom = await roomService.getRoomByNumber(editingRoom.room_number);
setEditingRoom(updatedRoom.data.room);
@@ -251,8 +218,6 @@ const RoomManagementPage: React.FC = () => {
amenities: Array.isArray(formData.amenities) ? formData.amenities : [],
};
const response = await roomService.createRoom(createData);
- toast.success('Room added successfully');
-
if (response.data?.room) {
@@ -307,7 +272,7 @@ const RoomManagementPage: React.FC = () => {
});
- await fetchRooms();
+ await contextCreateRoom(createData);
return;
}
@@ -315,7 +280,7 @@ const RoomManagementPage: React.FC = () => {
setShowModal(false);
resetForm();
- fetchRooms();
+ await refreshRooms();
} catch (error: any) {
toast.error(error.response?.data?.message || 'An error occurred');
}
@@ -406,12 +371,10 @@ const RoomManagementPage: React.FC = () => {
if (!window.confirm('Are you sure you want to delete this room?')) return;
try {
- await roomService.deleteRoom(id);
- toast.success('Room deleted successfully');
+ await contextDeleteRoom(id);
setSelectedRooms(selectedRooms.filter(roomId => roomId !== id));
- fetchRooms();
} catch (error: any) {
- toast.error(error.response?.data?.message || 'Unable to delete room');
+ // Error already handled in context
}
};
@@ -427,7 +390,7 @@ const RoomManagementPage: React.FC = () => {
await roomService.bulkDeleteRooms(selectedRooms);
toast.success(`Successfully deleted ${selectedRooms.length} room(s)`);
setSelectedRooms([]);
- fetchRooms();
+ await refreshRooms();
} catch (error: any) {
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete rooms');
}
@@ -502,7 +465,7 @@ const RoomManagementPage: React.FC = () => {
toast.success('Images uploaded successfully');
setSelectedFiles([]);
- fetchRooms();
+ await refreshRooms();
const response = await roomService.getRoomByNumber(editingRoom.room_number);
@@ -540,7 +503,7 @@ const RoomManagementPage: React.FC = () => {
});
toast.success('Image deleted successfully');
- fetchRooms();
+ await refreshRooms();
const response = await roomService.getRoomByNumber(editingRoom.room_number);
diff --git a/Frontend/src/pages/admin/UserManagementPage.tsx b/Frontend/src/pages/admin/UserManagementPage.tsx
index f2fe143f..def177ad 100644
--- a/Frontend/src/pages/admin/UserManagementPage.tsx
+++ b/Frontend/src/pages/admin/UserManagementPage.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import { Plus, Search, Edit, Trash2, X } from 'lucide-react';
import userService, { User } from '../../features/auth/services/userService';
import { toast } from 'react-toastify';
@@ -199,6 +199,18 @@ const UserManagementPage: React.FC = () => {
label: 'Staff',
border: 'border-blue-200'
},
+ accountant: {
+ bg: 'bg-gradient-to-r from-purple-50 to-violet-50',
+ text: 'text-purple-800',
+ label: 'Accountant',
+ border: 'border-purple-200'
+ },
+ housekeeping: {
+ bg: 'bg-gradient-to-r from-amber-50 to-orange-50',
+ text: 'text-amber-800',
+ label: 'Housekeeping',
+ border: 'border-amber-200'
+ },
customer: {
bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
text: 'text-emerald-800',
@@ -264,6 +276,8 @@ const UserManagementPage: React.FC = () => {
+
+
diff --git a/Frontend/src/pages/housekeeping/DashboardPage.tsx b/Frontend/src/pages/housekeeping/DashboardPage.tsx
new file mode 100644
index 00000000..f98d5a97
--- /dev/null
+++ b/Frontend/src/pages/housekeeping/DashboardPage.tsx
@@ -0,0 +1,505 @@
+import React, { useEffect, useState, useRef } from 'react';
+import {
+ Sparkles,
+ Clock,
+ CheckCircle,
+ AlertCircle,
+ RefreshCw,
+ Calendar,
+ MapPin,
+ Play,
+ ChevronDown,
+ ChevronUp,
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import { formatDate } from '../../shared/utils/format';
+import advancedRoomService, { HousekeepingTask, ChecklistItem } from '../../features/rooms/services/advancedRoomService';
+import { logger } from '../../shared/utils/logger';
+import useAuthStore from '../../store/useAuthStore';
+
+const HousekeepingDashboardPage: React.FC = () => {
+ const { userInfo } = useAuthStore();
+ const [loading, setLoading] = useState(true);
+ const [tasks, setTasks] = useState