This commit is contained in:
Iliyan Angelov
2025-12-02 10:42:35 +02:00
parent 4b053ce703
commit 2d770dd27b
24 changed files with 1766 additions and 1402 deletions

View File

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