// app.jsx — Main shell for Sao Mai Hotel Booking const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "blockStyle": "default", "daysToShow": 14, "primaryColor": "#1a73e8", "showWeekendShade": true }/*EDITMODE-END*/; function App() { const data = window.HotelData; const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); const [collapsed, setCollapsed] = React.useState(false); const [active, setActive] = React.useState('calendar'); const [view, setView] = React.useState('timeline'); // Anchor date for timeline / week views (Mon of current week) const today = data.today; const initialAnchor = React.useMemo(() => { // Start a few days before today so today is visible mid-screen return window.HotelData.addDays(today, -3); }, [today]); const [anchorDate, setAnchorDate] = React.useState(initialAnchor); const [miniDate, setMiniDate] = React.useState(today); const [search, setSearch] = React.useState(''); const [filters, setFilters] = React.useState({ statuses: ['website', 'walkin', 'pending', 'cancelled', 'maintenance'], types: ['Standard', 'Deluxe', 'Suite'], }); const [bookings, setBookings] = React.useState(data.bookings); const [guests, setGuests] = React.useState(data.guests); const [rooms, setRooms] = React.useState(data.rooms); const [openBooking, setOpenBooking] = React.useState(null); const [newSlot, setNewSlot] = React.useState(null); // {roomId, date} | true const [orderingFor, setOrderingFor] = React.useState(null); // booking const [editingRoom, setEditingRoom] = React.useState(null); // {} for new, room obj for edit const [toasts, setToasts] = React.useState([]); const [dark, setDark] = React.useState(false); // Toast helper const toast = (msg, kind = 'success') => { const id = Date.now() + Math.random(); setToasts(t => [...t, { id, msg, kind }]); setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3000); }; // Apply primary color tweak React.useEffect(() => { document.documentElement.style.setProperty('--primary', tweaks.primaryColor); }, [tweaks.primaryColor]); // Apply dark mode React.useEffect(() => { document.documentElement.classList.toggle('dark', dark); }, [dark]); // Keep window.HotelData.rooms in sync so child components using static refs still work React.useEffect(() => { window.HotelData.rooms = rooms; }, [rooms]); // Sync miniDate → anchor (when user clicks a date) const setMiniAndAnchor = (d) => { setMiniDate(d); if (view === 'timeline') setAnchorDate(window.HotelData.addDays(d, -3)); else setAnchorDate(d); }; // Quick stats for today const stats = React.useMemo(() => { const todayMidnight = startOfDay(today); const checkIns = bookings.filter(b => sameDay(b.checkIn, today) && b.status !== 'cancelled' && b.status !== 'maintenance').length; const checkOuts = bookings.filter(b => sameDay(b.checkOut, today) && b.status !== 'cancelled' && b.status !== 'maintenance').length; const occupied = bookings.filter(b => { if (b.status === 'cancelled' || b.status === 'maintenance') return false; return todayMidnight >= startOfDay(b.checkIn) && todayMidnight < startOfDay(b.checkOut); }).length; const total = rooms.length; const available = total - occupied - rooms.filter(r => r.status === 'maintenance' || r.status === 'cleaning').length; const occupancy = Math.round((occupied / total) * 100); const revenue = bookings .filter(b => sameDay(b.checkIn, today) && b.status !== 'cancelled') .reduce((sum, b) => { const t = computeBookingTotal(b, rooms, data.serviceCatalog, data.inStayMenu); return sum + t.total; }, 0); return { available, checkIns, checkOuts, occupancy, revenue }; }, [bookings, today, data]); // Searched bookings (highlight not implemented but filter) const filteredBookings = React.useMemo(() => { if (!search) return bookings; const q = search.toLowerCase(); return bookings.filter(b => { const g = b.guestId ? findGuest(guests, b.guestId) : null; const r = findRoom(rooms, b.roomId); return ( (g?.name || '').toLowerCase().includes(q) || (g?.phone || '').toLowerCase().includes(q) || (r?.number || '').toLowerCase().includes(q) || b.code.toLowerCase().includes(q) ); }); }, [bookings, search, guests, rooms]); // Save new booking const handleSaveBooking = (b) => { let newGuests = guests; if (b._newGuest) { newGuests = [...guests, { ...b._newGuest, id: b.guestId }]; setGuests(newGuests); } delete b._newGuest; setBookings([...bookings, b]); setNewSlot(null); toast('Đã tạo đặt phòng ' + b.code); }; // Booking action const handleAction = (action) => { if (!openBooking) return; if (action === 'checkin') { setBookings(bs => bs.map(b => b.id === openBooking.id ? { ...b, checkedIn: true } : b)); setOpenBooking(b => ({ ...b, checkedIn: true })); toast('Đã nhận phòng ' + openBooking.code); } else if (action === 'checkout') { setBookings(bs => bs.map(b => b.id === openBooking.id ? { ...b, checkedIn: false, status: b.status } : b)); toast('Đã trả phòng ' + openBooking.code + ' · ' + VND(computeBookingTotal(openBooking, rooms, data.serviceCatalog, data.inStayMenu).total)); setOpenBooking(null); } else if (action === 'cancel') { if (confirm('Hủy đặt phòng ' + openBooking.code + '?')) { setBookings(bs => bs.map(b => b.id === openBooking.id ? { ...b, status: 'cancelled' } : b)); toast('Đã hủy đặt phòng'); setOpenBooking(null); } } }; // In-stay orders const handleSubmitOrder = (newOrders) => { if (!orderingFor) return; const bookingId = orderingFor.id; setBookings(bs => bs.map(b => { if (b.id !== bookingId) return b; const existing = b.orders || []; return { ...b, orders: [...existing, ...newOrders] }; })); // Reflect in open drawer setOpenBooking(ob => ob && ob.id === bookingId ? { ...ob, orders: [...(ob.orders || []), ...newOrders] } : ob); setOrderingFor(null); const count = newOrders.reduce((s, o) => s + o.qty, 0); const room = findRoom(rooms, orderingFor.roomId); toast(`Đã gửi ${count} món tới Phòng ${room?.number} · Đang chuẩn bị…`); }; const handleMarkOrderServed = (orderId) => { if (!openBooking) return; const bookingId = openBooking.id; setBookings(bs => bs.map(b => { if (b.id !== bookingId) return b; return { ...b, orders: (b.orders || []).map(o => o.id === orderId ? { ...o, status: 'served' } : o), }; })); setOpenBooking(ob => ob && { ...ob, orders: (ob.orders || []).map(o => o.id === orderId ? { ...o, status: 'served' } : o), }); toast('Đã đánh dấu phục vụ xong'); }; // Room CRUD const handleSaveRoom = (room) => { const exists = rooms.some(r => r.id === room.id); if (exists) { setRooms(rs => rs.map(r => r.id === room.id ? room : r)); toast(`Đã cập nhật ${room.name || 'Phòng ' + room.number}`); } else { setRooms(rs => [...rs, room]); toast(`Đã thêm ${room.name || 'Phòng ' + room.number}`); } setEditingRoom(null); }; const handleDeleteRoom = (roomId) => { setRooms(rs => rs.filter(r => r.id !== roomId)); setBookings(bs => bs.filter(b => b.roomId !== roomId)); toast('Đã xóa phòng'); setEditingRoom(null); }; return (
setCollapsed(!collapsed)} active={active} onNav={setActive} onNewBooking={() => setNewSlot({})} miniDate={miniDate} setMiniDate={setMiniAndAnchor} filters={filters} setFilters={setFilters} rooms={rooms} bookings={bookings} today={today} stats={stats} />
setNewSlot({})} dark={dark} setDark={setDark} onCalendar={active === 'calendar'} pageTitle={ active === 'rooms' ? 'Quản lý phòng' : active === 'bookings' ? 'Danh sách đặt phòng' : active === 'invoices' ? 'Hóa đơn' : active === 'services' ? 'Dịch vụ' : active === 'reports' ? 'Báo cáo' : '' } /> {active === 'calendar' && view === 'timeline' && ( setNewSlot({ roomId, date })} onOpenBooking={(id) => setOpenBooking(bookings.find(b => b.id === id))} blockStyle={tweaks.blockStyle} daysToShow={tweaks.daysToShow} /> )} {active === 'calendar' && view !== 'timeline' && (
Chế độ xem "{view === 'day' ? 'Ngày' : view === 'week' ? 'Tuần' : 'Tháng'}" — chế độ Sơ đồ là góc nhìn chính · Bấm "Sơ đồ" để xem
)} {active === 'rooms' && ( setOpenBooking(b)} onNewBooking={(roomId) => setNewSlot({ roomId, date: today })} onAddRoom={() => setEditingRoom({})} onEditRoom={(r) => setEditingRoom(r)} onRoomAction={(room, action) => { if (action === 'history') { const last = bookings.filter(b => b.roomId === room.id).sort((a, b) => b.checkIn - a.checkIn)[0]; if (last) setOpenBooking(last); else toast('Phòng này chưa có lịch sử đặt'); } else if (action === 'done_cleaning') { setRooms(rs => rs.map(r => r.id === room.id ? { ...r, status: 'available' } : r)); toast(`${room.name} đã dọn xong · sẵn sàng đón khách`); } else if (action === 'block') { setRooms(rs => rs.map(r => r.id === room.id ? { ...r, status: 'maintenance' } : r)); toast(`Đã khóa ${room.name} tạm thời`); } else if (action === 'reopen') { setRooms(rs => rs.map(r => r.id === room.id ? { ...r, status: 'available' } : r)); toast(`Đã mở lại ${room.name}`); } }} /> )} {active !== 'calendar' && active !== 'rooms' && (
Trang "{active === 'bookings' ? 'Danh sách đặt phòng' : active === 'invoices' ? 'Hóa đơn' : active === 'services' ? 'Dịch vụ' : 'Báo cáo'}" — đang xây dựng
)}
{newSlot && ( setNewSlot(null)} onSave={handleSaveBooking} /> )} {openBooking && ( setOpenBooking(null)} onAction={handleAction} onEdit={() => { setNewSlot({ roomId: openBooking.roomId, date: openBooking.checkIn }); setOpenBooking(null); }} onOrder={() => setOrderingFor(openBooking)} onMarkOrderServed={handleMarkOrderServed} /> )} {orderingFor && ( setOrderingFor(null)} onSubmit={handleSubmitOrder} /> )} {editingRoom && ( setEditingRoom(null)} onSave={handleSaveRoom} onDelete={handleDeleteRoom} /> )} {/* Tweaks panel */} setTweak('blockStyle', v)} /> setTweak('daysToShow', v)} /> setTweak('primaryColor', v)} /> {/* Toasts */}
{toasts.map(t => (
{t.msg}
))}
); } window.App = App;