import React, { useState, useEffect, useCallback } from 'react'; import { LogIn, LogOut, Search, User, Hotel, CheckCircle, AlertCircle, FileText, CreditCard, Printer, Sparkles, ChevronRight, Eye, XCircle, Loader2, Plus, Edit, Trash2, X, Image as ImageIcon, Check, Calendar, Wrench } from 'lucide-react'; import bookingService, { Booking } from '../../features/bookings/services/bookingService'; import roomService, { Room } from '../../features/rooms/services/roomService'; import serviceService, { Service } from '../../features/hotel_services/services/serviceService'; import userService from '../../features/auth/services/userService'; import { toast } from 'react-toastify'; import Loading from '../../shared/components/Loading'; import CurrencyIcon from '../../shared/components/CurrencyIcon'; import Pagination from '../../shared/components/Pagination'; import apiClient from '../../shared/services/apiClient'; import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { parseDateLocal } from '../../shared/utils/format'; import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal'; import { logger } from '../../shared/utils/logger'; type ReceptionTab = 'overview' | 'check-in' | 'check-out' | 'walk-in' | '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('overview'); const [checkInBookingNumber, setCheckInBookingNumber] = useState(''); const [checkInBooking, setCheckInBooking] = useState(null); const [checkInLoading, setCheckInLoading] = useState(false); const [checkInSearching, setCheckInSearching] = useState(false); const [actualRoomNumber, setActualRoomNumber] = useState(''); const [guests, setGuests] = useState([{ name: '', id_number: '', phone: '' }]); const [extraPersons, setExtraPersons] = useState(0); const [children, setChildren] = useState(0); const [additionalFee, setAdditionalFee] = useState(0); const [checkOutBookingNumber, setCheckOutBookingNumber] = useState(''); const [checkOutBooking, setCheckOutBooking] = useState(null); const [checkOutLoading, setCheckOutLoading] = useState(false); const [checkOutSearching, setCheckOutSearching] = useState(false); const [checkOutServices, setCheckOutServices] = useState([]); const [paymentMethod, setPaymentMethod] = useState<'cash' | 'stripe'>('cash'); const [discount, setDiscount] = useState(0); const [showInvoice, setShowInvoice] = useState(false); // Walk-in booking state const [walkInLoading, setWalkInLoading] = useState(false); const [walkInSearchingRooms, setWalkInSearchingRooms] = useState(false); const [walkInAvailableRooms, setWalkInAvailableRooms] = useState([]); const [walkInForm, setWalkInForm] = useState({ guestName: '', guestEmail: '', guestPhone: '', guestIdNumber: '', selectedRoomId: '', checkInDate: new Date().toISOString().split('T')[0], checkOutDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0], numGuests: 1, numChildren: 0, specialRequests: '', paymentMethod: 'cash' as 'cash' | 'stripe', paymentStatus: 'unpaid' as 'unpaid' | 'deposit' | 'full', }); const [walkInTotalPrice, setWalkInTotalPrice] = useState(0); const [walkInSelectedRoom, setWalkInSelectedRoom] = useState(null); const [bookings, setBookings] = useState([]); const [bookingsLoading, setBookingsLoading] = useState(true); const [selectedBooking, setSelectedBooking] = useState(null); const [showBookingDetailModal, setShowBookingDetailModal] = useState(false); const [updatingBookingId, setUpdatingBookingId] = useState(null); const [cancellingBookingId, setCancellingBookingId] = useState(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; const [showCreateBookingModal, setShowCreateBookingModal] = useState(false); const [rooms, setRooms] = useState([]); const [roomsLoading, setRoomsLoading] = useState(true); const [showRoomModal, setShowRoomModal] = useState(false); const [editingRoom, setEditingRoom] = useState(null); const [selectedRooms, setSelectedRooms] = useState([]); 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([]); const [roomTypes, setRoomTypes] = useState>([]); const [uploadingImages, setUploadingImages] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const [services, setServices] = useState([]); const [servicesLoading, setServicesLoading] = useState(true); const [showServiceModal, setShowServiceModal] = useState(false); const [editingService, setEditingService] = useState(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', }); 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; }; useEffect(() => { const extraPersonFee = extraPersons * 200000; const childrenFee = children * 100000; const total = extraPersonFee + childrenFee; setAdditionalFee(total); }, [extraPersons, children]); 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); } }; 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 calculateTotalPaid = () => { if (!checkOutBooking?.payments) return 0; return checkOutBooking.payments .filter(payment => payment.payment_status === 'completed') .reduce((sum, payment) => sum + (payment.amount || 0), 0); }; const calculateSubtotal = () => { return calculateRoomFee() + calculateServiceFee() + calculateCheckOutAdditionalFee(); }; const calculateTotal = () => { return calculateSubtotal() - discount; }; const calculateRemaining = () => { const total = calculateTotal(); const totalPaid = calculateTotalPaid(); return total - totalPaid; }; 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); }; // Walk-in booking handlers const handleWalkInSearchRooms = async () => { if (!walkInForm.checkInDate || !walkInForm.checkOutDate) { toast.error('Please select check-in and check-out dates'); return; } try { setWalkInSearchingRooms(true); const response = await roomService.searchAvailableRooms({ from: walkInForm.checkInDate, to: walkInForm.checkOutDate, limit: 50, }); setWalkInAvailableRooms(response.data.rooms || []); if (response.data.rooms && response.data.rooms.length === 0) { toast.warning('No available rooms for selected dates'); } } catch (error: any) { toast.error('Failed to search available rooms'); logger.error('Error searching rooms', error); } finally { setWalkInSearchingRooms(false); } }; useEffect(() => { if (walkInForm.checkInDate && walkInForm.checkOutDate && walkInForm.selectedRoomId) { const selectedRoom = walkInAvailableRooms.find(r => r.id.toString() === walkInForm.selectedRoomId); if (selectedRoom) { setWalkInSelectedRoom(selectedRoom); const checkIn = new Date(walkInForm.checkInDate); const checkOut = new Date(walkInForm.checkOutDate); const nights = Math.ceil((checkOut.getTime() - checkIn.getTime()) / (1000 * 60 * 60 * 24)); const roomPrice = selectedRoom.room_type?.base_price || selectedRoom.price || 0; setWalkInTotalPrice(roomPrice * nights); } } }, [walkInForm.checkInDate, walkInForm.checkOutDate, walkInForm.selectedRoomId, walkInAvailableRooms]); const handleWalkInBooking = async () => { if (!walkInForm.guestName || !walkInForm.guestPhone || !walkInForm.selectedRoomId) { toast.error('Please fill in all required fields'); return; } if (!walkInSelectedRoom) { toast.error('Please select a room'); return; } try { setWalkInLoading(true); // First, create or find user let userId: number; try { const userResponse = await userService.getUsers({ search: walkInForm.guestEmail || walkInForm.guestPhone, role: 'customer', limit: 1, }); if (userResponse.data?.users && userResponse.data.users.length > 0) { userId = userResponse.data.users[0].id; } else { // Create new user for walk-in const createUserResponse = await userService.createUser({ full_name: walkInForm.guestName, email: walkInForm.guestEmail || `${walkInForm.guestPhone}@walkin.local`, phone_number: walkInForm.guestPhone, password: 'temp123', // Temporary password role: 'customer', }); userId = createUserResponse.data.user.id; } } catch (error: any) { toast.error('Failed to create/find guest profile'); logger.error('Error creating user', error); return; } // Create booking const bookingData = { user_id: userId, room_id: parseInt(walkInForm.selectedRoomId), check_in_date: walkInForm.checkInDate, check_out_date: walkInForm.checkOutDate, guest_count: walkInForm.numGuests, total_price: walkInTotalPrice, status: 'confirmed', payment_method: walkInForm.paymentMethod, guest_info: { full_name: walkInForm.guestName, email: walkInForm.guestEmail || `${walkInForm.guestPhone}@walkin.local`, phone: walkInForm.guestPhone, }, notes: walkInForm.specialRequests || undefined, }; await bookingService.adminCreateBooking(bookingData); toast.success('Walk-in booking created successfully!'); // Reset form setWalkInForm({ guestName: '', guestEmail: '', guestPhone: '', guestIdNumber: '', selectedRoomId: '', checkInDate: new Date().toISOString().split('T')[0], checkOutDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0], numGuests: 1, numChildren: 0, specialRequests: '', paymentMethod: 'cash', paymentStatus: 'unpaid', }); setWalkInAvailableRooms([]); setWalkInSelectedRoom(null); setWalkInTotalPrice(0); // Optionally switch to check-in tab setActiveTab('check-in'); } catch (error: any) { toast.error(error.response?.data?.message || 'Failed to create walk-in booking'); logger.error('Error creating walk-in booking', error); } finally { setWalkInLoading(false); } }; const fetchBookings = useCallback(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); } }, [bookingFilters.search, bookingFilters.status, bookingCurrentPage]); useEffect(() => { setBookingCurrentPage(1); }, [bookingFilters.search, bookingFilters.status]); useEffect(() => { if (activeTab === 'bookings') { fetchBookings(); } }, [activeTab, fetchBookings]); 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 = { 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: '❌ Canceled', border: 'border-rose-200' }, }; const badge = badges[status] || badges.pending; return ( {badge.label} ); }; const fetchAvailableAmenities = useCallback(async () => { try { const response = await roomService.getAmenities(); if (response.data?.amenities) { setAvailableAmenities(response.data.amenities); } } catch (error) { logger.error('Failed to fetch amenities', error); } }, []); const fetchRooms = useCallback(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(); 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); } }, [roomFilters.search, roomFilters.status, roomFilters.type, roomCurrentPage]); useEffect(() => { setRoomCurrentPage(1); setSelectedRooms([]); }, [roomFilters.search, roomFilters.status, roomFilters.type]); useEffect(() => { if (activeTab === 'rooms') { fetchRooms(); fetchAvailableAmenities(); } }, [activeTab, fetchRooms, fetchAvailableAmenities]); useEffect(() => { if (activeTab !== 'rooms') return; const fetchAllRoomTypes = async () => { try { const response = await roomService.getRooms({ limit: 100, page: 1 }); const allUniqueRoomTypes = new Map(); 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) { logger.error(`Failed to fetch page ${page}`, err); } } } if (allUniqueRoomTypes.size > 0) { const roomTypesList = Array.from(allUniqueRoomTypes.values()); setRoomTypes(roomTypesList); setRoomFormData(prev => { if (!editingRoom && prev.room_type_id === 1 && roomTypesList.length > 0) { return { ...prev, room_type_id: roomTypesList[0].id }; } return prev; }); } } catch (err) { logger.error('Failed to fetch room types', err); } }; fetchAllRoomTypes(); }, [activeTab, editingRoom]); 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) { logger.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) { logger.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) => { 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) { logger.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 = { 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 ( {badge.label} ); }; const fetchServices = useCallback(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); } }, [serviceFilters.search, serviceFilters.status, serviceCurrentPage]); useEffect(() => { setServiceCurrentPage(1); }, [serviceFilters.search, serviceFilters.status]); useEffect(() => { if (activeTab === 'services') { fetchServices(); } }, [activeTab, fetchServices]); 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' ? ( Active ) : ( Inactive ); }; 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: 'walk-in' as ReceptionTab, label: 'Walk-in Booking', icon: Plus }, { 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 (
{}

