2962 lines
153 KiB
TypeScript
2962 lines
153 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
LogIn,
|
|
LogOut,
|
|
Search,
|
|
User,
|
|
Hotel,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
FileText,
|
|
CreditCard,
|
|
Printer,
|
|
Sparkles,
|
|
ChevronRight,
|
|
Eye,
|
|
XCircle,
|
|
Loader2,
|
|
Plus,
|
|
Edit,
|
|
Trash2,
|
|
X,
|
|
Upload,
|
|
Image as ImageIcon,
|
|
Check,
|
|
Calendar,
|
|
Wrench
|
|
} from 'lucide-react';
|
|
import { bookingService, Booking, roomService, Room, serviceService, Service } from '../../services/api';
|
|
import { toast } from 'react-toastify';
|
|
import Loading from '../../components/common/Loading';
|
|
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
|
import Pagination from '../../components/common/Pagination';
|
|
import apiClient from '../../services/api/apiClient';
|
|
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
|
import { parseDateLocal } from '../../utils/format';
|
|
|
|
type ReceptionTab = 'overview' | 'check-in' | 'check-out' | 'bookings' | 'rooms' | 'services';
|
|
|
|
interface GuestInfo {
|
|
name: string;
|
|
id_number: string;
|
|
phone: string;
|
|
}
|
|
|
|
interface ServiceItem {
|
|
service_name: string;
|
|
quantity: number;
|
|
price: number;
|
|
total: number;
|
|
}
|
|
|
|
const ReceptionDashboardPage: React.FC = () => {
|
|
const { formatCurrency } = useFormatCurrency();
|
|
const [activeTab, setActiveTab] = useState<ReceptionTab>('overview');
|
|
|
|
// Check-in State
|
|
const [checkInBookingNumber, setCheckInBookingNumber] = useState('');
|
|
const [checkInBooking, setCheckInBooking] = useState<Booking | null>(null);
|
|
const [checkInLoading, setCheckInLoading] = useState(false);
|
|
const [checkInSearching, setCheckInSearching] = useState(false);
|
|
const [actualRoomNumber, setActualRoomNumber] = useState('');
|
|
const [guests, setGuests] = useState<GuestInfo[]>([{ name: '', id_number: '', phone: '' }]);
|
|
const [extraPersons, setExtraPersons] = useState(0);
|
|
const [children, setChildren] = useState(0);
|
|
const [additionalFee, setAdditionalFee] = useState(0);
|
|
|
|
// Check-out State
|
|
const [checkOutBookingNumber, setCheckOutBookingNumber] = useState('');
|
|
const [checkOutBooking, setCheckOutBooking] = useState<Booking | null>(null);
|
|
const [checkOutLoading, setCheckOutLoading] = useState(false);
|
|
const [checkOutSearching, setCheckOutSearching] = useState(false);
|
|
const [checkOutServices, setCheckOutServices] = useState<ServiceItem[]>([]);
|
|
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'stripe'>('cash');
|
|
const [discount, setDiscount] = useState(0);
|
|
const [showInvoice, setShowInvoice] = useState(false);
|
|
|
|
// Bookings Management State
|
|
const [bookings, setBookings] = useState<Booking[]>([]);
|
|
const [bookingsLoading, setBookingsLoading] = useState(true);
|
|
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
|
|
const [showBookingDetailModal, setShowBookingDetailModal] = useState(false);
|
|
const [updatingBookingId, setUpdatingBookingId] = useState<number | null>(null);
|
|
const [cancellingBookingId, setCancellingBookingId] = useState<number | null>(null);
|
|
const [bookingFilters, setBookingFilters] = useState({
|
|
search: '',
|
|
status: '',
|
|
});
|
|
const [bookingCurrentPage, setBookingCurrentPage] = useState(1);
|
|
const [bookingTotalPages, setBookingTotalPages] = useState(1);
|
|
const [bookingTotalItems, setBookingTotalItems] = useState(0);
|
|
const bookingItemsPerPage = 5;
|
|
|
|
// Rooms Management State
|
|
const [rooms, setRooms] = useState<Room[]>([]);
|
|
const [roomsLoading, setRoomsLoading] = useState(true);
|
|
const [showRoomModal, setShowRoomModal] = useState(false);
|
|
const [editingRoom, setEditingRoom] = useState<Room | null>(null);
|
|
const [selectedRooms, setSelectedRooms] = useState<number[]>([]);
|
|
const [roomFilters, setRoomFilters] = useState({
|
|
search: '',
|
|
status: '',
|
|
type: '',
|
|
});
|
|
const [roomCurrentPage, setRoomCurrentPage] = useState(1);
|
|
const [roomTotalPages, setRoomTotalPages] = useState(1);
|
|
const [roomTotalItems, setRoomTotalItems] = useState(0);
|
|
const roomItemsPerPage = 5;
|
|
const [roomFormData, setRoomFormData] = useState({
|
|
room_number: '',
|
|
floor: 1,
|
|
room_type_id: 1,
|
|
status: 'available' as 'available' | 'occupied' | 'maintenance',
|
|
featured: false,
|
|
price: '',
|
|
description: '',
|
|
capacity: '',
|
|
room_size: '',
|
|
view: '',
|
|
amenities: [] as string[],
|
|
});
|
|
const [availableAmenities, setAvailableAmenities] = useState<string[]>([]);
|
|
const [roomTypes, setRoomTypes] = useState<Array<{ id: number; name: string }>>([]);
|
|
const [uploadingImages, setUploadingImages] = useState(false);
|
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
|
|
// Services Management State
|
|
const [services, setServices] = useState<Service[]>([]);
|
|
const [servicesLoading, setServicesLoading] = useState(true);
|
|
const [showServiceModal, setShowServiceModal] = useState(false);
|
|
const [editingService, setEditingService] = useState<Service | null>(null);
|
|
const [serviceFilters, setServiceFilters] = useState({
|
|
search: '',
|
|
status: '',
|
|
});
|
|
const [serviceCurrentPage, setServiceCurrentPage] = useState(1);
|
|
const [serviceTotalPages, setServiceTotalPages] = useState(1);
|
|
const [serviceTotalItems, setServiceTotalItems] = useState(0);
|
|
const serviceItemsPerPage = 5;
|
|
const [serviceFormData, setServiceFormData] = useState({
|
|
name: '',
|
|
description: '',
|
|
price: 0,
|
|
unit: 'time',
|
|
status: 'active' as 'active' | 'inactive',
|
|
});
|
|
|
|
// Check-in Functions
|
|
const handleCheckInSearch = async () => {
|
|
if (!checkInBookingNumber.trim()) {
|
|
toast.error('Please enter booking number');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setCheckInSearching(true);
|
|
const response = await bookingService.checkBookingByNumber(checkInBookingNumber);
|
|
setCheckInBooking(response.data.booking);
|
|
setActualRoomNumber(response.data.booking.room?.room_number || '');
|
|
toast.success('Booking found');
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Booking not found');
|
|
setCheckInBooking(null);
|
|
} finally {
|
|
setCheckInSearching(false);
|
|
}
|
|
};
|
|
|
|
const handleAddGuest = () => {
|
|
setGuests([...guests, { name: '', id_number: '', phone: '' }]);
|
|
};
|
|
|
|
const handleRemoveGuest = (index: number) => {
|
|
if (guests.length > 1) {
|
|
setGuests(guests.filter((_, i) => i !== index));
|
|
}
|
|
};
|
|
|
|
const handleGuestChange = (index: number, field: keyof GuestInfo, value: string) => {
|
|
const newGuests = [...guests];
|
|
newGuests[index][field] = value;
|
|
setGuests(newGuests);
|
|
};
|
|
|
|
const calculateCheckInAdditionalFee = () => {
|
|
const extraPersonFee = extraPersons * 200000;
|
|
const childrenFee = children * 100000;
|
|
const total = extraPersonFee + childrenFee;
|
|
setAdditionalFee(total);
|
|
return total;
|
|
};
|
|
|
|
const handleCheckIn = async () => {
|
|
if (!checkInBooking) return;
|
|
|
|
if (!actualRoomNumber.trim()) {
|
|
toast.error('Please enter actual room number');
|
|
return;
|
|
}
|
|
|
|
const mainGuest = guests[0];
|
|
if (!mainGuest.name || !mainGuest.id_number || !mainGuest.phone) {
|
|
toast.error('Please fill in all main guest information');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setCheckInLoading(true);
|
|
calculateCheckInAdditionalFee();
|
|
|
|
await bookingService.updateBooking(checkInBooking.id, {
|
|
status: 'checked_in',
|
|
} as any);
|
|
|
|
toast.success('Check-in successful');
|
|
|
|
setCheckInBooking(null);
|
|
setCheckInBookingNumber('');
|
|
setActualRoomNumber('');
|
|
setGuests([{ name: '', id_number: '', phone: '' }]);
|
|
setExtraPersons(0);
|
|
setChildren(0);
|
|
setAdditionalFee(0);
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'An error occurred during check-in');
|
|
} finally {
|
|
setCheckInLoading(false);
|
|
}
|
|
};
|
|
|
|
// Check-out Functions
|
|
const handleCheckOutSearch = async () => {
|
|
if (!checkOutBookingNumber.trim()) {
|
|
toast.error('Please enter booking number');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setCheckOutSearching(true);
|
|
const response = await bookingService.checkBookingByNumber(checkOutBookingNumber);
|
|
const foundBooking = response.data.booking;
|
|
|
|
if (foundBooking.status !== 'checked_in') {
|
|
toast.warning('Only checked-in bookings can be checked out');
|
|
}
|
|
|
|
setCheckOutBooking(foundBooking);
|
|
|
|
setCheckOutServices([
|
|
{ service_name: 'Laundry', quantity: 2, price: 50000, total: 100000 },
|
|
{ service_name: 'Minibar', quantity: 1, price: 150000, total: 150000 },
|
|
]);
|
|
|
|
toast.success('Booking found');
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Booking not found');
|
|
setCheckOutBooking(null);
|
|
} finally {
|
|
setCheckOutSearching(false);
|
|
}
|
|
};
|
|
|
|
const calculateRoomFee = () => {
|
|
if (!checkOutBooking) return 0;
|
|
return checkOutBooking.total_price || 0;
|
|
};
|
|
|
|
const calculateServiceFee = () => {
|
|
return checkOutServices.reduce((sum, service) => sum + service.total, 0);
|
|
};
|
|
|
|
const calculateCheckOutAdditionalFee = () => {
|
|
return 0;
|
|
};
|
|
|
|
const calculateDeposit = () => {
|
|
return checkOutBooking?.total_price ? checkOutBooking.total_price * 0.3 : 0;
|
|
};
|
|
|
|
const calculateSubtotal = () => {
|
|
return calculateRoomFee() + calculateServiceFee() + calculateCheckOutAdditionalFee();
|
|
};
|
|
|
|
const calculateTotal = () => {
|
|
return calculateSubtotal() - discount;
|
|
};
|
|
|
|
const calculateRemaining = () => {
|
|
return calculateTotal() - calculateDeposit();
|
|
};
|
|
|
|
const handleCheckOut = async () => {
|
|
if (!checkOutBooking) return;
|
|
|
|
if (calculateRemaining() < 0) {
|
|
toast.error('Invalid refund amount');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setCheckOutLoading(true);
|
|
|
|
await bookingService.updateBooking(checkOutBooking.id, {
|
|
status: 'checked_out',
|
|
} as any);
|
|
|
|
toast.success('Check-out successful');
|
|
setShowInvoice(true);
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'An error occurred during check-out');
|
|
} finally {
|
|
setCheckOutLoading(false);
|
|
}
|
|
};
|
|
|
|
const handlePrintInvoice = () => {
|
|
window.print();
|
|
};
|
|
|
|
const resetCheckOutForm = () => {
|
|
setCheckOutBooking(null);
|
|
setCheckOutBookingNumber('');
|
|
setCheckOutServices([]);
|
|
setDiscount(0);
|
|
setPaymentMethod('cash');
|
|
setShowInvoice(false);
|
|
};
|
|
|
|
// Bookings Management Functions
|
|
useEffect(() => {
|
|
setBookingCurrentPage(1);
|
|
}, [bookingFilters]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'bookings') {
|
|
fetchBookings();
|
|
}
|
|
}, [bookingFilters, bookingCurrentPage, activeTab]);
|
|
|
|
const fetchBookings = async () => {
|
|
try {
|
|
setBookingsLoading(true);
|
|
const response = await bookingService.getAllBookings({
|
|
...bookingFilters,
|
|
page: bookingCurrentPage,
|
|
limit: bookingItemsPerPage,
|
|
});
|
|
setBookings(response.data.bookings);
|
|
if (response.data.pagination) {
|
|
setBookingTotalPages(response.data.pagination.totalPages);
|
|
setBookingTotalItems(response.data.pagination.total);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to load bookings list');
|
|
} finally {
|
|
setBookingsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleUpdateBookingStatus = async (id: number, status: string) => {
|
|
try {
|
|
setUpdatingBookingId(id);
|
|
await bookingService.updateBooking(id, { status } as any);
|
|
toast.success('Status updated successfully');
|
|
await fetchBookings();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to update status');
|
|
} finally {
|
|
setUpdatingBookingId(null);
|
|
}
|
|
};
|
|
|
|
const handleCancelBooking = async (id: number) => {
|
|
if (!window.confirm('Are you sure you want to cancel this booking?')) return;
|
|
|
|
try {
|
|
setCancellingBookingId(id);
|
|
await bookingService.cancelBooking(id);
|
|
toast.success('Booking cancelled successfully');
|
|
await fetchBookings();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to cancel booking');
|
|
} finally {
|
|
setCancellingBookingId(null);
|
|
}
|
|
};
|
|
|
|
const getBookingStatusBadge = (status: string) => {
|
|
const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
|
|
pending: {
|
|
bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
|
|
text: 'text-amber-800',
|
|
label: 'Pending confirmation',
|
|
border: 'border-amber-200'
|
|
},
|
|
confirmed: {
|
|
bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
|
|
text: 'text-blue-800',
|
|
label: 'Confirmed',
|
|
border: 'border-blue-200'
|
|
},
|
|
checked_in: {
|
|
bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
|
|
text: 'text-emerald-800',
|
|
label: 'Checked in',
|
|
border: 'border-emerald-200'
|
|
},
|
|
checked_out: {
|
|
bg: 'bg-gradient-to-r from-slate-50 to-gray-50',
|
|
text: 'text-slate-700',
|
|
label: 'Checked out',
|
|
border: 'border-slate-200'
|
|
},
|
|
cancelled: {
|
|
bg: 'bg-gradient-to-r from-rose-50 to-red-50',
|
|
text: 'text-rose-800',
|
|
label: 'Cancelled',
|
|
border: 'border-rose-200'
|
|
},
|
|
};
|
|
const badge = badges[status] || badges.pending;
|
|
return (
|
|
<span className={`px-4 py-1.5 rounded-full text-xs font-semibold border ${badge.bg} ${badge.text} ${badge.border} shadow-sm`}>
|
|
{badge.label}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
// Rooms Management Functions
|
|
useEffect(() => {
|
|
setRoomCurrentPage(1);
|
|
setSelectedRooms([]);
|
|
}, [roomFilters]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'rooms') {
|
|
fetchRooms();
|
|
fetchAvailableAmenities();
|
|
}
|
|
}, [roomFilters, roomCurrentPage, activeTab]);
|
|
|
|
useEffect(() => {
|
|
const fetchAllRoomTypes = async () => {
|
|
try {
|
|
const response = await roomService.getRooms({ limit: 100, page: 1 });
|
|
const allUniqueRoomTypes = new Map<number, { id: number; name: string }>();
|
|
response.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 (response.data.pagination && response.data.pagination.totalPages > 1) {
|
|
const totalPages = response.data.pagination.totalPages;
|
|
for (let page = 2; page <= totalPages; 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) {
|
|
console.error(`Failed to fetch page ${page}:`, err);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allUniqueRoomTypes.size > 0) {
|
|
const roomTypesList = Array.from(allUniqueRoomTypes.values());
|
|
setRoomTypes(roomTypesList);
|
|
if (!editingRoom && roomFormData.room_type_id === 1 && roomTypesList.length > 0) {
|
|
setRoomFormData(prev => ({ ...prev, room_type_id: roomTypesList[0].id }));
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch room types:', err);
|
|
}
|
|
};
|
|
if (activeTab === 'rooms') {
|
|
fetchAllRoomTypes();
|
|
}
|
|
}, [activeTab]);
|
|
|
|
const fetchAvailableAmenities = async () => {
|
|
try {
|
|
const response = await roomService.getAmenities();
|
|
if (response.data?.amenities) {
|
|
setAvailableAmenities(response.data.amenities);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch amenities:', error);
|
|
}
|
|
};
|
|
|
|
const fetchRooms = async () => {
|
|
try {
|
|
setRoomsLoading(true);
|
|
const response = await roomService.getRooms({
|
|
...roomFilters,
|
|
page: roomCurrentPage,
|
|
limit: roomItemsPerPage,
|
|
});
|
|
setRooms(response.data.rooms);
|
|
if (response.data.pagination) {
|
|
setRoomTotalPages(response.data.pagination.totalPages);
|
|
setRoomTotalItems(response.data.pagination.total);
|
|
}
|
|
|
|
const uniqueRoomTypes = new Map<number, { id: number; name: string }>();
|
|
response.data.rooms.forEach((room: Room) => {
|
|
if (room.room_type && !uniqueRoomTypes.has(room.room_type.id)) {
|
|
uniqueRoomTypes.set(room.room_type.id, {
|
|
id: room.room_type.id,
|
|
name: room.room_type.name,
|
|
});
|
|
}
|
|
});
|
|
setRoomTypes(Array.from(uniqueRoomTypes.values()));
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to load rooms list');
|
|
} finally {
|
|
setRoomsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRoomSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
try {
|
|
if (editingRoom) {
|
|
const updateData = {
|
|
...roomFormData,
|
|
price: roomFormData.price ? parseFloat(roomFormData.price) : undefined,
|
|
description: roomFormData.description || undefined,
|
|
capacity: roomFormData.capacity ? parseInt(roomFormData.capacity) : undefined,
|
|
room_size: roomFormData.room_size || undefined,
|
|
view: roomFormData.view || undefined,
|
|
amenities: Array.isArray(roomFormData.amenities) ? roomFormData.amenities : [],
|
|
};
|
|
await roomService.updateRoom(editingRoom.id, updateData);
|
|
toast.success('Room updated successfully');
|
|
await fetchRooms();
|
|
try {
|
|
const updatedRoom = await roomService.getRoomByNumber(editingRoom.room_number);
|
|
setEditingRoom(updatedRoom.data.room);
|
|
} catch (err) {
|
|
console.error('Failed to refresh room data:', err);
|
|
}
|
|
} else {
|
|
const createData = {
|
|
...roomFormData,
|
|
price: roomFormData.price ? parseFloat(roomFormData.price) : undefined,
|
|
description: roomFormData.description || undefined,
|
|
capacity: roomFormData.capacity ? parseInt(roomFormData.capacity) : undefined,
|
|
room_size: roomFormData.room_size || undefined,
|
|
view: roomFormData.view || undefined,
|
|
amenities: Array.isArray(roomFormData.amenities) ? roomFormData.amenities : [],
|
|
};
|
|
const response = await roomService.createRoom(createData);
|
|
toast.success('Room added successfully');
|
|
|
|
if (response.data?.room) {
|
|
if (selectedFiles.length > 0) {
|
|
try {
|
|
setUploadingImages(true);
|
|
const uploadFormData = new FormData();
|
|
selectedFiles.forEach(file => {
|
|
uploadFormData.append('images', file);
|
|
});
|
|
|
|
await apiClient.post(`/rooms/${response.data.room.id}/images`, uploadFormData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
});
|
|
|
|
toast.success('Images uploaded successfully');
|
|
setSelectedFiles([]);
|
|
const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number);
|
|
setEditingRoom(updatedRoom.data.room);
|
|
} catch (uploadError: any) {
|
|
toast.error(uploadError.response?.data?.message || 'Room created but failed to upload images');
|
|
const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number);
|
|
setEditingRoom(updatedRoom.data.room);
|
|
} finally {
|
|
setUploadingImages(false);
|
|
}
|
|
} else {
|
|
const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number);
|
|
setEditingRoom(updatedRoom.data.room);
|
|
}
|
|
|
|
setRoomFormData({
|
|
room_number: response.data.room.room_number,
|
|
floor: response.data.room.floor,
|
|
room_type_id: response.data.room.room_type_id,
|
|
status: response.data.room.status,
|
|
featured: response.data.room.featured,
|
|
price: response.data.room.price?.toString() || '',
|
|
description: response.data.room.description || '',
|
|
capacity: response.data.room.capacity?.toString() || '',
|
|
room_size: response.data.room.room_size || '',
|
|
view: response.data.room.view || '',
|
|
amenities: response.data.room.amenities || [],
|
|
});
|
|
|
|
await fetchRooms();
|
|
return;
|
|
}
|
|
}
|
|
setShowRoomModal(false);
|
|
resetRoomForm();
|
|
fetchRooms();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'An error occurred');
|
|
}
|
|
};
|
|
|
|
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) {
|
|
console.error('Failed to fetch full room details:', error);
|
|
}
|
|
};
|
|
|
|
const handleDeleteRoom = async (id: number) => {
|
|
if (!window.confirm('Are you sure you want to delete this room?')) return;
|
|
|
|
try {
|
|
await roomService.deleteRoom(id);
|
|
toast.success('Room deleted successfully');
|
|
setSelectedRooms(selectedRooms.filter(roomId => roomId !== id));
|
|
fetchRooms();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to delete room');
|
|
}
|
|
};
|
|
|
|
const handleBulkDeleteRooms = async () => {
|
|
if (selectedRooms.length === 0) {
|
|
toast.warning('Please select at least one room to delete');
|
|
return;
|
|
}
|
|
|
|
if (!window.confirm(`Are you sure you want to delete ${selectedRooms.length} room(s)?`)) return;
|
|
|
|
try {
|
|
await roomService.bulkDeleteRooms(selectedRooms);
|
|
toast.success(`Successfully deleted ${selectedRooms.length} room(s)`);
|
|
setSelectedRooms([]);
|
|
fetchRooms();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete rooms');
|
|
}
|
|
};
|
|
|
|
const handleSelectRoom = (roomId: number) => {
|
|
setSelectedRooms(prev =>
|
|
prev.includes(roomId)
|
|
? prev.filter(id => id !== roomId)
|
|
: [...prev, roomId]
|
|
);
|
|
};
|
|
|
|
const handleSelectAllRooms = () => {
|
|
if (selectedRooms.length === rooms.length) {
|
|
setSelectedRooms([]);
|
|
} else {
|
|
setSelectedRooms(rooms.map(room => room.id));
|
|
}
|
|
};
|
|
|
|
const resetRoomForm = () => {
|
|
setEditingRoom(null);
|
|
setRoomFormData({
|
|
room_number: '',
|
|
floor: 1,
|
|
room_type_id: 1,
|
|
status: 'available',
|
|
featured: false,
|
|
price: '',
|
|
description: '',
|
|
capacity: '',
|
|
room_size: '',
|
|
view: '',
|
|
amenities: [],
|
|
});
|
|
setSelectedFiles([]);
|
|
setUploadingImages(false);
|
|
};
|
|
|
|
const toggleAmenity = (amenity: string) => {
|
|
setRoomFormData(prev => ({
|
|
...prev,
|
|
amenities: prev.amenities.includes(amenity)
|
|
? prev.amenities.filter(a => a !== amenity)
|
|
: [...prev.amenities, amenity]
|
|
}));
|
|
};
|
|
|
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (e.target.files) {
|
|
const files = Array.from(e.target.files);
|
|
setSelectedFiles(files);
|
|
}
|
|
};
|
|
|
|
const handleUploadImages = async () => {
|
|
if (!editingRoom || selectedFiles.length === 0) return;
|
|
|
|
try {
|
|
setUploadingImages(true);
|
|
const formData = new FormData();
|
|
selectedFiles.forEach(file => {
|
|
formData.append('images', file);
|
|
});
|
|
|
|
await apiClient.post(`/rooms/${editingRoom.id}/images`, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
});
|
|
|
|
toast.success('Images uploaded successfully');
|
|
setSelectedFiles([]);
|
|
fetchRooms();
|
|
|
|
const response = await roomService.getRoomByNumber(editingRoom.room_number);
|
|
setEditingRoom(response.data.room);
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to upload images');
|
|
} finally {
|
|
setUploadingImages(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteImage = async (imageUrl: string) => {
|
|
if (!editingRoom) return;
|
|
if (!window.confirm('Are you sure you want to delete this image?')) return;
|
|
|
|
try {
|
|
let imagePath = imageUrl;
|
|
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
|
try {
|
|
const url = new URL(imageUrl);
|
|
imagePath = url.pathname;
|
|
} catch (e) {
|
|
const match = imageUrl.match(/(\/uploads\/.*)/);
|
|
imagePath = match ? match[1] : imageUrl;
|
|
}
|
|
}
|
|
|
|
await apiClient.delete(`/rooms/${editingRoom.id}/images`, {
|
|
params: { image_url: imagePath },
|
|
});
|
|
|
|
toast.success('Image deleted successfully');
|
|
fetchRooms();
|
|
|
|
const response = await roomService.getRoomByNumber(editingRoom.room_number);
|
|
setEditingRoom(response.data.room);
|
|
} catch (error: any) {
|
|
console.error('Error deleting image:', error);
|
|
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete image');
|
|
}
|
|
};
|
|
|
|
const getRoomStatusBadge = (status: string) => {
|
|
const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
|
|
available: {
|
|
bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
|
|
text: 'text-emerald-800',
|
|
label: 'Available',
|
|
border: 'border-emerald-200'
|
|
},
|
|
occupied: {
|
|
bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
|
|
text: 'text-blue-800',
|
|
label: 'Occupied',
|
|
border: 'border-blue-200'
|
|
},
|
|
maintenance: {
|
|
bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
|
|
text: 'text-amber-800',
|
|
label: 'Maintenance',
|
|
border: 'border-amber-200'
|
|
},
|
|
};
|
|
const badge = badges[status] || badges.available;
|
|
return (
|
|
<span className={`px-4 py-1.5 rounded-full text-xs font-semibold border shadow-sm ${badge.bg} ${badge.text} ${badge.border}`}>
|
|
{badge.label}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
// Services Management Functions
|
|
useEffect(() => {
|
|
setServiceCurrentPage(1);
|
|
}, [serviceFilters]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'services') {
|
|
fetchServices();
|
|
}
|
|
}, [serviceFilters, serviceCurrentPage, activeTab]);
|
|
|
|
const fetchServices = async () => {
|
|
try {
|
|
setServicesLoading(true);
|
|
const response = await serviceService.getServices({
|
|
...serviceFilters,
|
|
page: serviceCurrentPage,
|
|
limit: serviceItemsPerPage,
|
|
});
|
|
setServices(response.data.services);
|
|
if (response.data.pagination) {
|
|
setServiceTotalPages(response.data.pagination.totalPages);
|
|
setServiceTotalItems(response.data.pagination.total);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to load services list');
|
|
} finally {
|
|
setServicesLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleServiceSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
try {
|
|
if (editingService) {
|
|
await serviceService.updateService(editingService.id, serviceFormData);
|
|
toast.success('Service updated successfully');
|
|
} else {
|
|
await serviceService.createService(serviceFormData);
|
|
toast.success('Service added successfully');
|
|
}
|
|
setShowServiceModal(false);
|
|
resetServiceForm();
|
|
fetchServices();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'An error occurred');
|
|
}
|
|
};
|
|
|
|
const handleEditService = (service: Service) => {
|
|
setEditingService(service);
|
|
setServiceFormData({
|
|
name: service.name,
|
|
description: service.description || '',
|
|
price: service.price,
|
|
unit: service.unit || 'time',
|
|
status: service.status,
|
|
});
|
|
setShowServiceModal(true);
|
|
};
|
|
|
|
const handleDeleteService = async (id: number) => {
|
|
if (!window.confirm('Are you sure you want to delete this service?')) return;
|
|
|
|
try {
|
|
await serviceService.deleteService(id);
|
|
toast.success('Service deleted successfully');
|
|
fetchServices();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to delete service');
|
|
}
|
|
};
|
|
|
|
const resetServiceForm = () => {
|
|
setEditingService(null);
|
|
setServiceFormData({
|
|
name: '',
|
|
description: '',
|
|
price: 0,
|
|
unit: 'time',
|
|
status: 'active',
|
|
});
|
|
};
|
|
|
|
const getServiceStatusBadge = (status: string) => {
|
|
return status === 'active' ? (
|
|
<span className="px-4 py-1.5 rounded-full text-xs font-semibold border shadow-sm bg-gradient-to-r from-emerald-50 to-green-50 text-emerald-800 border-emerald-200">
|
|
Active
|
|
</span>
|
|
) : (
|
|
<span className="px-4 py-1.5 rounded-full text-xs font-semibold border shadow-sm bg-gradient-to-r from-slate-50 to-gray-50 text-slate-700 border-slate-200">
|
|
Inactive
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const tabs = [
|
|
{ id: 'overview' as ReceptionTab, label: 'Overview', icon: LogIn },
|
|
{ id: 'check-in' as ReceptionTab, label: 'Check-in', icon: LogIn },
|
|
{ id: 'check-out' as ReceptionTab, label: 'Check-out', icon: LogOut },
|
|
{ id: 'bookings' as ReceptionTab, label: 'Bookings', icon: Calendar },
|
|
{ id: 'rooms' as ReceptionTab, label: 'Rooms', icon: Hotel },
|
|
{ id: 'services' as ReceptionTab, label: 'Services', icon: Wrench },
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-10 animate-fade-in">
|
|
{/* Luxury Header */}
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-emerald-400/5 via-transparent to-teal-600/5 rounded-3xl blur-3xl"></div>
|
|
<div className="relative bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-emerald-200/30 p-8 md:p-10">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-8">
|
|
<div className="flex items-start gap-5">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-emerald-400 to-teal-600 rounded-2xl blur-lg opacity-50"></div>
|
|
<div className="relative p-4 rounded-2xl bg-gradient-to-br from-emerald-500 via-emerald-500 to-teal-600 shadow-xl border border-emerald-400/50">
|
|
<LogIn className="w-8 h-8 text-white" />
|
|
<div className="absolute -top-1 -right-1 w-4 h-4 bg-gradient-to-br from-green-300 to-emerald-500 rounded-full shadow-lg animate-pulse"></div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3 flex-1">
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-4xl md:text-5xl font-extrabold bg-gradient-to-r from-slate-900 via-emerald-700 to-slate-900 bg-clip-text text-transparent">
|
|
Reception Dashboard
|
|
</h1>
|
|
<Sparkles className="w-6 h-6 text-emerald-500 animate-pulse" />
|
|
</div>
|
|
<p className="text-gray-600 text-base md:text-lg max-w-2xl leading-relaxed">
|
|
Manage guest check-in and check-out processes
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Premium Tab Navigation */}
|
|
<div className="mt-10 pt-8 border-t border-gradient-to-r from-transparent via-emerald-200/30 to-transparent">
|
|
<div className="flex flex-wrap gap-3">
|
|
{tabs.map((tab) => {
|
|
const Icon = tab.icon;
|
|
const isActive = activeTab === tab.id;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`
|
|
group relative flex items-center gap-3 px-6 py-3.5 rounded-xl font-semibold text-sm
|
|
transition-all duration-300 overflow-hidden
|
|
${
|
|
isActive
|
|
? 'bg-gradient-to-r from-emerald-500 via-emerald-500 to-teal-600 text-white shadow-xl shadow-emerald-500/40 scale-105'
|
|
: 'bg-white/80 text-gray-700 border border-gray-200/60 hover:border-emerald-300/60 hover:bg-gradient-to-r hover:from-emerald-50/50 hover:to-teal-50/30 hover:shadow-lg hover:scale-102'
|
|
}
|
|
`}
|
|
>
|
|
{isActive && (
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer"></div>
|
|
)}
|
|
<Icon className={`w-5 h-5 transition-transform duration-300 ${isActive ? 'text-white' : 'text-gray-600 group-hover:text-emerald-600 group-hover:scale-110'}`} />
|
|
<span className="relative z-10">{tab.label}</span>
|
|
{isActive && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-green-300 via-emerald-400 to-teal-400"></div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overview Tab */}
|
|
{activeTab === 'overview' && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
|
<div
|
|
onClick={() => setActiveTab('check-in')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-emerald-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-emerald-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-emerald-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-lg border border-emerald-400/50 group-hover:scale-110 transition-transform">
|
|
<LogIn className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-xl mb-1">Check-in</h3>
|
|
<div className="h-1 w-12 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
Process guest check-in and room assignment
|
|
</p>
|
|
<div className="pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-500 font-medium">Start Check-in</span>
|
|
<ChevronRight className="w-5 h-5 text-emerald-600 group-hover:translate-x-1 transition-transform" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-emerald-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('check-out')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-teal-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-teal-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-teal-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-teal-400 to-teal-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-teal-500 to-teal-600 shadow-lg border border-teal-400/50 group-hover:scale-110 transition-transform">
|
|
<LogOut className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-xl mb-1">Check-out</h3>
|
|
<div className="h-1 w-12 bg-gradient-to-r from-teal-500 to-teal-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
Process guest check-out and payment collection
|
|
</p>
|
|
<div className="pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-500 font-medium">Start Check-out</span>
|
|
<ChevronRight className="w-5 h-5 text-teal-600 group-hover:translate-x-1 transition-transform" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-teal-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('bookings')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-blue-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-blue-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-400 to-blue-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg border border-blue-400/50 group-hover:scale-110 transition-transform">
|
|
<Calendar className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-xl mb-1">Bookings</h3>
|
|
<div className="h-1 w-12 bg-gradient-to-r from-blue-500 to-blue-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
Manage and track all hotel bookings
|
|
</p>
|
|
<div className="pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-500 font-medium">Manage Bookings</span>
|
|
<ChevronRight className="w-5 h-5 text-blue-600 group-hover:translate-x-1 transition-transform" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-blue-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('rooms')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-amber-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-amber-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-amber-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-amber-400 to-amber-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 shadow-lg border border-amber-400/50 group-hover:scale-110 transition-transform">
|
|
<Hotel className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-xl mb-1">Rooms</h3>
|
|
<div className="h-1 w-12 bg-gradient-to-r from-amber-500 to-amber-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
Manage hotel room information and availability
|
|
</p>
|
|
<div className="pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-500 font-medium">Manage Rooms</span>
|
|
<ChevronRight className="w-5 h-5 text-amber-600 group-hover:translate-x-1 transition-transform" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-amber-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('services')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-purple-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-purple-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-purple-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-purple-400 to-purple-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 shadow-lg border border-purple-400/50 group-hover:scale-110 transition-transform">
|
|
<Wrench className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-xl mb-1">Services</h3>
|
|
<div className="h-1 w-12 bg-gradient-to-r from-purple-500 to-purple-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
Manage hotel services and amenities
|
|
</p>
|
|
<div className="pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-500 font-medium">Manage Services</span>
|
|
<ChevronRight className="w-5 h-5 text-purple-600 group-hover:translate-x-1 transition-transform" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-purple-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Check-in Tab */}
|
|
{activeTab === 'check-in' && (
|
|
<div className="space-y-8">
|
|
{checkInLoading && (
|
|
<Loading fullScreen text="Processing check-in..." />
|
|
)}
|
|
|
|
{/* Section Header */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-emerald-500/10 to-green-500/10 border border-emerald-200/40">
|
|
<LogIn className="w-6 h-6 text-emerald-600" />
|
|
</div>
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Check-in</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Customer check-in process and room assignment
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search Booking */}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
|
|
<span className="text-emerald-600 font-bold">1</span>
|
|
</div>
|
|
Search Booking
|
|
</h2>
|
|
<div className="flex gap-4">
|
|
<div className="flex-1 relative group">
|
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 group-focus-within:text-emerald-500 transition-colors" />
|
|
<input
|
|
type="text"
|
|
value={checkInBookingNumber}
|
|
onChange={(e) => setCheckInBookingNumber(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleCheckInSearch()}
|
|
placeholder="Enter booking number"
|
|
className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleCheckInSearch}
|
|
disabled={checkInSearching}
|
|
className="group relative px-8 py-3.5 bg-gradient-to-r from-emerald-500 via-emerald-500 to-teal-600 text-white font-semibold rounded-xl shadow-xl shadow-emerald-500/30 hover:shadow-2xl hover:shadow-emerald-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<div className="relative flex items-center gap-2">
|
|
<Search className="w-5 h-5" />
|
|
{checkInSearching ? 'Searching...' : 'Search'}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Booking Info */}
|
|
{checkInBooking && (
|
|
<>
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
|
|
<CheckCircle className="w-5 h-5 text-emerald-600" />
|
|
</div>
|
|
Booking Information
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Booking Number:</span>
|
|
<span className="font-bold text-gray-900 font-mono">{checkInBooking.booking_number}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Customer:</span>
|
|
<span className="font-semibold text-gray-900">{checkInBooking.user?.full_name}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Email:</span>
|
|
<span className="text-gray-900">{checkInBooking.user?.email}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2">
|
|
<span className="text-gray-600 font-medium">Phone:</span>
|
|
<span className="text-gray-900">{checkInBooking.user?.phone_number || 'N/A'}</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Room Type:</span>
|
|
<span className="font-semibold text-gray-900">{checkInBooking.room?.room_type?.name || 'N/A'}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Check-in:</span>
|
|
<span className="text-gray-900">{checkInBooking.check_in_date ? parseDateLocal(checkInBooking.check_in_date).toLocaleDateString('en-US') : 'N/A'}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Check-out:</span>
|
|
<span className="text-gray-900">{checkInBooking.check_out_date ? parseDateLocal(checkInBooking.check_out_date).toLocaleDateString('en-US') : 'N/A'}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2">
|
|
<span className="text-gray-600 font-medium">Number of Guests:</span>
|
|
<span className="font-semibold text-gray-900">{checkInBooking.guest_count} guest(s)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{checkInBooking.status !== 'confirmed' && (
|
|
<div className="mt-6 p-4 bg-gradient-to-br from-amber-50 to-yellow-50 border border-amber-200 rounded-xl flex items-start gap-3">
|
|
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" />
|
|
<div>
|
|
<p className="text-sm font-semibold text-amber-900">Warning</p>
|
|
<p className="text-sm text-amber-800 mt-1">
|
|
Booking status: <span className="font-semibold">{checkInBooking.status}</span>. Only check-in confirmed bookings.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Assign Room */}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
<Hotel className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
Assign Actual Room Number
|
|
</h2>
|
|
<div className="max-w-md space-y-3">
|
|
<label className="block text-sm font-semibold text-gray-900">
|
|
Room Number <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={actualRoomNumber}
|
|
onChange={(e) => setActualRoomNumber(e.target.value)}
|
|
placeholder="e.g: 101, 202, 305"
|
|
className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm hover:shadow-md"
|
|
/>
|
|
<p className="text-xs text-gray-500">
|
|
Enter the actual room number to assign to the guest
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Guest Information */}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-purple-100 flex items-center justify-center">
|
|
<User className="w-5 h-5 text-purple-600" />
|
|
</div>
|
|
Guest Information
|
|
</h2>
|
|
<div className="space-y-6">
|
|
{guests.map((guest, index) => (
|
|
<div key={index} className="p-6 bg-gradient-to-br from-gray-50 to-white border-2 border-gray-200 rounded-xl hover:border-purple-200 transition-colors">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="font-bold text-gray-900">
|
|
{index === 0 ? 'Main Guest' : `Guest ${index + 1}`}
|
|
{index === 0 && <span className="text-red-500 ml-1">*</span>}
|
|
</h3>
|
|
{index > 0 && (
|
|
<button
|
|
onClick={() => handleRemoveGuest(index)}
|
|
className="px-4 py-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors text-sm font-semibold"
|
|
>
|
|
Remove
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-semibold text-gray-700">
|
|
Full Name {index === 0 && <span className="text-red-500">*</span>}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={guest.name}
|
|
onChange={(e) => handleGuestChange(index, 'name', e.target.value)}
|
|
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
|
|
placeholder="John Doe"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-semibold text-gray-700">
|
|
ID Number {index === 0 && <span className="text-red-500">*</span>}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={guest.id_number}
|
|
onChange={(e) => handleGuestChange(index, 'id_number', e.target.value)}
|
|
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm font-mono"
|
|
placeholder="001234567890"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-semibold text-gray-700">
|
|
Phone Number {index === 0 && <span className="text-red-500">*</span>}
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={guest.phone}
|
|
onChange={(e) => handleGuestChange(index, 'phone', e.target.value)}
|
|
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
|
|
placeholder="0912345678"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<button
|
|
onClick={handleAddGuest}
|
|
className="px-6 py-3 text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 rounded-xl transition-colors text-sm font-semibold border-2 border-emerald-200 hover:border-emerald-300"
|
|
>
|
|
+ Add Guest
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Additional Charges */}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">Additional Fees (if any)</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="space-y-3">
|
|
<label className="block text-sm font-semibold text-gray-900">Extra Persons</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={extraPersons}
|
|
onChange={(e) => {
|
|
setExtraPersons(parseInt(e.target.value) || 0);
|
|
calculateCheckInAdditionalFee();
|
|
}}
|
|
className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
|
|
/>
|
|
<p className="text-xs text-gray-500">€50/person</p>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<label className="block text-sm font-semibold text-gray-900">Number of Children</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={children}
|
|
onChange={(e) => {
|
|
setChildren(parseInt(e.target.value) || 0);
|
|
calculateCheckInAdditionalFee();
|
|
}}
|
|
className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
|
|
/>
|
|
<p className="text-xs text-gray-500">€25/child</p>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<label className="block text-sm font-semibold text-gray-900">Total Additional Fee</label>
|
|
<div className="px-4 py-3.5 bg-gradient-to-br from-emerald-50 to-green-50 border-2 border-emerald-200 rounded-xl text-lg font-bold text-emerald-600">
|
|
{formatCurrency(calculateCheckInAdditionalFee())}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary & Action */}
|
|
<div className="relative bg-gradient-to-r from-emerald-500 via-emerald-600 to-teal-600 rounded-2xl shadow-2xl p-8 text-white overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -mr-32 -mt-32"></div>
|
|
<div className="relative flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
|
<div>
|
|
<h3 className="text-xl font-bold mb-2">Confirm Check-in</h3>
|
|
<p className="text-emerald-100 text-sm">
|
|
Guest: <span className="font-semibold">{checkInBooking.user?.full_name}</span> |
|
|
Room: <span className="font-semibold">{actualRoomNumber || 'Not assigned'}</span>
|
|
{additionalFee > 0 && (
|
|
<> | Additional Fee: <span className="font-semibold">{formatCurrency(additionalFee)}</span></>
|
|
)}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleCheckIn}
|
|
disabled={!actualRoomNumber || !guests[0].name || checkInBooking.status !== 'confirmed'}
|
|
className="px-8 py-4 bg-white text-emerald-600 rounded-xl hover:bg-emerald-50 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed font-bold flex items-center gap-2 transition-all duration-200 shadow-xl hover:shadow-2xl hover:scale-105 disabled:hover:scale-100"
|
|
>
|
|
<CheckCircle className="w-5 h-5" />
|
|
Confirm Check-in
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!checkInBooking && !checkInSearching && (
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-12 text-center">
|
|
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-emerald-100 to-emerald-200 flex items-center justify-center">
|
|
<Search className="w-10 h-10 text-emerald-600" />
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">No booking selected</h3>
|
|
<p className="text-gray-600">
|
|
Please enter booking number above to start check-in process
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Check-out Tab */}
|
|
{activeTab === 'check-out' && (
|
|
<div className="space-y-8">
|
|
{checkOutLoading && (
|
|
<Loading fullScreen text="Processing check-out..." />
|
|
)}
|
|
|
|
{/* Section Header */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-teal-500/10 to-cyan-500/10 border border-teal-200/40">
|
|
<LogOut className="w-6 h-6 text-teal-600" />
|
|
</div>
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Check-out</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Payment and check-out process for guests
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search Booking */}
|
|
{!showInvoice && (
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-teal-100 flex items-center justify-center">
|
|
<span className="text-teal-600 font-bold">1</span>
|
|
</div>
|
|
Search Booking
|
|
</h2>
|
|
<div className="flex gap-4">
|
|
<div className="flex-1 relative group">
|
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 group-focus-within:text-teal-500 transition-colors" />
|
|
<input
|
|
type="text"
|
|
value={checkOutBookingNumber}
|
|
onChange={(e) => setCheckOutBookingNumber(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleCheckOutSearch()}
|
|
placeholder="Enter booking number or room number"
|
|
className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-teal-400 focus:ring-4 focus:ring-teal-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleCheckOutSearch}
|
|
disabled={checkOutSearching}
|
|
className="group relative px-8 py-3.5 bg-gradient-to-r from-teal-500 via-teal-500 to-cyan-600 text-white font-semibold rounded-xl shadow-xl shadow-teal-500/30 hover:shadow-2xl hover:shadow-teal-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<div className="relative flex items-center gap-2">
|
|
<Search className="w-5 h-5" />
|
|
{checkOutSearching ? 'Searching...' : 'Search'}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Invoice */}
|
|
{checkOutBooking && !showInvoice && (
|
|
<>
|
|
{/* Booking Info */}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-teal-100 flex items-center justify-center">
|
|
<CheckCircle className="w-5 h-5 text-teal-600" />
|
|
</div>
|
|
Booking Information
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Booking number:</span>
|
|
<span className="font-bold text-gray-900 font-mono">{checkOutBooking.booking_number}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Customer:</span>
|
|
<span className="font-semibold text-gray-900">{checkOutBooking.user?.full_name}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2">
|
|
<span className="text-gray-600 font-medium">Room number:</span>
|
|
<span className="font-semibold text-gray-900">{checkOutBooking.room?.room_number}</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Check-in:</span>
|
|
<span className="text-gray-900">{checkOutBooking.check_in_date ? parseDateLocal(checkOutBooking.check_in_date).toLocaleDateString('en-US') : 'N/A'}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
<span className="text-gray-600 font-medium">Check-out:</span>
|
|
<span className="text-gray-900">{checkOutBooking.check_out_date ? parseDateLocal(checkOutBooking.check_out_date).toLocaleDateString('en-US') : 'N/A'}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2">
|
|
<span className="text-gray-600 font-medium">Nights:</span>
|
|
<span className="font-semibold text-gray-900">
|
|
{checkOutBooking.check_in_date && checkOutBooking.check_out_date
|
|
? Math.ceil((parseDateLocal(checkOutBooking.check_out_date).getTime() - parseDateLocal(checkOutBooking.check_in_date).getTime()) / (1000 * 60 * 60 * 24))
|
|
: 0} night(s)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bill Details */}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
<FileText className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
Invoice Details
|
|
</h2>
|
|
|
|
{/* Room Fee */}
|
|
<div className="mb-6">
|
|
<h3 className="font-semibold text-gray-700 mb-3">Room Fee</h3>
|
|
<div className="bg-gradient-to-br from-gray-50 to-white p-5 rounded-xl border border-gray-200">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-gray-900 font-medium">{checkOutBooking.room?.room_type?.name || 'Room'}</span>
|
|
<span className="font-bold text-gray-900">{formatCurrency(calculateRoomFee())}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Service Fee */}
|
|
{checkOutServices.length > 0 && (
|
|
<div className="mb-6">
|
|
<h3 className="font-semibold text-gray-700 mb-3">Services Used</h3>
|
|
<div className="bg-gradient-to-br from-gray-50 to-white p-5 rounded-xl border border-gray-200 space-y-3">
|
|
{checkOutServices.map((service, index) => (
|
|
<div key={index} className="flex justify-between items-center text-sm py-2 border-b border-gray-200 last:border-0">
|
|
<span className="text-gray-700">
|
|
{service.service_name} (x{service.quantity})
|
|
</span>
|
|
<span className="font-semibold text-gray-900">{formatCurrency(service.total)}</span>
|
|
</div>
|
|
))}
|
|
<div className="pt-3 border-t-2 border-gray-300 flex justify-between font-bold">
|
|
<span>Total services:</span>
|
|
<span className="text-teal-600">{formatCurrency(calculateServiceFee())}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Additional Fee */}
|
|
{calculateCheckOutAdditionalFee() > 0 && (
|
|
<div className="mb-6">
|
|
<h3 className="font-semibold text-gray-700 mb-3">Additional Fees</h3>
|
|
<div className="bg-gradient-to-br from-gray-50 to-white p-5 rounded-xl border border-gray-200">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-gray-700">Extra person/children fee</span>
|
|
<span className="font-semibold text-gray-900">{formatCurrency(calculateCheckOutAdditionalFee())}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Discount */}
|
|
<div className="mb-6">
|
|
<h3 className="font-semibold text-gray-700 mb-3">Discount</h3>
|
|
<input
|
|
type="number"
|
|
value={discount}
|
|
onChange={(e) => setDiscount(parseFloat(e.target.value) || 0)}
|
|
placeholder="Enter discount amount"
|
|
className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-teal-400 focus:ring-4 focus:ring-teal-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
<div className="border-t-2 border-gray-300 pt-6 space-y-4">
|
|
<div className="flex justify-between items-center text-lg">
|
|
<span className="text-gray-700 font-medium">Subtotal:</span>
|
|
<span className="font-bold text-gray-900">{formatCurrency(calculateSubtotal())}</span>
|
|
</div>
|
|
{discount > 0 && (
|
|
<div className="flex justify-between items-center text-lg text-rose-600">
|
|
<span>Discount:</span>
|
|
<span className="font-semibold">-{formatCurrency(discount)}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between items-center text-2xl font-extrabold bg-gradient-to-r from-teal-600 to-cyan-600 bg-clip-text text-transparent pt-2 border-t-2 border-gray-300">
|
|
<span>Total:</span>
|
|
<span>{formatCurrency(calculateTotal())}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center text-lg text-gray-600">
|
|
<span>Deposit paid:</span>
|
|
<span className="font-semibold">-{formatCurrency(calculateDeposit())}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center text-3xl font-extrabold text-emerald-600 pt-4 border-t-2 border-gray-300">
|
|
<span>Remaining payment:</span>
|
|
<span>{formatCurrency(calculateRemaining())}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Payment Method */}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6 flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-green-100 flex items-center justify-center">
|
|
<CreditCard className="w-5 h-5 text-green-600" />
|
|
</div>
|
|
Payment Method
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl">
|
|
<button
|
|
onClick={() => setPaymentMethod('cash')}
|
|
className={`p-6 border-2 rounded-xl text-center transition-all duration-200 ${
|
|
paymentMethod === 'cash'
|
|
? 'border-emerald-600 bg-gradient-to-br from-emerald-50 to-green-50 text-emerald-700 shadow-lg'
|
|
: 'border-gray-200 hover:border-gray-300 text-gray-700 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<CurrencyIcon className="mx-auto mb-3" size={40} />
|
|
<div className="font-bold text-lg">Cash</div>
|
|
</button>
|
|
<button
|
|
onClick={() => setPaymentMethod('stripe')}
|
|
className={`p-6 border-2 rounded-xl text-center transition-all duration-200 ${
|
|
paymentMethod === 'stripe'
|
|
? 'border-indigo-600 bg-gradient-to-br from-indigo-50 to-purple-50 text-indigo-700 shadow-lg'
|
|
: 'border-gray-200 hover:border-gray-300 text-gray-700 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<CreditCard className="w-10 h-10 mx-auto mb-3" />
|
|
<div className="font-bold text-lg">Stripe</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action */}
|
|
<div className="relative bg-gradient-to-r from-teal-500 via-teal-600 to-cyan-600 rounded-2xl shadow-2xl p-8 text-white overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -mr-32 -mt-32"></div>
|
|
<div className="relative flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
|
<div>
|
|
<h3 className="text-xl font-bold mb-2">Confirm Check-out</h3>
|
|
<p className="text-teal-100 text-sm">
|
|
Total payment: <span className="font-bold text-white text-xl">{formatCurrency(calculateRemaining())}</span>
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleCheckOut}
|
|
disabled={checkOutBooking.status !== 'checked_in'}
|
|
className="px-8 py-4 bg-white text-teal-600 rounded-xl hover:bg-teal-50 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed font-bold flex items-center gap-2 transition-all duration-200 shadow-xl hover:shadow-2xl hover:scale-105 disabled:hover:scale-100"
|
|
>
|
|
<CheckCircle className="w-5 h-5" />
|
|
Confirm payment & Check-out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Invoice Display */}
|
|
{showInvoice && checkOutBooking && (
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-2xl border border-gray-200/50 p-8">
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-3xl font-extrabold text-gray-900">PAYMENT INVOICE</h2>
|
|
<p className="text-gray-600 mt-2">Check-out successful</p>
|
|
</div>
|
|
|
|
<div className="border-t-2 border-b-2 border-gray-300 py-6 mb-8">
|
|
<div className="grid grid-cols-2 gap-8">
|
|
<div>
|
|
<p className="text-sm text-gray-600 mb-1">Booking number:</p>
|
|
<p className="font-bold text-gray-900 font-mono">{checkOutBooking.booking_number}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-600 mb-1">Check-out date:</p>
|
|
<p className="font-bold text-gray-900">{new Date().toLocaleString('en-US')}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-600 mb-1">Customer:</p>
|
|
<p className="font-bold text-gray-900">{checkOutBooking.user?.full_name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-600 mb-1">Payment method:</p>
|
|
<p className="font-bold text-gray-900">
|
|
{paymentMethod === 'cash' ? 'Cash' : 'Stripe'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-8">
|
|
<div className="flex justify-between items-center text-2xl font-extrabold text-emerald-600">
|
|
<span>Total payment:</span>
|
|
<span>{formatCurrency(calculateRemaining())}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
<button
|
|
onClick={handlePrintInvoice}
|
|
className="flex-1 px-6 py-4 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 font-semibold flex items-center justify-center gap-2 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
>
|
|
<Printer className="w-5 h-5" />
|
|
Print Invoice
|
|
</button>
|
|
<button
|
|
onClick={resetCheckOutForm}
|
|
className="flex-1 px-6 py-4 bg-gradient-to-r from-gray-400 to-gray-500 text-white rounded-xl hover:from-gray-500 hover:to-gray-600 font-semibold transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
>
|
|
Complete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!checkOutBooking && !checkOutSearching && !showInvoice && (
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-12 text-center">
|
|
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-teal-100 to-teal-200 flex items-center justify-center">
|
|
<Search className="w-10 h-10 text-teal-600" />
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">No booking selected</h3>
|
|
<p className="text-gray-600">
|
|
Please enter booking number to start check-out process
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Bookings Tab */}
|
|
{activeTab === 'bookings' && (
|
|
<div className="space-y-8">
|
|
{bookingsLoading && <Loading />}
|
|
|
|
{/* Section Header */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-blue-500/10 to-indigo-500/10 border border-blue-200/40">
|
|
<Calendar className="w-6 h-6 text-blue-600" />
|
|
</div>
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Bookings Management</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Manage and track all hotel bookings with precision
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div className="relative group">
|
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 group-focus-within:text-blue-500 transition-colors" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search by booking number, guest name..."
|
|
value={bookingFilters.search}
|
|
onChange={(e) => setBookingFilters({ ...bookingFilters, search: e.target.value })}
|
|
className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
|
|
/>
|
|
</div>
|
|
<select
|
|
value={bookingFilters.status}
|
|
onChange={(e) => setBookingFilters({ ...bookingFilters, status: e.target.value })}
|
|
className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all duration-200 text-gray-700 font-medium shadow-sm hover:shadow-md cursor-pointer"
|
|
>
|
|
<option value="">All statuses</option>
|
|
<option value="pending">Pending confirmation</option>
|
|
<option value="confirmed">Confirmed</option>
|
|
<option value="checked_in">Checked in</option>
|
|
<option value="checked_out">Checked out</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bookings Table */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900">
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Booking Number</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Customer</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Room</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Check-in/out</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Total Price</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Status</th>
|
|
<th className="px-8 py-5 text-right text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-slate-100">
|
|
{bookings.map((booking) => (
|
|
<tr
|
|
key={booking.id}
|
|
className="hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30 transition-all duration-200 group border-b border-slate-100"
|
|
>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-bold text-gray-900 group-hover:text-blue-700 transition-colors font-mono">
|
|
{booking.booking_number}
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-semibold text-gray-900">{booking.guest_info?.full_name || booking.user?.name}</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">{booking.guest_info?.email || booking.user?.email}</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-800">
|
|
<span className="text-blue-600 font-semibold">Room {booking.room?.room_number}</span>
|
|
<span className="text-gray-400 mx-2">•</span>
|
|
<span className="text-gray-600">{booking.room?.room_type?.name}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{parseDateLocal(booking.check_in_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">
|
|
→ {parseDateLocal(booking.check_out_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-bold text-gray-900 bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
|
|
{formatCurrency(booking.total_price)}
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
{getBookingStatusBadge(booking.status)}
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap text-right">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={() => {
|
|
setSelectedBooking(booking);
|
|
setShowBookingDetailModal(true);
|
|
}}
|
|
className="p-2 rounded-lg text-gray-600 hover:text-blue-600 hover:bg-blue-50 transition-all duration-200 shadow-sm hover:shadow-md border border-gray-200 hover:border-blue-300"
|
|
title="View details"
|
|
>
|
|
<Eye className="w-5 h-5" />
|
|
</button>
|
|
{booking.status === 'pending' && (
|
|
<>
|
|
<button
|
|
onClick={() => handleUpdateBookingStatus(booking.id, 'confirmed')}
|
|
disabled={updatingBookingId === booking.id || cancellingBookingId === booking.id}
|
|
className="p-2 rounded-lg text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow-md border border-emerald-200 hover:border-emerald-300"
|
|
title="Confirm"
|
|
>
|
|
{updatingBookingId === booking.id ? (
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
) : (
|
|
<CheckCircle className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => handleCancelBooking(booking.id)}
|
|
disabled={updatingBookingId === booking.id || cancellingBookingId === booking.id}
|
|
className="p-2 rounded-lg text-rose-600 hover:text-rose-700 hover:bg-rose-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow-md border border-rose-200 hover:border-rose-300"
|
|
title="Cancel"
|
|
>
|
|
{cancellingBookingId === booking.id ? (
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
) : (
|
|
<XCircle className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
</>
|
|
)}
|
|
{booking.status === 'confirmed' && (
|
|
<button
|
|
onClick={() => handleUpdateBookingStatus(booking.id, 'checked_in')}
|
|
disabled={updatingBookingId === booking.id || cancellingBookingId === booking.id}
|
|
className="p-2 rounded-lg text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow-md border border-emerald-200 hover:border-emerald-300"
|
|
title="Check-in"
|
|
>
|
|
{updatingBookingId === booking.id ? (
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
) : (
|
|
<CheckCircle className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<Pagination
|
|
currentPage={bookingCurrentPage}
|
|
totalPages={bookingTotalPages}
|
|
onPageChange={setBookingCurrentPage}
|
|
totalItems={bookingTotalItems}
|
|
itemsPerPage={bookingItemsPerPage}
|
|
/>
|
|
</div>
|
|
|
|
{/* Booking Detail Modal */}
|
|
{showBookingDetailModal && selectedBooking && (
|
|
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden">
|
|
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-3xl font-bold text-amber-100 mb-1">Booking Details</h2>
|
|
<p className="text-amber-200/80 text-sm font-light">Comprehensive booking information</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowBookingDetailModal(false)}
|
|
className="w-10 h-10 flex items-center justify-center rounded-xl text-amber-100 hover:text-white hover:bg-slate-700/50 transition-all duration-200 border border-slate-600 hover:border-amber-400"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-8 overflow-y-auto max-h-[calc(90vh-120px)]">
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Booking Number</label>
|
|
<p className="text-xl font-bold text-slate-900 font-mono">{selectedBooking.booking_number}</p>
|
|
</div>
|
|
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Status</label>
|
|
<div className="mt-1">{getBookingStatusBadge(selectedBooking.status)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-br from-amber-50/50 to-yellow-50/50 p-6 rounded-xl border border-amber-100">
|
|
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
|
<div className="w-1 h-4 bg-gradient-to-b from-amber-400 to-amber-600 rounded-full"></div>
|
|
Customer Information
|
|
</label>
|
|
<div className="space-y-2">
|
|
<p className="text-lg font-bold text-slate-900">{selectedBooking.guest_info?.full_name || selectedBooking.user?.name}</p>
|
|
<p className="text-slate-600">{selectedBooking.guest_info?.email || selectedBooking.user?.email}</p>
|
|
<p className="text-slate-600">{selectedBooking.guest_info?.phone || selectedBooking.user?.phone}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-br from-blue-50/50 to-indigo-50/50 p-6 rounded-xl border border-blue-100">
|
|
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
|
<div className="w-1 h-4 bg-gradient-to-b from-blue-400 to-blue-600 rounded-full"></div>
|
|
Room Information
|
|
</label>
|
|
<p className="text-lg font-semibold text-slate-900">
|
|
<span className="text-amber-600">Room {selectedBooking.room?.room_number}</span>
|
|
<span className="text-slate-400 mx-2">•</span>
|
|
<span className="text-slate-700">{selectedBooking.room?.room_type?.name}</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Check-in Date</label>
|
|
<p className="text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_in_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
|
</div>
|
|
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Check-out Date</label>
|
|
<p className="text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_out_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Number of Guests</label>
|
|
<p className="text-lg font-semibold text-slate-900">{selectedBooking.guest_count} guest{selectedBooking.guest_count !== 1 ? 's' : ''}</p>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-br from-amber-50 via-yellow-50 to-amber-50 p-6 rounded-xl border-2 border-amber-200 shadow-lg">
|
|
<label className="text-xs font-semibold text-amber-700 uppercase tracking-wider mb-2 block">Total Price</label>
|
|
<p className="text-4xl font-bold bg-gradient-to-r from-amber-600 via-amber-700 to-amber-600 bg-clip-text text-transparent">
|
|
{formatCurrency(selectedBooking.total_price)}
|
|
</p>
|
|
</div>
|
|
|
|
{selectedBooking.notes && (
|
|
<div className="bg-gradient-to-br from-slate-50 to-white p-6 rounded-xl border border-slate-200">
|
|
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 block">Special Notes</label>
|
|
<p className="text-slate-700 leading-relaxed">{selectedBooking.notes}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-8 pt-6 border-t border-slate-200 flex justify-end">
|
|
<button
|
|
onClick={() => setShowBookingDetailModal(false)}
|
|
className="px-8 py-3 bg-gradient-to-r from-slate-700 to-slate-800 text-white rounded-xl font-semibold hover:from-slate-800 hover:to-slate-900 transition-all duration-200 shadow-lg hover:shadow-xl border border-slate-600"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Rooms Tab */}
|
|
{activeTab === 'rooms' && (
|
|
<div className="space-y-8">
|
|
{roomsLoading && <Loading />}
|
|
|
|
{/* Section Header */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex justify-between items-center">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-amber-500/10 to-yellow-500/10 border border-amber-200/40">
|
|
<Hotel className="w-6 h-6 text-amber-600" />
|
|
</div>
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Room Management</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Manage hotel room information and availability
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
{selectedRooms.length > 0 && (
|
|
<button
|
|
onClick={handleBulkDeleteRooms}
|
|
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-rose-500 to-rose-600 text-white rounded-xl font-semibold hover:from-rose-600 hover:to-rose-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
Delete Selected ({selectedRooms.length})
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => {
|
|
resetRoomForm();
|
|
setShowRoomModal(true);
|
|
}}
|
|
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
Add Room
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
|
<div className="relative group">
|
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 group-focus-within:text-amber-500 transition-colors" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search rooms..."
|
|
value={roomFilters.search}
|
|
onChange={(e) => setRoomFilters({ ...roomFilters, search: e.target.value })}
|
|
className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
|
|
/>
|
|
</div>
|
|
<select
|
|
value={roomFilters.status}
|
|
onChange={(e) => setRoomFilters({ ...roomFilters, status: e.target.value })}
|
|
className="px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-gray-700 font-medium shadow-sm hover:shadow-md cursor-pointer"
|
|
>
|
|
<option value="">All Statuses</option>
|
|
<option value="available">Available</option>
|
|
<option value="occupied">Occupied</option>
|
|
<option value="maintenance">Maintenance</option>
|
|
</select>
|
|
<select
|
|
value={roomFilters.type}
|
|
onChange={(e) => setRoomFilters({ ...roomFilters, type: e.target.value })}
|
|
className="px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-gray-700 font-medium shadow-sm hover:shadow-md cursor-pointer"
|
|
>
|
|
<option value="">All Room Types</option>
|
|
{roomTypes.map((roomType) => (
|
|
<option key={roomType.id} value={roomType.name}>
|
|
{roomType.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rooms Table */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900">
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700 w-12">
|
|
<input
|
|
type="checkbox"
|
|
checked={rooms.length > 0 && selectedRooms.length === rooms.length}
|
|
onChange={handleSelectAllRooms}
|
|
title="Select all rooms"
|
|
className="w-5 h-5 text-amber-600 bg-slate-700 border-slate-600 rounded focus:ring-amber-500 cursor-pointer"
|
|
/>
|
|
</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Room Number</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Room Type</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Floor</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Price</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Status</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Featured</th>
|
|
<th className="px-8 py-5 text-right text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-slate-100">
|
|
{rooms.map((room) => (
|
|
<tr
|
|
key={room.id}
|
|
className={`hover:bg-gradient-to-r hover:from-amber-50/30 hover:to-yellow-50/30 transition-all duration-200 group border-b border-slate-100 ${selectedRooms.includes(room.id) ? 'bg-amber-50/50' : ''}`}
|
|
>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedRooms.includes(room.id)}
|
|
onChange={() => handleSelectRoom(room.id)}
|
|
className="w-5 h-5 text-amber-600 bg-white border-slate-300 rounded focus:ring-amber-500 cursor-pointer"
|
|
title={`Select room ${room.room_number}`}
|
|
/>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-bold text-gray-900 font-mono">{room.room_number}</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">{room.room_type?.name || 'N/A'}</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm text-gray-700">Floor {room.floor}</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-bold bg-gradient-to-r from-emerald-600 to-emerald-700 bg-clip-text text-transparent">
|
|
{formatCurrency(room.price || room.room_type?.base_price || 0)}
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
{getRoomStatusBadge(room.status)}
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
{room.featured ? (
|
|
<span className="text-amber-500 text-lg">⭐</span>
|
|
) : (
|
|
<span className="text-gray-400">-</span>
|
|
)}
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap text-right">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={() => handleEditRoom(room)}
|
|
className="p-2 rounded-lg text-blue-600 hover:text-blue-700 hover:bg-blue-50 transition-all duration-200 shadow-sm hover:shadow-md border border-blue-200 hover:border-blue-300"
|
|
title="Edit"
|
|
>
|
|
<Edit className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteRoom(room.id)}
|
|
className="p-2 rounded-lg text-rose-600 hover:text-rose-700 hover:bg-rose-50 transition-all duration-200 shadow-sm hover:shadow-md border border-rose-200 hover:border-rose-300"
|
|
title="Delete"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<Pagination
|
|
currentPage={roomCurrentPage}
|
|
totalPages={roomTotalPages}
|
|
onPageChange={setRoomCurrentPage}
|
|
totalItems={roomTotalItems}
|
|
itemsPerPage={roomItemsPerPage}
|
|
/>
|
|
</div>
|
|
|
|
{/* Room Modal - Simplified version, will need full implementation */}
|
|
{showRoomModal && (
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
|
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] rounded-xl border border-[#d4af37]/30 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/20 p-8 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<div className="flex justify-between items-center mb-6 pb-6 border-b border-[#d4af37]/20">
|
|
<div>
|
|
<h2 className="text-3xl font-serif font-semibold bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent tracking-wide">
|
|
{editingRoom ? 'Update Room' : 'Add New Room'}
|
|
</h2>
|
|
<p className="text-gray-400 text-sm mt-1 font-light">
|
|
{editingRoom ? 'Modify room details and amenities' : 'Create a new luxurious room'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowRoomModal(false)}
|
|
className="p-2 hover:bg-[#d4af37]/10 rounded-lg transition-colors border border-[#d4af37]/20 hover:border-[#d4af37]/40"
|
|
>
|
|
<X className="w-6 h-6 text-[#d4af37] hover:text-white transition-colors" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleRoomSubmit} className="space-y-6">
|
|
{/* Basic Information Section */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-serif font-semibold text-[#d4af37] mb-4 flex items-center gap-2">
|
|
<div className="w-1 h-6 bg-gradient-to-b from-[#d4af37] to-[#c9a227]" />
|
|
Basic Information
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Room Number
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={roomFormData.room_number}
|
|
onChange={(e) => setRoomFormData({ ...roomFormData, room_number: 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., 1001"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Floor
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={roomFormData.floor}
|
|
onChange={(e) => setRoomFormData({ ...roomFormData, floor: parseInt(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"
|
|
required
|
|
min="1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Room Type
|
|
</label>
|
|
<select
|
|
value={roomFormData.room_type_id}
|
|
onChange={(e) => setRoomFormData({ ...roomFormData, room_type_id: parseInt(e.target.value) })}
|
|
className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
|
|
required
|
|
>
|
|
{roomTypes.length > 0 ? (
|
|
roomTypes.map((roomType) => (
|
|
<option key={roomType.id} value={roomType.id} className="bg-[#1a1a1a]">
|
|
{roomType.name}
|
|
</option>
|
|
))
|
|
) : (
|
|
<option value="" className="bg-[#1a1a1a]">Loading...</option>
|
|
)}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Status
|
|
</label>
|
|
<select
|
|
value={roomFormData.status}
|
|
onChange={(e) => setRoomFormData({ ...roomFormData, status: e.target.value as any })}
|
|
className="w-full px-4 py-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] transition-all duration-300"
|
|
required
|
|
>
|
|
<option value="available" className="bg-[#1a1a1a]">Available</option>
|
|
<option value="occupied" className="bg-[#1a1a1a]">Occupied</option>
|
|
<option value="maintenance" className="bg-[#1a1a1a]">Maintenance</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 p-4 bg-[#0a0a0a] rounded-lg border border-[#d4af37]/20">
|
|
<input
|
|
type="checkbox"
|
|
id="featured"
|
|
checked={roomFormData.featured}
|
|
onChange={(e) => 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"
|
|
/>
|
|
<label htmlFor="featured" className="text-sm text-gray-300 cursor-pointer font-medium">
|
|
Featured Room
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Room Price
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
value={roomFormData.price}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Room Description
|
|
</label>
|
|
<textarea
|
|
value={roomFormData.description}
|
|
onChange={(e) => setRoomFormData({ ...roomFormData, description: e.target.value })}
|
|
rows={4}
|
|
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 resize-none"
|
|
placeholder="Enter the actual description for this specific room..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Max Guests (Capacity)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
value={roomFormData.capacity}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Room Size
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={roomFormData.room_size}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
View
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={roomFormData.view}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Amenities Selection */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-serif font-semibold text-[#d4af37] mb-4 flex items-center gap-2">
|
|
<div className="w-1 h-6 bg-gradient-to-b from-[#d4af37] to-[#c9a227]" />
|
|
Amenities & Features
|
|
</h3>
|
|
|
|
<div className="border border-[#d4af37]/20 rounded-lg p-4 max-h-80 overflow-y-auto bg-[#0a0a0a]/50 backdrop-blur-sm">
|
|
{availableAmenities.length === 0 ? (
|
|
<p className="text-sm text-gray-400 text-center py-4">Loading amenities...</p>
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{availableAmenities.map((amenity) => {
|
|
const isSelected = roomFormData.amenities.includes(amenity);
|
|
return (
|
|
<label
|
|
key={amenity}
|
|
className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-all duration-300 ${
|
|
isSelected
|
|
? 'bg-gradient-to-r from-[#d4af37]/20 to-[#c9a227]/20 border-2 border-[#d4af37] shadow-lg shadow-[#d4af37]/20'
|
|
: 'bg-[#1a1a1a]/50 border-2 border-[#333] hover:border-[#d4af37]/30 hover:bg-[#1a1a1a]'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={() => toggleAmenity(amenity)}
|
|
className="hidden"
|
|
/>
|
|
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all duration-300 ${
|
|
isSelected
|
|
? 'bg-[#d4af37] border-[#d4af37] shadow-lg shadow-[#d4af37]/30'
|
|
: 'border-gray-600 bg-transparent'
|
|
}`}>
|
|
{isSelected && <Check className="w-3 h-3 text-[#0f0f0f] font-bold" />}
|
|
</div>
|
|
<span className={`text-sm flex-1 transition-colors ${
|
|
isSelected
|
|
? 'font-semibold text-[#d4af37]'
|
|
: 'text-gray-400 hover:text-gray-300'
|
|
}`}>
|
|
{amenity}
|
|
</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{roomFormData.amenities.length > 0 && (
|
|
<p className="text-xs text-gray-400 font-light italic">
|
|
{roomFormData.amenities.length} amenit{roomFormData.amenities.length === 1 ? 'y' : 'ies'} selected
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-4 pt-4 border-t border-[#d4af37]/20">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowRoomModal(false)}
|
|
className="flex-1 px-6 py-3 border border-[#d4af37]/30 text-gray-300 rounded-lg hover:bg-[#d4af37]/10 hover:border-[#d4af37] transition-all duration-300 font-medium"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="flex-1 px-6 py-3 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] rounded-lg hover:from-[#f5d76e] hover:to-[#d4af37] transition-all duration-300 font-semibold shadow-lg shadow-[#d4af37]/30 hover:shadow-xl hover:shadow-[#d4af37]/40"
|
|
>
|
|
{editingRoom ? 'Update Room' : 'Add Room'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Image Upload Section - Only show when editing */}
|
|
{editingRoom && (
|
|
<div className="mt-8 pt-8 border-t border-[#d4af37]/20">
|
|
<h3 className="text-lg font-serif font-semibold text-[#d4af37] mb-6 flex items-center gap-2">
|
|
<div className="w-1 h-6 bg-gradient-to-b from-[#d4af37] to-[#c9a227]" />
|
|
<ImageIcon className="w-5 h-5" />
|
|
Room Images
|
|
</h3>
|
|
|
|
{/* Helper function to normalize image URLs */}
|
|
{(() => {
|
|
// Get API base URL from environment or default
|
|
const apiBaseUrl = import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000';
|
|
|
|
const normalizeImageUrl = (img: string): string => {
|
|
if (!img) return '';
|
|
// If already a full URL, return as-is
|
|
if (img.startsWith('http://') || img.startsWith('https://')) {
|
|
return img;
|
|
}
|
|
// Normalize relative paths
|
|
const cleanPath = img.startsWith('/') ? img : `/${img}`;
|
|
return `${apiBaseUrl}${cleanPath}`;
|
|
};
|
|
|
|
// Get all images - prioritize room images over room type images
|
|
const roomImages = editingRoom.images || [];
|
|
const roomTypeImages = editingRoom.room_type?.images || [];
|
|
|
|
// Normalize all image paths for comparison
|
|
const normalizeForComparison = (img: string): string => {
|
|
if (!img) return '';
|
|
// Extract just the path part for comparison
|
|
if (img.startsWith('http://') || img.startsWith('https://')) {
|
|
try {
|
|
const url = new URL(img);
|
|
return url.pathname;
|
|
} catch {
|
|
const match = img.match(/(\/uploads\/.*)/);
|
|
return match ? match[1] : img;
|
|
}
|
|
}
|
|
return img.startsWith('/') ? img : `/${img}`;
|
|
};
|
|
|
|
// Combine images: room images first, then room type images that aren't already in room images
|
|
const normalizedRoomImages = roomImages.map(normalizeForComparison);
|
|
const allImages = [
|
|
...roomImages,
|
|
...roomTypeImages.filter(img => {
|
|
const normalized = normalizeForComparison(img);
|
|
return !normalizedRoomImages.includes(normalized);
|
|
})
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{/* Current Images */}
|
|
{allImages.length > 0 ? (
|
|
<div className="mb-6">
|
|
<p className="text-sm text-gray-400 mb-4 font-light">
|
|
Current Images ({allImages.length}):
|
|
</p>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{allImages.map((img, index) => {
|
|
const imageUrl = normalizeImageUrl(img);
|
|
// Check if this is a room image by comparing normalized paths
|
|
const normalizedImg = normalizeForComparison(img);
|
|
const normalizedRoomImgs = roomImages.map(normalizeForComparison);
|
|
const isRoomImage = normalizedRoomImgs.includes(normalizedImg);
|
|
|
|
return (
|
|
<div key={index} className="relative group">
|
|
<div className="overflow-hidden rounded-lg border border-[#d4af37]/20
|
|
hover:border-[#d4af37] transition-all duration-300">
|
|
<img
|
|
src={imageUrl}
|
|
alt={`Room Image ${index + 1}`}
|
|
className="w-full h-32 object-cover group-hover:scale-110
|
|
transition-transform duration-300"
|
|
onError={(e) => {
|
|
// Fallback if image fails to load
|
|
(e.target as HTMLImageElement).src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iIzMzMzMzMyIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM2NjY2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj5JbWFnZSBub3QgZm91bmQ8L3RleHQ+PC9zdmc+';
|
|
}}
|
|
/>
|
|
</div>
|
|
{isRoomImage && (
|
|
<>
|
|
<div className="absolute top-2 left-2 bg-[#d4af37]/90 backdrop-blur-sm
|
|
text-[#0f0f0f] px-2 py-1 rounded text-xs font-semibold
|
|
opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
Room Image
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDeleteImage(img)}
|
|
className="absolute top-2 right-2 bg-red-600/90 backdrop-blur-sm
|
|
text-white p-2 rounded-full opacity-0 group-hover:opacity-100
|
|
transition-all duration-300 hover:bg-red-600 shadow-lg
|
|
hover:scale-110 transform"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="mb-6 p-8 bg-[#0a0a0a]/50 rounded-lg border border-[#d4af37]/10 text-center">
|
|
<ImageIcon className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
|
<p className="text-sm text-gray-400 font-light">
|
|
No images uploaded yet. Upload images below to display them here.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
})()}
|
|
|
|
{/* Upload New Images */}
|
|
<div className="space-y-4">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Add New Images (max 5 images):
|
|
</label>
|
|
<div className="flex gap-4">
|
|
<div className="flex-1">
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
multiple
|
|
onChange={handleFileSelect}
|
|
className="w-full text-sm text-gray-400 file:mr-4 file:py-3 file:px-6 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-gradient-to-r file:from-[#d4af37]/20 file:to-[#c9a227]/20 file:text-[#d4af37] file:border file:border-[#d4af37]/30 hover:file:from-[#d4af37]/30 hover:file:to-[#c9a227]/30 hover:file:border-[#d4af37] file:cursor-pointer transition-all duration-300 bg-[#0a0a0a] rounded-lg"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleUploadImages}
|
|
disabled={selectedFiles.length === 0 || uploadingImages}
|
|
className="px-6 py-3 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-lg hover:from-green-500 hover:to-green-600 disabled:from-gray-600 disabled:to-gray-700 disabled:cursor-not-allowed flex items-center gap-2 font-semibold shadow-lg shadow-green-600/30 hover:shadow-xl hover:shadow-green-600/40 transition-all duration-300"
|
|
>
|
|
<Upload className="w-4 h-4" />
|
|
{uploadingImages ? 'Uploading...' : 'Upload'}
|
|
</button>
|
|
</div>
|
|
{selectedFiles.length > 0 && (
|
|
<p className="text-sm text-gray-400 font-light italic">
|
|
{selectedFiles.length} file(s) selected
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bottom Cancel/Close Button */}
|
|
<div className="mt-8 pt-6 border-t border-[#d4af37]/20 flex justify-end">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowRoomModal(false);
|
|
resetRoomForm();
|
|
}}
|
|
className="px-8 py-3 bg-gradient-to-r from-gray-600 to-gray-700 text-white rounded-lg hover:from-gray-700 hover:to-gray-800 transition-all duration-300 font-semibold shadow-lg shadow-gray-600/30 hover:shadow-xl hover:shadow-gray-600/40 border border-gray-500/30"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Services Tab */}
|
|
{activeTab === 'services' && (
|
|
<div className="space-y-8">
|
|
{servicesLoading && <Loading />}
|
|
|
|
{/* Section Header */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex justify-between items-center">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-purple-500/10 to-violet-500/10 border border-purple-200/40">
|
|
<Wrench className="w-6 h-6 text-purple-600" />
|
|
</div>
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Service Management</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Manage hotel services and amenities
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
resetServiceForm();
|
|
setShowServiceModal(true);
|
|
}}
|
|
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
Add Service
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div className="relative group">
|
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 group-focus-within:text-purple-500 transition-colors" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search services..."
|
|
value={serviceFilters.search}
|
|
onChange={(e) => setServiceFilters({ ...serviceFilters, search: e.target.value })}
|
|
className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
|
|
/>
|
|
</div>
|
|
<select
|
|
value={serviceFilters.status}
|
|
onChange={(e) => setServiceFilters({ ...serviceFilters, status: e.target.value })}
|
|
className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-gray-700 font-medium shadow-sm hover:shadow-md cursor-pointer"
|
|
>
|
|
<option value="">All Statuses</option>
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Services Table */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900">
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Service Name</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Description</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Price</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Unit</th>
|
|
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Status</th>
|
|
<th className="px-8 py-5 text-right text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-slate-100">
|
|
{services.map((service) => (
|
|
<tr
|
|
key={service.id}
|
|
className="hover:bg-gradient-to-r hover:from-purple-50/30 hover:to-violet-50/30 transition-all duration-200 group border-b border-slate-100"
|
|
>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-semibold text-gray-900">{service.name}</div>
|
|
</td>
|
|
<td className="px-8 py-5">
|
|
<div className="text-sm text-gray-700 max-w-xs truncate">{service.description}</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm font-bold bg-gradient-to-r from-emerald-600 to-emerald-700 bg-clip-text text-transparent">
|
|
{formatCurrency(service.price)}
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
<div className="text-sm text-gray-600">{service.unit}</div>
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap">
|
|
{getServiceStatusBadge(service.status)}
|
|
</td>
|
|
<td className="px-8 py-5 whitespace-nowrap text-right">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={() => handleEditService(service)}
|
|
className="p-2 rounded-lg text-blue-600 hover:text-blue-700 hover:bg-blue-50 transition-all duration-200 shadow-sm hover:shadow-md border border-blue-200 hover:border-blue-300"
|
|
title="Edit"
|
|
>
|
|
<Edit className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteService(service.id)}
|
|
className="p-2 rounded-lg text-rose-600 hover:text-rose-700 hover:bg-rose-50 transition-all duration-200 shadow-sm hover:shadow-md border border-rose-200 hover:border-rose-300"
|
|
title="Delete"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<Pagination
|
|
currentPage={serviceCurrentPage}
|
|
totalPages={serviceTotalPages}
|
|
onPageChange={setServiceCurrentPage}
|
|
totalItems={serviceTotalItems}
|
|
itemsPerPage={serviceItemsPerPage}
|
|
/>
|
|
</div>
|
|
|
|
{/* Service Modal */}
|
|
{showServiceModal && (
|
|
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-hidden border border-slate-200">
|
|
{/* Modal Header */}
|
|
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-6 py-5 border-b border-slate-700">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-amber-100">
|
|
{editingService ? 'Update Service' : 'Add New Service'}
|
|
</h2>
|
|
<p className="text-amber-200/80 text-sm font-light mt-1">
|
|
{editingService ? 'Modify service information' : 'Create a new service'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setShowServiceModal(false);
|
|
resetServiceForm();
|
|
}}
|
|
className="w-10 h-10 flex items-center justify-center rounded-xl text-amber-100 hover:text-white hover:bg-slate-700/50 transition-all duration-200 border border-slate-600 hover:border-amber-400"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal Content */}
|
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
|
|
<form onSubmit={handleServiceSubmit} className="space-y-5">
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2">
|
|
Service Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={serviceFormData.name}
|
|
onChange={(e) => setServiceFormData({ ...serviceFormData, name: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
value={serviceFormData.description}
|
|
onChange={(e) => setServiceFormData({ ...serviceFormData, description: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2">
|
|
Price
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={serviceFormData.price}
|
|
onChange={(e) => setServiceFormData({ ...serviceFormData, price: parseFloat(e.target.value) || 0 })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
required
|
|
min="0"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2">
|
|
Unit
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={serviceFormData.unit}
|
|
onChange={(e) => setServiceFormData({ ...serviceFormData, unit: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
placeholder="e.g: time, hour, day..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2">
|
|
Status
|
|
</label>
|
|
<select
|
|
value={serviceFormData.status}
|
|
onChange={(e) => setServiceFormData({ ...serviceFormData, status: e.target.value as any })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-slate-700 font-medium shadow-sm cursor-pointer"
|
|
>
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex gap-3 pt-4 border-t border-slate-200">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowServiceModal(false);
|
|
resetServiceForm();
|
|
}}
|
|
className="flex-1 px-6 py-3 border-2 border-slate-300 rounded-xl text-slate-700 font-semibold hover:bg-slate-50 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="flex-1 px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
>
|
|
{editingService ? 'Update' : 'Create'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ReceptionDashboardPage;
|
|
|