From 8312d2372e0e3139be050364bcd5d1bd12a1518a Mon Sep 17 00:00:00 2001 From: HotelSync Date: Mon, 23 Mar 2026 16:03:17 +0300 Subject: [PATCH] fix: maintenance overlay + context menu UX improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Calendar overlay: normalize date strings (slice 0-10) to handle full ISO timestamps from Postgres; fix off-by-one (to = exclusive); stronger stripe visibility; guard against vFrom >= vTo - Context menu: dateFrom defaults to today, indefinite defaults to false (both date inputs shown immediately) - DateRangeForm: extracted DateInput component with CalendarDays icon that triggers native date picker - HK status order: clean → inspect → cleaning → dirty Co-Authored-By: Claude Sonnet 4.6 --- src/components/calendar/BookingCalendar.tsx | 36 +++++++------- src/components/rooms/RoomContextMenu.tsx | 54 +++++++++++++-------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx index 8301110..6ee6a67 100644 --- a/src/components/calendar/BookingCalendar.tsx +++ b/src/components/calendar/BookingCalendar.tsx @@ -215,12 +215,14 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook if (draftRoom && (draftRoom.status === 'maintenance' || draftRoom.status === 'blocked')) { const bookIn = new Date(checkIn + 'T00:00:00') const bookOut = new Date(checkOut + 'T00:00:00') - const mFrom = draftRoom.maintenanceFrom ? new Date(draftRoom.maintenanceFrom + 'T00:00:00') : null - const mTo = draftRoom.maintenanceTo ? new Date(draftRoom.maintenanceTo + 'T00:00:00') : null + const mFrom = draftRoom.maintenanceFrom ? new Date(draftRoom.maintenanceFrom.slice(0, 10) + 'T00:00:00') : null + const mTo = draftRoom.maintenanceTo ? new Date(draftRoom.maintenanceTo.slice(0, 10) + 'T00:00:00') : null const overlaps = (!mFrom || bookOut > mFrom) && (!mTo || bookIn < mTo) if (overlaps) { - const period = mFrom - ? ` (${draftRoom.maintenanceFrom}${mTo ? ` — ${draftRoom.maintenanceTo}` : ' — без срока'})` + const fromStr = draftRoom.maintenanceFrom?.slice(0, 10) + const toStr = draftRoom.maintenanceTo?.slice(0, 10) + const period = fromStr + ? ` (${fromStr}${toStr ? ` — ${toStr}` : ' — без срока'})` : ' (без срока)' setMaintenanceWarningDraft({ draft, roomStatus: draftRoom.status, period }) setDragStart(null); setDragEnd(null); setDraft(null) @@ -522,16 +524,16 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
{/* Maintenance / blocked overlay — with optional date range */} {(room.status === 'maintenance' || room.status === 'blocked') && (() => { - const from = room.maintenanceFrom ? new Date(room.maintenanceFrom + 'T00:00:00') : null - const to = room.maintenanceTo ? new Date(room.maintenanceTo + 'T00:00:00') : null - // Calculate left offset and width if dates are set - let left = 0, width = '100%' + // slice(0,10) normalises both 'YYYY-MM-DD' and full ISO timestamps from Postgres + const from = room.maintenanceFrom ? new Date(room.maintenanceFrom.slice(0, 10) + 'T00:00:00') : null + const to = room.maintenanceTo ? new Date(room.maintenanceTo.slice(0, 10) + 'T00:00:00') : null + let left = 0, width: string | number = '100%' if (from || to) { - const visibleFrom = from ? Math.max(0, differenceInDays(from, startDate)) : 0 - const visibleTo = to ? Math.min(dates.length - 1, differenceInDays(to, startDate)) : dates.length - 1 - if (visibleTo < 0 || visibleFrom >= dates.length) return null - left = visibleFrom * CELL_WIDTH - width = `${(visibleTo - visibleFrom + 1) * CELL_WIDTH}px` + const vFrom = from ? Math.max(0, differenceInDays(from, startDate)) : 0 + const vTo = to ? Math.min(dates.length, differenceInDays(to, startDate)) : dates.length + if (vTo <= 0 || vFrom >= dates.length || vFrom >= vTo) return null + left = vFrom * CELL_WIDTH + width = (vTo - vFrom) * CELL_WIDTH } return (
) diff --git a/src/components/rooms/RoomContextMenu.tsx b/src/components/rooms/RoomContextMenu.tsx index 0c17494..b412c02 100644 --- a/src/components/rooms/RoomContextMenu.tsx +++ b/src/components/rooms/RoomContextMenu.tsx @@ -1,9 +1,12 @@ import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' -import { Wrench, Ban, CheckCircle, Sparkles, ClipboardList, Pencil, ChevronRight, X as XIcon } from 'lucide-react' +import { Wrench, Ban, CheckCircle, Sparkles, ClipboardList, Pencil, ChevronRight, X as XIcon, CalendarDays } from 'lucide-react' +import { format } from 'date-fns' import type { Room, RoomStatus, HousekeepingStatus } from '../../types' import { cn } from '../../lib/utils' +const today = () => format(new Date(), 'yyyy-MM-dd') + interface RoomContextMenuProps { room: Room x: number @@ -16,9 +19,9 @@ interface RoomContextMenuProps { const HK_ITEMS: { status: HousekeepingStatus; label: string; icon: React.ReactNode; color: string }[] = [ { status: 'clean', label: 'Чисто', icon: , color: 'text-emerald-600 dark:text-emerald-400' }, - { status: 'dirty', label: 'Убрать', icon: , color: 'text-red-500 dark:text-red-400' }, - { status: 'cleaning', label: 'Убирается', icon: , color: 'text-blue-500 dark:text-blue-400' }, { status: 'inspect', label: 'Проверить', icon: , color: 'text-amber-600 dark:text-amber-400' }, + { status: 'cleaning', label: 'Убирается', icon: , color: 'text-blue-500 dark:text-blue-400' }, + { status: 'dirty', label: 'Убрать', icon: , color: 'text-red-500 dark:text-red-400' }, ] type SubForm = 'maintenance' | 'blocked' | null @@ -26,9 +29,9 @@ type SubForm = 'maintenance' | 'blocked' | null export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatusChange, onEdit }: RoomContextMenuProps) { const ref = useRef(null) const [subForm, setSubForm] = useState(null) - const [dateFrom, setDateFrom] = useState(room.maintenanceFrom ?? '') - const [dateTo, setDateTo] = useState(room.maintenanceTo ?? '') - const [indefinite, setIndefinite] = useState(!room.maintenanceTo) + const [dateFrom, setDateFrom] = useState(room.maintenanceFrom ? room.maintenanceFrom.slice(0, 10) : today()) + const [dateTo, setDateTo] = useState(room.maintenanceTo ? room.maintenanceTo.slice(0, 10) : '') + const [indefinite, setIndefinite] = useState(false) useEffect(() => { const handler = (e: MouseEvent) => { @@ -200,6 +203,30 @@ export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatu ) } +function DateInput({ value, onChange, min }: { value: string; onChange: (v: string) => void; min?: string }) { + const ref = useRef(null) + return ( +
+ onChange(e.target.value)} + /> + +
+ ) +} + function DateRangeForm({ dateFrom, setDateFrom, dateTo, setDateTo, indefinite, setIndefinite, label, onApply, onCancel, @@ -217,12 +244,7 @@ function DateRangeForm({
- setDateFrom(e.target.value)} - /> +
@@ -238,13 +260,7 @@ function DateRangeForm({
{!indefinite && ( - setDateTo(e.target.value)} - /> + )}