updates
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Hotel,
|
||||
Wrench,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
MapPin,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
X,
|
||||
Image as ImageIcon,
|
||||
Check,
|
||||
@@ -35,6 +37,7 @@ import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
|
||||
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections';
|
||||
|
||||
const AdvancedRoomManagementPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
statusBoardRooms,
|
||||
statusBoardLoading,
|
||||
@@ -73,6 +76,8 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
const [roomTypes, setRoomTypes] = useState<Array<{ id: number; name: string }>>([]);
|
||||
const [uploadingImages, setUploadingImages] = useState(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [customAmenityInput, setCustomAmenityInput] = useState('');
|
||||
const [editingAmenity, setEditingAmenity] = useState<{ name: string; newName: string } | null>(null);
|
||||
|
||||
// Define fetchFloors before using it in useEffect
|
||||
const fetchFloors = useCallback(async () => {
|
||||
@@ -451,87 +456,31 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditRoom = async (room: Room) => {
|
||||
setEditingRoom(room);
|
||||
|
||||
let amenitiesArray: string[] = [];
|
||||
if (room.amenities) {
|
||||
if (Array.isArray(room.amenities)) {
|
||||
amenitiesArray = room.amenities;
|
||||
} else if (typeof room.amenities === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(room.amenities);
|
||||
amenitiesArray = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
const amenitiesStr: string = room.amenities;
|
||||
amenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRoomFormData({
|
||||
room_number: room.room_number,
|
||||
floor: room.floor,
|
||||
room_type_id: room.room_type_id,
|
||||
status: room.status,
|
||||
featured: room.featured,
|
||||
price: room.price?.toString() || '',
|
||||
description: room.description || '',
|
||||
capacity: room.capacity?.toString() || '',
|
||||
room_size: room.room_size || '',
|
||||
view: room.view || '',
|
||||
amenities: amenitiesArray,
|
||||
});
|
||||
|
||||
setShowRoomModal(true);
|
||||
|
||||
try {
|
||||
const fullRoom = await roomService.getRoomByNumber(room.room_number);
|
||||
const roomData = fullRoom.data.room;
|
||||
|
||||
let updatedAmenitiesArray: string[] = [];
|
||||
if (roomData.amenities) {
|
||||
if (Array.isArray(roomData.amenities)) {
|
||||
updatedAmenitiesArray = roomData.amenities;
|
||||
} else if (typeof roomData.amenities === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(roomData.amenities);
|
||||
updatedAmenitiesArray = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
const amenitiesStr: string = roomData.amenities;
|
||||
updatedAmenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRoomFormData({
|
||||
room_number: roomData.room_number,
|
||||
floor: roomData.floor,
|
||||
room_type_id: roomData.room_type_id,
|
||||
status: roomData.status,
|
||||
featured: roomData.featured,
|
||||
price: roomData.price?.toString() || '',
|
||||
description: roomData.description || '',
|
||||
capacity: roomData.capacity?.toString() || '',
|
||||
room_size: roomData.room_size || '',
|
||||
view: roomData.view || '',
|
||||
amenities: updatedAmenitiesArray,
|
||||
});
|
||||
|
||||
setEditingRoom(roomData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch full room details', error);
|
||||
toast.error('Failed to load complete room details');
|
||||
}
|
||||
const handleEditRoom = (room: Room) => {
|
||||
navigate(`/admin/rooms/${room.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDeleteRoom = async (id: number) => {
|
||||
if (!window.confirm('Are you sure you want to delete this room?')) return;
|
||||
const room = contextRooms.find(r => r.id === id) || statusBoardRooms.find(r => r.id === id);
|
||||
const roomNumber = room?.room_number || 'this room';
|
||||
|
||||
if (!window.confirm(`Are you sure you want to delete room ${roomNumber}? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await contextDeleteRoom(id);
|
||||
toast.success(`Room ${roomNumber} deleted successfully`);
|
||||
await refreshStatusBoard();
|
||||
await refreshRooms();
|
||||
// Remove from expanded rooms if it was expanded
|
||||
setExpandedRooms(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Error already handled in context
|
||||
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to delete room');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -552,6 +501,8 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
});
|
||||
setSelectedFiles([]);
|
||||
setUploadingImages(false);
|
||||
setCustomAmenityInput('');
|
||||
setEditingAmenity(null);
|
||||
};
|
||||
|
||||
const toggleAmenity = (amenity: string) => {
|
||||
@@ -563,6 +514,86 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddCustomAmenity = () => {
|
||||
const trimmed = customAmenityInput.trim();
|
||||
if (trimmed && !roomFormData.amenities.includes(trimmed)) {
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: [...prev.amenities, trimmed]
|
||||
}));
|
||||
setCustomAmenityInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAmenity = (amenity: string) => {
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.filter(a => a !== amenity)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleEditAmenity = (amenity: string) => {
|
||||
setEditingAmenity({ name: amenity, newName: amenity });
|
||||
};
|
||||
|
||||
const handleSaveAmenityEdit = async () => {
|
||||
if (!editingAmenity || editingAmenity.name === editingAmenity.newName.trim()) {
|
||||
setEditingAmenity(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newName = editingAmenity.newName.trim();
|
||||
if (!newName) {
|
||||
toast.error('Amenity name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await roomService.updateAmenity(editingAmenity.name, newName);
|
||||
toast.success(`Amenity "${editingAmenity.name}" updated to "${newName}"`);
|
||||
|
||||
setAvailableAmenities(prev => {
|
||||
const updated = prev.map(a => a === editingAmenity.name ? newName : a);
|
||||
return updated.sort();
|
||||
});
|
||||
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.map(a => a === editingAmenity.name ? newName : a)
|
||||
}));
|
||||
|
||||
setEditingAmenity(null);
|
||||
await fetchAvailableAmenities();
|
||||
await refreshRooms();
|
||||
await refreshStatusBoard();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to update amenity');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAmenity = async (amenity: string) => {
|
||||
if (!window.confirm(`Are you sure you want to delete "${amenity}"? This will remove it from all rooms and room types.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await roomService.deleteAmenity(amenity);
|
||||
toast.success(`Amenity "${amenity}" deleted successfully`);
|
||||
|
||||
setAvailableAmenities(prev => prev.filter(a => a !== amenity));
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.filter(a => a !== amenity)
|
||||
}));
|
||||
|
||||
await fetchAvailableAmenities();
|
||||
await refreshRooms();
|
||||
await refreshStatusBoard();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to delete amenity');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const files = Array.from(e.target.files);
|
||||
@@ -806,17 +837,29 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
<span>{getStatusLabel(effectiveStatus)}</span>
|
||||
</div>
|
||||
|
||||
{/* Edit Button */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditRoomFromStatusBoard(room.id);
|
||||
}}
|
||||
className="absolute top-3 left-3 p-2 bg-white/90 backdrop-blur-sm rounded-lg text-blue-600 hover:text-blue-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-blue-200 hover:border-blue-300 z-20"
|
||||
title="Edit Room"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
{/* Action Buttons */}
|
||||
<div className="absolute top-3 left-3 flex gap-2 z-20">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditRoomFromStatusBoard(room.id);
|
||||
}}
|
||||
className="p-2 bg-white/90 backdrop-blur-sm rounded-lg text-blue-600 hover:text-blue-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-blue-200 hover:border-blue-300"
|
||||
title="Edit Room"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteRoom(room.id);
|
||||
}}
|
||||
className="p-2 bg-white/90 backdrop-blur-sm rounded-lg text-rose-600 hover:text-rose-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-rose-200 hover:border-rose-300 hover:scale-105"
|
||||
title={`Delete Room ${room.room_number}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Room Content */}
|
||||
<div className="p-5 pt-16 cursor-pointer" onClick={() => toggleRoomExpansion(room.id)}>
|
||||
|
||||
Reference in New Issue
Block a user