Add hourly indicator in calendar, extra beds, custom amenities, meal/inclusion management

- Calendar: show clock icon + hours on hourly bookings (detect [Почасово:] prefix in notes)
- BookingModal: add extra beds counter (max 4, 1500₽/place/night) included in total
- RoomCategoriesPage: custom amenity input — type any amenity and add it alongside predefined list
- TariffsPage: collapsible Справочники section with full add/edit/delete for meal plan types and room inclusions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 15:28:43 +03:00
parent 92f0270290
commit c347c99228
4 changed files with 247 additions and 17 deletions

View File

@@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'
import type { ReactNode } from 'react'
import { addDays, format, startOfDay, differenceInDays, parseISO, isToday } from 'date-fns'
import { ru } from 'date-fns/locale'
import { ChevronLeft, ChevronRight, Plus, CalendarDays, ChevronDown, AlignJustify } from 'lucide-react'
import { ChevronLeft, ChevronRight, Plus, CalendarDays, ChevronDown, AlignJustify, Clock } from 'lucide-react'
import { cn, BOOKING_STATUS_COLORS, BOOKING_STATUS_LABELS, SOURCE_LABELS } from '../../lib/utils'
import type { Room, Booking, DraftBooking } from '../../types'
import type { RentalObject, RentalBooking } from '../../data/rentalData'
@@ -412,6 +412,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
const isFading = fadingBookingIds?.has(booking.id)
const isUnpaid = booking.paidAmount < booking.totalAmount
const guestCount = (booking.adults ?? 0) + (booking.children ?? 0)
const hourlyMatch = booking.notes?.match(/\[Почасово: (\d+):00(\d+):00\]/)
return (
<div
key={booking.id}
@@ -428,20 +429,26 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
position: 'absolute',
}}
onClick={(e) => { e.stopPropagation(); setSelectedBooking(booking) }}
title={`${booking.guestName}${booking.checkIn} ${booking.checkOut}${isUnpaid ? ' • Не оплачено' : ''}`}
title={`${booking.guestName}${booking.checkIn}${hourlyMatch ? ` ${hourlyMatch[1]}:00${hourlyMatch[2]}:00` : ` ${booking.checkOut}`}${isUnpaid ? ' • Не оплачено' : ''}`}
>
{isUnpaid && (
<span className="shrink-0 w-1.5 h-1.5 rounded-full bg-red-500 shadow-sm mr-1 mt-0.5" />
)}
{hourlyMatch && <Clock size={9} className="shrink-0 mr-0.5 opacity-80" />}
<span className="truncate text-xs font-semibold opacity-95 drop-shadow-sm">
{booking.guestName}
</span>
{!compact && style.width > 90 && guestCount > 0 && (
{!compact && hourlyMatch && style.width > 80 && (
<span className="ml-1 opacity-80 text-[10px] shrink-0">
{hourlyMatch[1]}{hourlyMatch[2]}ч
</span>
)}
{!compact && !hourlyMatch && style.width > 90 && guestCount > 0 && (
<span className="ml-1.5 opacity-75 text-[10px] shrink-0">
{guestCount}г
</span>
)}
{!compact && style.width > 130 && (
{!compact && !hourlyMatch && style.width > 130 && (
<span className="ml-1.5 opacity-75 text-[10px] shrink-0">
{nights}н
</span>