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 [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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user