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:
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine } from 'lucide-react'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine, Minus, BedDouble } from 'lucide-react'
|
||||
import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage'
|
||||
import type { Discount } from '../../pages/DiscountsPage'
|
||||
import { Modal } from '../ui/Modal'
|
||||
@@ -99,6 +99,10 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
const [startHour, setStartHour] = useState(10)
|
||||
const [endHour, setEndHour] = useState(12)
|
||||
|
||||
// Extra beds
|
||||
const [extraBeds, setExtraBeds] = useState(0)
|
||||
const EXTRA_BED_PRICE = 1500
|
||||
|
||||
const room = rooms.find(r => r.id === form.roomId)
|
||||
const nights = form.checkIn && form.checkOut
|
||||
? Math.max(0, (new Date(form.checkOut).getTime() - new Date(form.checkIn).getTime()) / 86400000)
|
||||
@@ -115,7 +119,8 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
: 0
|
||||
const roomTotal = Math.max(0, roomBaseTotal - discountAmount)
|
||||
const servicesTotal = addedServices.reduce((s, sv) => s + sv.price * sv.qty, 0)
|
||||
const total = roomTotal + servicesTotal
|
||||
const extraBedsTotal = extraBeds * EXTRA_BED_PRICE * (isHourly ? 1 : Math.max(1, nights))
|
||||
const total = roomTotal + servicesTotal + extraBedsTotal
|
||||
|
||||
const debt = Math.max(0, total - paidAmount)
|
||||
|
||||
@@ -150,6 +155,7 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
const hourlyPrefix = isHourly && room?.allowHourly
|
||||
? `[Почасово: ${String(startHour).padStart(2, '0')}:00–${String(endHour).padStart(2, '0')}:00] `
|
||||
: ''
|
||||
const extraBedsPrefix = extraBeds > 0 ? `[Доп.места: ${extraBeds}] ` : ''
|
||||
|
||||
const checkIn = isHourly && room?.allowHourly ? hourlyDate : form.checkIn
|
||||
const checkOut = isHourly && room?.allowHourly ? hourlyDate : form.checkOut
|
||||
@@ -159,7 +165,7 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
source: 'direct',
|
||||
checkIn,
|
||||
checkOut,
|
||||
notes: hourlyPrefix + form.notes,
|
||||
notes: hourlyPrefix + extraBedsPrefix + form.notes,
|
||||
totalAmount: total,
|
||||
paidAmount,
|
||||
id: existing?.id ?? `b-${Date.now()}`,
|
||||
@@ -418,6 +424,37 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Extra beds */}
|
||||
<div className="flex items-center justify-between px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-700/40 border border-slate-200 dark:border-slate-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<BedDouble size={15} className="text-slate-400 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 leading-tight">Доп. места</p>
|
||||
<p className="text-[10px] text-slate-400">{EXTRA_BED_PRICE.toLocaleString('ru-RU')} ₽/место{!isHourly && nights > 0 ? '/ночь' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{extraBeds > 0 && (
|
||||
<span className="text-xs font-medium text-amber-600 dark:text-amber-400 mr-1">
|
||||
+{extraBedsTotal.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
)}
|
||||
<button type="button" onClick={() => setExtraBeds(e => Math.max(0, e - 1))}
|
||||
className="w-6 h-6 rounded-md bg-slate-200 dark:bg-slate-600 flex items-center justify-center hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors disabled:opacity-40"
|
||||
disabled={extraBeds === 0}
|
||||
>
|
||||
<Minus size={11} />
|
||||
</button>
|
||||
<span className="w-5 text-center text-sm font-semibold text-slate-900 dark:text-slate-100">{extraBeds}</span>
|
||||
<button type="button" onClick={() => setExtraBeds(e => Math.min(4, e + 1))}
|
||||
className="w-6 h-6 rounded-md bg-slate-200 dark:bg-slate-600 flex items-center justify-center hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors disabled:opacity-40"
|
||||
disabled={extraBeds >= 4}
|
||||
>
|
||||
<Plus size={11} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Guest tags */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user