Reception Dashboard

Manage guest check-in and check-out processes

{}
{tabs.map((tab) => { const Icon = tab.icon; const isActive = activeTab === tab.id; return ( ); })}
{} {activeTab === 'overview' && (
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" >

Check-in

Process guest check-in and room assignment

Start Check-in
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" >

Check-out

Process guest check-out and payment collection

Start Check-out
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" >

Bookings

Manage and track all hotel bookings

Manage Bookings
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" >

Rooms

Manage hotel room information and availability

Manage Rooms
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" >

Services

Manage hotel services and amenities

Manage Services
)} {} {activeTab === 'check-in' && (
{checkInLoading && ( )} {}

Check-in

Customer check-in process and room assignment

{}

1
Search Booking

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" />
{} {checkInBooking && ( <>

Booking Information

Booking Number: {checkInBooking.booking_number}
Customer: {checkInBooking.user?.full_name}
Email: {checkInBooking.user?.email}
Phone: {checkInBooking.user?.phone_number || 'N/A'}
Room Type: {checkInBooking.room?.room_type?.name || 'N/A'}
Check-in: {checkInBooking.check_in_date ? parseDateLocal(checkInBooking.check_in_date).toLocaleDateString('en-US') : 'N/A'}
Check-out: {checkInBooking.check_out_date ? parseDateLocal(checkInBooking.check_out_date).toLocaleDateString('en-US') : 'N/A'}
Number of Guests: {checkInBooking.guest_count} guest(s)
{}

Payment Information

Payment Method: {checkInBooking.payment_method === 'cash' ? '💵 Pay at Hotel' : checkInBooking.payment_method === 'stripe' ? '💳 Stripe (Card)' : checkInBooking.payment_method === 'paypal' ? '💳 PayPal' : checkInBooking.payment_method || 'N/A'}
Payment Status: {checkInBooking.payment_status === 'paid' ? '✅ Paid' : checkInBooking.payment_status === 'refunded' ? '💰 Refunded' : '❌ Unpaid'}
{(() => { const completedPayments = checkInBooking.payments?.filter( (p) => p.payment_status === 'completed' ) || []; const amountPaid = completedPayments.reduce( (sum, p) => sum + (p.amount || 0), 0 ); const remainingDue = checkInBooking.total_price - amountPaid; const hasPayments = completedPayments.length > 0; return ( <>
Total Price: {formatCurrency(checkInBooking.total_price)}
{hasPayments && ( <>
Amount Paid: {formatCurrency(amountPaid)}
{remainingDue > 0 && (
Remaining Due: {formatCurrency(remainingDue)}
)} {completedPayments.length > 0 && (

Payment Details:

{completedPayments.map((payment, idx) => (
• {formatCurrency(payment.amount)} via {payment.payment_method === 'stripe' ? 'Stripe' : payment.payment_method === 'paypal' ? 'PayPal' : payment.payment_method || 'Cash'} {payment.payment_type === 'deposit' && ' (Deposit 20%)'} {payment.transaction_id && ` - ${payment.transaction_id}`}
))}
)} )} ); })()}
{checkInBooking.status !== 'confirmed' && (

Warning

Booking status: {checkInBooking.status}. Only check-in confirmed bookings.

)}
{}

Assign Actual Room Number

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" />

Enter the actual room number to assign to the guest

{}

Guest Information

{guests.map((guest, index) => (

{index === 0 ? 'Main Guest' : `Guest ${index + 1}`} {index === 0 && *}

{index > 0 && ( )}
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" />
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" />
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" />
))}
{}

Additional Fees (if any)

{ 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" />

€50/person

{ 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" />

€25/child

{formatCurrency(additionalFee)}
{}

Confirm Check-in

Guest: {checkInBooking.user?.full_name} | Room: {actualRoomNumber || 'Not assigned'} {additionalFee > 0 && ( <> | Additional Fee: {formatCurrency(additionalFee)} )}

)} {} {!checkInBooking && !checkInSearching && (

No booking selected

Please enter booking number above to start check-in process

)}
)} {} {activeTab === 'check-out' && (
{checkOutLoading && ( )} {}

Check-out

Payment and check-out process for guests

{} {!showInvoice && (

1
Search Booking

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" />
)} {} {checkOutBooking && !showInvoice && ( <> {}

Booking Information

Booking number: {checkOutBooking.booking_number}
Customer: {checkOutBooking.user?.full_name}
Room number: {checkOutBooking.room?.room_number}
Check-in: {checkOutBooking.check_in_date ? parseDateLocal(checkOutBooking.check_in_date).toLocaleDateString('en-US') : 'N/A'}
Check-out: {checkOutBooking.check_out_date ? parseDateLocal(checkOutBooking.check_out_date).toLocaleDateString('en-US') : 'N/A'}
Nights: {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)
{}

Invoice Details

{}

Room Fee

{checkOutBooking.room?.room_type?.name || 'Room'} {formatCurrency(calculateRoomFee())}
{} {checkOutServices.length > 0 && (

Services Used

{checkOutServices.map((service, index) => (
{service.service_name} (x{service.quantity}) {formatCurrency(service.total)}
))}
Total services: {formatCurrency(calculateServiceFee())}
)} {} {calculateCheckOutAdditionalFee() > 0 && (

Additional Fees

Extra person/children fee {formatCurrency(calculateCheckOutAdditionalFee())}
)} {}

Discount

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" />
{}
Subtotal: {formatCurrency(calculateSubtotal())}
{discount > 0 && (
Discount: -{formatCurrency(discount)}
)}
Total: {formatCurrency(calculateTotal())}
{calculateTotalPaid() > 0 && (
Total paid: -{formatCurrency(calculateTotalPaid())}
)}
Remaining payment: {formatCurrency(calculateRemaining())}
{}

Payment Method

{}

Confirm Check-out

Total payment: {formatCurrency(calculateRemaining())}

)} {} {showInvoice && checkOutBooking && (

PAYMENT INVOICE

Check-out successful

Booking number:

{checkOutBooking.booking_number}

Check-out date:

{new Date().toLocaleString('en-US')}

Customer:

{checkOutBooking.user?.full_name}

Payment method:

{paymentMethod === 'cash' ? 'Cash' : 'Stripe'}

Total payment: {formatCurrency(calculateRemaining())}
)} {} {!checkOutBooking && !checkOutSearching && !showInvoice && (

No booking selected

Please enter booking number to start check-out process

)}
)} {} {activeTab === 'walk-in' && (
{walkInLoading && ( )} {}

Walk-in Booking

Quick booking creation for walk-in guests

{}
{}

1
Guest Information

setWalkInForm({ ...walkInForm, guestName: e.target.value })} className="w-full px-4 py-3 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="Guest full name" />
setWalkInForm({ ...walkInForm, guestPhone: e.target.value })} className="w-full px-4 py-3 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="+1234567890" />
setWalkInForm({ ...walkInForm, guestEmail: e.target.value })} className="w-full px-4 py-3 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="guest@example.com" />
setWalkInForm({ ...walkInForm, guestIdNumber: e.target.value })} className="w-full px-4 py-3 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="ID/Passport number" />

2
Booking Details

setWalkInForm({ ...walkInForm, checkInDate: e.target.value })} min={new Date().toISOString().split('T')[0]} className="w-full px-4 py-3 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" />
setWalkInForm({ ...walkInForm, checkOutDate: e.target.value })} min={walkInForm.checkInDate || new Date().toISOString().split('T')[0]} className="w-full px-4 py-3 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" />
setWalkInForm({ ...walkInForm, numGuests: parseInt(e.target.value) || 1 })} className="w-full px-4 py-3 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" />
setWalkInForm({ ...walkInForm, numChildren: parseInt(e.target.value) || 0 })} className="w-full px-4 py-3 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" />