fix: maintenance overlay + context menu UX improvements

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 16:03:17 +03:00
parent c3a30be74e
commit 8312d2372e
2 changed files with 54 additions and 36 deletions

View File

@@ -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: <Sparkles size={14} />, color: 'text-emerald-600 dark:text-emerald-400' },
{ status: 'dirty', label: 'Убрать', icon: <ClipboardList size={14} />, color: 'text-red-500 dark:text-red-400' },
{ status: 'cleaning', label: 'Убирается', icon: <ClipboardList size={14} />, color: 'text-blue-500 dark:text-blue-400' },
{ status: 'inspect', label: 'Проверить', icon: <ClipboardList size={14} />, color: 'text-amber-600 dark:text-amber-400' },
{ status: 'cleaning', label: 'Убирается', icon: <ClipboardList size={14} />, color: 'text-blue-500 dark:text-blue-400' },
{ status: 'dirty', label: 'Убрать', icon: <ClipboardList size={14} />, 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<HTMLDivElement>(null)
const [subForm, setSubForm] = useState<SubForm>(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<HTMLInputElement>(null)
return (
<div className="relative mt-0.5">
<input
ref={ref}
type="date"
className="w-full text-xs border border-slate-200 dark:border-slate-600 rounded-lg pl-2 pr-7 py-1 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200"
value={value}
min={min}
onChange={e => onChange(e.target.value)}
/>
<button
type="button"
tabIndex={-1}
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-brand-500 transition-colors"
onClick={() => { ref.current?.showPicker?.() ?? ref.current?.focus() }}
>
<CalendarDays size={12} />
</button>
</div>
)
}
function DateRangeForm({
dateFrom, setDateFrom, dateTo, setDateTo,
indefinite, setIndefinite, label, onApply, onCancel,
@@ -217,12 +244,7 @@ function DateRangeForm({
<div className="space-y-1.5">
<div>
<label className="text-[10px] text-slate-500 dark:text-slate-400">С (начало)</label>
<input
type="date"
className="w-full text-xs border border-slate-200 dark:border-slate-600 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 mt-0.5"
value={dateFrom}
onChange={e => setDateFrom(e.target.value)}
/>
<DateInput value={dateFrom} onChange={setDateFrom} />
</div>
<div>
<div className="flex items-center justify-between">
@@ -238,13 +260,7 @@ function DateRangeForm({
</label>
</div>
{!indefinite && (
<input
type="date"
className="w-full text-xs border border-slate-200 dark:border-slate-600 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 mt-0.5"
value={dateTo}
min={dateFrom}
onChange={e => setDateTo(e.target.value)}
/>
<DateInput value={dateTo} onChange={setDateTo} min={dateFrom} />
)}
</div>
</div>