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:
@@ -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,7 +466,11 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
|
||||
{/* ── RENTAL FORM ── */}
|
||||
{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 */}
|
||||
{rentalObjects && rentalObjects.length > 1 && (
|
||||
<div>
|
||||
@@ -473,6 +479,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
{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',
|
||||
@@ -504,12 +511,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
{/* 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)}
|
||||
/>
|
||||
<input type="date" className="input" value={rentalDate} onChange={e => setRentalDate(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<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>
|
||||
|
||||
{/* 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 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} ₽`})
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user