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