feat: rental tab 2-column layout + payment section

Left col: object picker, date, full-day toggle, time selectors
Right col: guest autocomplete, phone, notes, total + payment block
Payment: Наличные/Терминал toggle buttons, оплачено input, 'Всю сумму' shortcut, debt indicator; paidAmount saved to booking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 01:31:16 +03:00
parent b1b6854663
commit 7c65bebc22

View File

@@ -112,6 +112,8 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
const [rentalPhone, setRentalPhone] = useState('')
const [rentalNotes, setRentalNotes] = useState('')
const [rentalSaving, setRentalSaving] = useState(false)
const [rentalPayMethod, setRentalPayMethod] = useState<'cash' | 'terminal' | null>(null)
const [rentalPaidAmount, setRentalPaidAmount] = useState(0)
// Rental guest autocomplete
const [rentalShowSugg, setRentalShowSugg] = useState(false)
const [rentalDbResults, setRentalDbResults] = useState<{ id: string; firstName: string; lastName: string; middleName: string | null; phone: string | null }[]>([])
@@ -218,7 +220,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
guestPhone: rentalPhone.trim(),
notes: rentalNotes.trim() || undefined,
totalAmount: rentalTotal,
paidAmount: 0,
paidAmount: rentalPaidAmount,
status: 'confirmed',
})
onClose()
@@ -464,124 +466,127 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
{/* ── RENTAL FORM ── */}
{bookingType === 'rental' && rentalObj && (
<div className="space-y-4">
{/* Object picker */}
{rentalObjects && rentalObjects.length > 1 && (
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Объект аренды</label>
<div className="grid grid-cols-2 gap-2">
{rentalObjects.map(o => (
<button
key={o.id}
onClick={() => { setRentalObjId(o.id); setRentalStartM(o.openHour * 60); setRentalEndM(o.openHour * 60 + 60) }}
className={cn(
'flex items-center gap-2 p-2.5 rounded-xl border text-sm font-medium transition-colors text-left',
rentalObjId === o.id
? '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-700 dark:text-slate-300 hover:border-brand-300 dark:hover:border-brand-600',
)}
>
<span className="text-lg">{o.icon}</span>
<div className="min-w-0">
<p className="truncate">{o.name}</p>
<p className="text-xs text-slate-400">{o.pricePerHour} /ч</p>
</div>
</button>
))}
</div>
</div>
)}
{rentalObjects?.length === 1 && (
<div className="flex items-center gap-3 p-3 rounded-xl bg-slate-50 dark:bg-slate-700/40">
<span className="text-2xl">{rentalObj.icon}</span>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">{rentalObj.name}</p>
<p className="text-xs text-slate-500">{rentalObj.pricePerHour} /ч · {rentalObj.pricePerDay} /день</p>
</div>
</div>
)}
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
{/* Date */}
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Дата</label>
<input
type="date"
className="input"
value={rentalDate}
onChange={e => setRentalDate(e.target.value)}
/>
{/* ── LEFT: объект + дата + время ── */}
<div className="flex-1 space-y-4 min-w-0">
{/* Object picker */}
{rentalObjects && rentalObjects.length > 1 && (
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Объект аренды</label>
<div className="grid grid-cols-2 gap-2">
{rentalObjects.map(o => (
<button
key={o.id}
type="button"
onClick={() => { setRentalObjId(o.id); setRentalStartM(o.openHour * 60); setRentalEndM(o.openHour * 60 + 60) }}
className={cn(
'flex items-center gap-2 p-2.5 rounded-xl border text-sm font-medium transition-colors text-left',
rentalObjId === o.id
? '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-700 dark:text-slate-300 hover:border-brand-300 dark:hover:border-brand-600',
)}
>
<span className="text-lg">{o.icon}</span>
<div className="min-w-0">
<p className="truncate">{o.name}</p>
<p className="text-xs text-slate-400">{o.pricePerHour} /ч</p>
</div>
</button>
))}
</div>
</div>
)}
{rentalObjects?.length === 1 && (
<div className="flex items-center gap-3 p-3 rounded-xl bg-slate-50 dark:bg-slate-700/40">
<span className="text-2xl">{rentalObj.icon}</span>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">{rentalObj.name}</p>
<p className="text-xs text-slate-500">{rentalObj.pricePerHour} /ч · {rentalObj.pricePerDay} /день</p>
</div>
</div>
)}
{/* Date */}
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Дата</label>
<input type="date" className="input" value={rentalDate} onChange={e => setRentalDate(e.target.value)} />
</div>
{/* Full day / time */}
{rentalHasFullDay ? (
<div className="flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm">
<AlertCircle size={15} className="shrink-0" />
<span>Объект уже забронирован на весь день</span>
</div>
) : (
<>
<div className={cn('flex items-center gap-3 p-3 rounded-xl border border-slate-200 dark:border-slate-600', rentalHasTimed && 'opacity-50 pointer-events-none')}>
<CalendarDays size={16} className="text-slate-400 shrink-0" />
<span className="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1">Весь день</span>
{rentalHasTimed && <span className="text-xs text-slate-400">Есть частичные брони</span>}
<button
type="button"
onClick={() => !rentalHasTimed && setRentalIsFullDay(v => !v)}
className={cn('relative rounded-full overflow-hidden transition-colors shrink-0 p-0', rentalIsFullDay ? 'bg-brand-600' : 'bg-slate-200 dark:bg-slate-600')}
style={{ height: 22, width: 40 }}
>
<span className={cn('absolute top-[3px] left-[2px] w-4 h-4 rounded-full bg-white shadow transition-transform duration-200', rentalIsFullDay ? 'translate-x-[18px]' : 'translate-x-0')} />
</button>
</div>
{!rentalIsFullDay && (
rentalNoSlots ? (
<div className="flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm">
<AlertCircle size={15} className="shrink-0" />
<span>Все слоты на этот день заняты</span>
</div>
) : (
<div>
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
Время
{rentalBufMin > 0 && <span className="ml-1 font-normal normal-case">(перерыв {rentalBufMin} мин)</span>}
</label>
<div className="flex items-center gap-3">
<div className="flex-1">
<label className="block text-xs text-slate-500 mb-1">Начало</label>
<select className="input text-sm" value={rentalEffStart}
onChange={e => {
const v = parseInt(e.target.value)
setRentalStartM(v)
const maxE = rentalGetMaxEnd(v)
if (rentalEndM <= v || rentalEndM > maxE) setRentalEndM(Math.min(v + 60, maxE))
}}>
{rentalAvailStarts.map(m => <option key={m} value={m}>{fmtTime(m)}</option>)}
</select>
</div>
<Clock size={14} className="text-slate-400 mt-4 shrink-0" />
<div className="flex-1">
<label className="block text-xs text-slate-500 mb-1">Конец</label>
<select className="input text-sm"
value={rentalAvailEnds.includes(rentalEndM) ? rentalEndM : (rentalAvailEnds[0] ?? rentalEndM)}
onChange={e => setRentalEndM(parseInt(e.target.value))}>
{rentalAvailEnds.map(m => <option key={m} value={m}>{fmtTime(m)}</option>)}
</select>
</div>
</div>
</div>
)
)}
</>
)}
</div>
{/* Full day / time */}
{rentalHasFullDay ? (
<div className="flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm">
<AlertCircle size={15} className="shrink-0" />
<span>Объект уже забронирован на весь день</span>
</div>
) : (
<>
<div className={cn('flex items-center gap-3 p-3 rounded-xl border border-slate-200 dark:border-slate-600', rentalHasTimed && 'opacity-50 pointer-events-none')}>
<CalendarDays size={16} className="text-slate-400 shrink-0" />
<span className="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1">Весь день</span>
{rentalHasTimed && <span className="text-xs text-slate-400">Есть частичные брони</span>}
<button
type="button"
onClick={() => !rentalHasTimed && setRentalIsFullDay(v => !v)}
className={cn('relative rounded-full overflow-hidden transition-colors shrink-0 p-0', rentalIsFullDay ? 'bg-brand-600' : 'bg-slate-200 dark:bg-slate-600')}
style={{ height: 22, width: 40 }}
>
<span className={cn('absolute top-[3px] left-[2px] w-4 h-4 rounded-full bg-white shadow transition-transform duration-200', rentalIsFullDay ? 'translate-x-[18px]' : 'translate-x-0')} />
</button>
</div>
{!rentalIsFullDay && (
rentalNoSlots ? (
<div className="flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm">
<AlertCircle size={15} className="shrink-0" />
<span>Все слоты на этот день заняты</span>
</div>
) : (
<div>
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
Время
{rentalBufMin > 0 && <span className="ml-1 font-normal normal-case">(перерыв {rentalBufMin} мин)</span>}
</label>
<div className="flex items-center gap-3">
<div className="flex-1">
<label className="block text-xs text-slate-500 mb-1">Начало</label>
<select className="input text-sm" value={rentalEffStart}
onChange={e => {
const v = parseInt(e.target.value)
setRentalStartM(v)
const maxE = rentalGetMaxEnd(v)
if (rentalEndM <= v || rentalEndM > maxE) setRentalEndM(Math.min(v + 60, maxE))
}}>
{rentalAvailStarts.map(m => <option key={m} value={m}>{fmtTime(m)}</option>)}
</select>
</div>
<Clock size={14} className="text-slate-400 mt-4 shrink-0" />
<div className="flex-1">
<label className="block text-xs text-slate-500 mb-1">Конец</label>
<select className="input text-sm"
value={rentalAvailEnds.includes(rentalEndM) ? rentalEndM : (rentalAvailEnds[0] ?? rentalEndM)}
onChange={e => setRentalEndM(parseInt(e.target.value))}>
{rentalAvailEnds.map(m => <option key={m} value={m}>{fmtTime(m)}</option>)}
</select>
</div>
</div>
</div>
)
)}
</>
)}
{/* ── RIGHT: гость + оплата ── */}
<div className="w-full sm:w-72 space-y-4 shrink-0">
{/* Guest */}
<div className="grid grid-cols-2 gap-3">
{/* Guest */}
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
<User size={13} className="inline mr-1" />Гость *
</label>
<div className="relative" ref={rentalNameRef}>
<input type="text" className="input" placeholder="Имя Фамилия" autoComplete="off"
<input type="text" className="input" placeholder="Фамилия Имя Отчество" autoComplete="off"
value={rentalGuest}
onFocus={() => setRentalShowSugg(true)}
onChange={e => {
@@ -616,7 +621,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
</button>
))}
{rentalDbResults
.filter(g => !rentalFiltered.some(b => b.guestName === `${g.lastName} ${g.firstName}`))
.filter(g => !rentalFiltered.some(b => b.guestName.startsWith(g.lastName)))
.map(g => (
<button key={g.id} type="button"
onMouseDown={() => { setRentalGuest([g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ')); setRentalPhone(g.phone ?? ''); setRentalShowSugg(false); setRentalDbResults([]) }}
@@ -624,7 +629,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
>
<div className="w-2 h-2 rounded-full shrink-0 bg-slate-400" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-slate-800 dark:text-slate-200 truncate">{g.lastName} {g.firstName}</p>
<p className="text-sm font-medium text-slate-800 dark:text-slate-200 truncate">{[g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ')}</p>
<p className="text-[11px] text-slate-400 truncate">{g.phone ?? 'нет телефона'}</p>
</div>
</button>
@@ -633,6 +638,8 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
)}
</div>
</div>
{/* Phone */}
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
<Phone size={13} className="inline mr-1" />Телефон
@@ -640,24 +647,84 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
<input type="tel" className="input" placeholder="+7 (999) 000-00-00"
value={rentalPhone} onChange={e => setRentalPhone(e.target.value)} />
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Примечания</label>
<textarea className="input resize-none h-16 text-sm" placeholder="Доп. пожелания..."
value={rentalNotes} onChange={e => setRentalNotes(e.target.value)} />
</div>
{rentalHours > 0 && (
<div className="rounded-xl bg-brand-50 dark:bg-brand-900/20 border border-brand-200 dark:border-brand-700 p-3">
<p className="text-sm font-semibold text-brand-700 dark:text-brand-300">
Итого: {rentalTotal.toLocaleString('ru-RU')}
<span className="ml-2 text-xs font-normal text-brand-500">
({rentalIsFullDay ? 'весь день' : `${rentalHours} ч × ${rentalObj.pricePerHour}`})
</span>
</p>
{/* Notes */}
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Примечания</label>
<textarea className="input resize-none h-16 text-sm" placeholder="Доп. пожелания..."
value={rentalNotes} onChange={e => setRentalNotes(e.target.value)} />
</div>
)}
{/* Payment + Total */}
{rentalHours > 0 && (
<div className="rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-700/40 p-3 space-y-3">
{/* Итого */}
<div className="flex items-baseline justify-between">
<span className="text-xs text-slate-500 dark:text-slate-400">
{rentalIsFullDay ? 'Весь день' : `${rentalHours} ч × ${rentalObj.pricePerHour.toLocaleString('ru-RU')}`}
</span>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
{rentalTotal.toLocaleString('ru-RU')}
</span>
</div>
{/* Способ оплаты */}
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1.5">Внесение оплаты</p>
<div className="flex gap-2">
<button
type="button"
onClick={() => setRentalPayMethod(p => p === 'cash' ? null : 'cash')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors flex-1 justify-center',
rentalPayMethod === 'cash'
? 'bg-emerald-600 text-white border-emerald-600'
: 'bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-emerald-400',
)}
>
<Banknote size={14} /> Наличные
</button>
<button
type="button"
onClick={() => setRentalPayMethod(p => p === 'terminal' ? null : 'terminal')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors flex-1 justify-center',
rentalPayMethod === 'terminal'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-blue-400',
)}
>
<CreditCard size={14} /> Терминал
</button>
</div>
</div>
{/* Оплачено */}
{rentalPayMethod && (
<div className="flex items-center gap-2">
<label className="text-xs text-slate-500 dark:text-slate-400 shrink-0">Оплачено ()</label>
<input
type="number" min={0} max={rentalTotal}
className="input flex-1 text-sm"
value={rentalPaidAmount}
onChange={e => setRentalPaidAmount(Math.min(rentalTotal, Math.max(0, parseInt(e.target.value) || 0)))}
/>
<button
type="button"
onClick={() => setRentalPaidAmount(rentalTotal)}
className="text-xs text-brand-600 dark:text-brand-400 hover:underline shrink-0"
>
Всю сумму
</button>
</div>
)}
{rentalPayMethod && rentalPaidAmount > 0 && rentalPaidAmount < rentalTotal && (
<p className="text-xs text-amber-600 dark:text-amber-400">
Долг: {(rentalTotal - rentalPaidAmount).toLocaleString('ru-RU')}
</p>
)}
</div>
)}
</div>
</div>
)}