// timeline.jsx — Timeline view: rooms x days grid with booking bars // Click empty cell → onNewSlot(roomId, date) // Click booking bar → onOpenBooking(id) function Timeline({ anchorDate, today, rooms, bookings, guests, filters, onNewSlot, onOpenBooking, blockStyle = 'default', // default | outline | soft | gradient daysToShow = 14, }) { const [tip, setTip] = React.useState(null); // {b, x, y} // Build day list const days = React.useMemo(() => { const arr = []; for (let i = 0; i < daysToShow; i++) { arr.push(window.HotelData.addDays(anchorDate, i)); } return arr; }, [anchorDate, daysToShow]); // Filter rooms const visibleRooms = React.useMemo(() => { return rooms.filter(r => { const t = r.type.startsWith('Suite') ? 'Suite' : r.type; if (filters.types.length && !filters.types.includes(t)) return false; return true; }); }, [rooms, filters.types]); // Group rooms by floor const grouped = React.useMemo(() => { const g = {}; visibleRooms.forEach(r => { if (!g[r.floor]) g[r.floor] = []; g[r.floor].push(r); }); return g; }, [visibleRooms]); // Filter bookings const visibleBookings = React.useMemo(() => { return bookings.filter(b => filters.statuses.includes(b.status)); }, [bookings, filters.statuses]); // Pre-compute width / today position const COL = 110; // sync with --col-w const ROOM_COL = 200; const totalWidth = ROOM_COL + COL * daysToShow; // Today line position const todayIdx = daysBetween(anchorDate, today); const showTodayLine = todayIdx >= 0 && todayIdx < daysToShow; const todayX = ROOM_COL + COL * todayIdx + (COL * 0.5); // place at noon // Booking → x, w, row const renderBookingBars = (roomId, roomRowIdx) => { const bks = visibleBookings.filter(b => b.roomId === roomId); return bks.map(b => { const startIdx = daysBetween(anchorDate, b.checkIn); const endIdx = daysBetween(anchorDate, b.checkOut); // Allow off-screen bookings to be clipped const visStart = Math.max(0, startIdx); const visEnd = Math.min(daysToShow, endIdx); if (visEnd <= 0 || visStart >= daysToShow) return null; // Inset by ~14px on each end of an unclipped edge for natural check-in/out gap const clipL = startIdx < 0; const clipR = endIdx > daysToShow; // Position: cell start + half day at check-in side, end at half day before checkout const leftPx = visStart * COL + (clipL ? 0 : COL * 0.5); const rightPx = visEnd * COL - (clipR ? 0 : COL * 0.5); const widthPx = Math.max(40, rightPx - leftPx); const guest = b.guestId ? findGuest(guests, b.guestId) : null; const isMaint = b.status === 'maintenance'; const isCancelled = b.status === 'cancelled'; const styleClass = blockStyle === 'default' ? 'style-default' : blockStyle === 'outline' ? 'style-outline' : blockStyle === 'soft' ? 'style-soft' : 'style-gradient'; const label = isMaint ? '🔧 ' + (b.note ? b.note.split('.')[0] : 'Bảo trì') : guest ? guest.name : 'Khách'; return (