fix: 8 improvements — calendar overlaps, legend, nav, tech tasks photos/sort/modal, hk history active maintenance
This commit is contained in:
@@ -367,11 +367,11 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Legend */}
|
||||
<div className="hidden lg:flex items-center gap-3 text-xs text-slate-500">
|
||||
<div className="hidden md:flex items-center gap-2 text-xs text-slate-500">
|
||||
{(['confirmed', 'checked_in', 'checked_out', 'inquiry'] as const).map(s => (
|
||||
<div key={s} className="flex items-center gap-1.5">
|
||||
<div className={cn('w-3 h-3 rounded-sm', BOOKING_STATUS_COLORS[s].split(' ')[0])} />
|
||||
<span>{BOOKING_STATUS_LABELS[s]}</span>
|
||||
<div key={s} className="flex items-center gap-1">
|
||||
<div className={cn('w-2.5 h-2.5 rounded-sm shrink-0', BOOKING_STATUS_COLORS[s].split(' ')[0])} />
|
||||
<span className="hidden lg:inline">{BOOKING_STATUS_LABELS[s]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -386,7 +386,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
'w-1.5 h-1.5 rounded-full',
|
||||
wsConnected ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-600 animate-pulse',
|
||||
)} />
|
||||
<span className="hidden sm:inline">{wsConnected ? 'Онлайн' : 'Офлайн'}</span>
|
||||
<span className="hidden sm:inline">{wsConnected ? 'Синхронизация' : 'Офлайн'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -489,7 +489,15 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
)
|
||||
}
|
||||
// Room row
|
||||
const roomBookings = bookings.filter(b => b.roomId === room.id && b.status !== 'cancelled')
|
||||
const allRoomBookings = bookings.filter(b => b.roomId === room.id && b.status !== 'cancelled')
|
||||
// Hide checked_out bookings that overlap with a newer active booking
|
||||
const activeRoomBookings = allRoomBookings.filter(b => b.status !== 'checked_out')
|
||||
const roomBookings = allRoomBookings.filter(b => {
|
||||
if (b.status !== 'checked_out') return true
|
||||
return !activeRoomBookings.some(other =>
|
||||
other.id !== b.id && b.checkIn < other.checkOut && b.checkOut > other.checkIn,
|
||||
)
|
||||
})
|
||||
const draftStyle = getDraftStyle(room.id)
|
||||
rows.push(
|
||||
<div
|
||||
|
||||
@@ -146,12 +146,17 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
</p>
|
||||
)}
|
||||
{isModuleActive('housekeeping') && (
|
||||
<NavItem
|
||||
to="/housekeeping"
|
||||
icon={MODULES_DATA.find(m => m.id === 'housekeeping')!.sidebarItem!.icon}
|
||||
label="Уборка"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<>
|
||||
<NavItem
|
||||
to="/housekeeping"
|
||||
icon={MODULES_DATA.find(m => m.id === 'housekeeping')!.sidebarItem!.icon}
|
||||
label="Уборка"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{!isHousekeeper && (
|
||||
<NavItem to="/technical" icon={Zap} label="Тех. задачи" onClick={onClose} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isHousekeeper && activeModuleItems.map(m => (
|
||||
<NavItem
|
||||
@@ -172,7 +177,6 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
<NavItem to="/users" icon={UserCog} label="Сотрудники" onClick={onClose} />
|
||||
<NavItem to="/loyalty" icon={Award} label="Лояльность" onClick={onClose} />
|
||||
<NavItem to="/maintenance" icon={Wrench} label="Тех. перерывы" onClick={onClose} />
|
||||
<NavItem to="/technical" icon={Zap} label="Тех. задачи" onClick={onClose} />
|
||||
<NavItem to="/floor-map" icon={Map} label="План этажей" onClick={onClose} />
|
||||
{isModuleActive('channel-manager') && (
|
||||
<NavItem
|
||||
|
||||
@@ -277,23 +277,34 @@ export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatu
|
||||
/>
|
||||
<div>
|
||||
<p className="text-[10px] text-slate-500 dark:text-slate-400 mb-1">Приоритет</p>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{TECH_PRIORITY_ITEMS.map(p => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => setTechPriority(p.value)}
|
||||
className={cn(
|
||||
'px-2 py-1.5 rounded-lg text-[11px] font-medium transition-colors border',
|
||||
techPriority === p.value
|
||||
? p.value === 'urgent'
|
||||
? 'border-red-500 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'
|
||||
: 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
|
||||
: 'border-slate-200 dark:border-slate-600 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700',
|
||||
)}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => setTechPriority('urgent')}
|
||||
className={cn(
|
||||
'w-full px-2 py-1.5 rounded-lg text-[11px] font-medium transition-colors border',
|
||||
techPriority === 'urgent'
|
||||
? 'border-red-500 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'
|
||||
: 'border-slate-200 dark:border-slate-600 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700',
|
||||
)}
|
||||
>
|
||||
🚨 Экстренно!
|
||||
</button>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{TECH_PRIORITY_ITEMS.filter(p => p.value !== 'urgent').map(p => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => setTechPriority(p.value)}
|
||||
className={cn(
|
||||
'px-2 py-1.5 rounded-lg text-[11px] font-medium transition-colors border',
|
||||
techPriority === p.value
|
||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
|
||||
: 'border-slate-200 dark:border-slate-600 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700',
|
||||
)}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{techPriority === 'urgent' && (
|
||||
<p className="text-[10px] text-red-600 dark:text-red-400 mt-1.5 flex items-center gap-1">
|
||||
@@ -310,24 +321,27 @@ export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatu
|
||||
className="hidden"
|
||||
onChange={e => { const f = e.target.files?.[0]; if (f) { handleTechPhotoUpload(f); e.target.value = '' } }}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{techPhotos.map((url, i) => (
|
||||
<div key={i} className="relative group">
|
||||
<img src={url} alt="" className="w-10 h-10 object-cover rounded border border-slate-200 dark:border-slate-600" />
|
||||
<button
|
||||
onClick={() => setTechPhotos(prev => prev.filter((_, j) => j !== i))}
|
||||
className="absolute -top-1 -right-1 w-3.5 h-3.5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
><XIcon size={8} /></button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => photoInputRef.current?.click()}
|
||||
disabled={photoUploading}
|
||||
className="w-10 h-10 rounded border-2 border-dashed border-slate-300 dark:border-slate-600 flex items-center justify-center text-slate-400 hover:border-brand-400 hover:text-brand-500 transition-colors"
|
||||
>
|
||||
{photoUploading ? <Loader2 size={12} className="animate-spin" /> : <Camera size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
{techPhotos.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||
{techPhotos.map((url, i) => (
|
||||
<div key={i} className="relative group">
|
||||
<img src={url} alt="" className="w-10 h-10 object-cover rounded border border-slate-200 dark:border-slate-600" />
|
||||
<button
|
||||
onClick={() => setTechPhotos(prev => prev.filter((_, j) => j !== i))}
|
||||
className="absolute -top-1 -right-1 w-3.5 h-3.5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
><XIcon size={8} /></button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => photoInputRef.current?.click()}
|
||||
disabled={photoUploading}
|
||||
className="w-full py-1.5 rounded-lg border border-dashed border-slate-300 dark:border-slate-600 flex items-center justify-center gap-1.5 text-[11px] text-slate-400 hover:border-brand-400 hover:text-brand-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{photoUploading ? <Loader2 size={11} className="animate-spin" /> : <Camera size={11} />}
|
||||
{photoUploading ? 'Загрузка...' : 'Добавить фото'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-1.5 pt-0.5">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user