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 [rentalPhone, setRentalPhone] = useState('')
const [rentalNotes, setRentalNotes] = useState('') const [rentalNotes, setRentalNotes] = useState('')
const [rentalSaving, setRentalSaving] = useState(false) const [rentalSaving, setRentalSaving] = useState(false)
const [rentalPayMethod, setRentalPayMethod] = useState<'cash' | 'terminal' | null>(null)
const [rentalPaidAmount, setRentalPaidAmount] = useState(0)
// Rental guest autocomplete // Rental guest autocomplete
const [rentalShowSugg, setRentalShowSugg] = useState(false) const [rentalShowSugg, setRentalShowSugg] = useState(false)
const [rentalDbResults, setRentalDbResults] = useState<{ id: string; firstName: string; lastName: string; middleName: string | null; phone: string | null }[]>([]) 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(), guestPhone: rentalPhone.trim(),
notes: rentalNotes.trim() || undefined, notes: rentalNotes.trim() || undefined,
totalAmount: rentalTotal, totalAmount: rentalTotal,
paidAmount: 0, paidAmount: rentalPaidAmount,
status: 'confirmed', status: 'confirmed',
}) })
onClose() onClose()
@@ -464,7 +466,11 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
{/* ── RENTAL FORM ── */} {/* ── RENTAL FORM ── */}
{bookingType === 'rental' && rentalObj && ( {bookingType === 'rental' && rentalObj && (
<div className="space-y-4"> <div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
{/* ── LEFT: объект + дата + время ── */}
<div className="flex-1 space-y-4 min-w-0">
{/* Object picker */} {/* Object picker */}
{rentalObjects && rentalObjects.length > 1 && ( {rentalObjects && rentalObjects.length > 1 && (
<div> <div>
@@ -473,6 +479,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
{rentalObjects.map(o => ( {rentalObjects.map(o => (
<button <button
key={o.id} key={o.id}
type="button"
onClick={() => { setRentalObjId(o.id); setRentalStartM(o.openHour * 60); setRentalEndM(o.openHour * 60 + 60) }} onClick={() => { setRentalObjId(o.id); setRentalStartM(o.openHour * 60); setRentalEndM(o.openHour * 60 + 60) }}
className={cn( className={cn(
'flex items-center gap-2 p-2.5 rounded-xl border text-sm font-medium transition-colors text-left', 'flex items-center gap-2 p-2.5 rounded-xl border text-sm font-medium transition-colors text-left',
@@ -504,12 +511,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
{/* Date */} {/* Date */}
<div> <div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Дата</label> <label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Дата</label>
<input <input type="date" className="input" value={rentalDate} onChange={e => setRentalDate(e.target.value)} />
type="date"
className="input"
value={rentalDate}
onChange={e => setRentalDate(e.target.value)}
/>
</div> </div>
{/* Full day / time */} {/* Full day / time */}
@@ -573,15 +575,18 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
)} )}
</> </>
)} )}
</div>
{/* ── RIGHT: гость + оплата ── */}
<div className="w-full sm:w-72 space-y-4 shrink-0">
{/* Guest */} {/* Guest */}
<div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5"> <label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
<User size={13} className="inline mr-1" />Гость * <User size={13} className="inline mr-1" />Гость *
</label> </label>
<div className="relative" ref={rentalNameRef}> <div className="relative" ref={rentalNameRef}>
<input type="text" className="input" placeholder="Имя Фамилия" autoComplete="off" <input type="text" className="input" placeholder="Фамилия Имя Отчество" autoComplete="off"
value={rentalGuest} value={rentalGuest}
onFocus={() => setRentalShowSugg(true)} onFocus={() => setRentalShowSugg(true)}
onChange={e => { onChange={e => {
@@ -616,7 +621,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
</button> </button>
))} ))}
{rentalDbResults {rentalDbResults
.filter(g => !rentalFiltered.some(b => b.guestName === `${g.lastName} ${g.firstName}`)) .filter(g => !rentalFiltered.some(b => b.guestName.startsWith(g.lastName)))
.map(g => ( .map(g => (
<button key={g.id} type="button" <button key={g.id} type="button"
onMouseDown={() => { setRentalGuest([g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ')); setRentalPhone(g.phone ?? ''); setRentalShowSugg(false); setRentalDbResults([]) }} 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="w-2 h-2 rounded-full shrink-0 bg-slate-400" />
<div className="min-w-0 flex-1"> <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> <p className="text-[11px] text-slate-400 truncate">{g.phone ?? 'нет телефона'}</p>
</div> </div>
</button> </button>
@@ -633,6 +638,8 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
)} )}
</div> </div>
</div> </div>
{/* Phone */}
<div> <div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5"> <label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
<Phone size={13} className="inline mr-1" />Телефон <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" <input type="tel" className="input" placeholder="+7 (999) 000-00-00"
value={rentalPhone} onChange={e => setRentalPhone(e.target.value)} /> value={rentalPhone} onChange={e => setRentalPhone(e.target.value)} />
</div> </div>
</div>
{/* Notes */}
<div> <div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Примечания</label> <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="Доп. пожелания..." <textarea className="input resize-none h-16 text-sm" placeholder="Доп. пожелания..."
value={rentalNotes} onChange={e => setRentalNotes(e.target.value)} /> value={rentalNotes} onChange={e => setRentalNotes(e.target.value)} />
</div> </div>
{/* Payment + Total */}
{rentalHours > 0 && ( {rentalHours > 0 && (
<div className="rounded-xl bg-brand-50 dark:bg-brand-900/20 border border-brand-200 dark:border-brand-700 p-3"> <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">
<p className="text-sm font-semibold text-brand-700 dark:text-brand-300"> {/* Итого */}
Итого: {rentalTotal.toLocaleString('ru-RU')} <div className="flex items-baseline justify-between">
<span className="ml-2 text-xs font-normal text-brand-500"> <span className="text-xs text-slate-500 dark:text-slate-400">
({rentalIsFullDay ? 'весь день' : `${rentalHours} ч × ${rentalObj.pricePerHour}`}) {rentalIsFullDay ? 'Весь день' : `${rentalHours} ч × ${rentalObj.pricePerHour.toLocaleString('ru-RU')}`}
</span> </span>
</p> <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> </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> </div>
)} )}