ui: боковая панель — ширина 560px, вкладка «Оплата» двухколоночная

- w-[400px] → w-[560px] для боковой панели бронирования
- Вкладка «Оплата»: расчёт + платежи слева, депозит справа (grid 2 cols)
- На мобиле — одна колонка как прежде

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 15:59:20 +03:00
parent 83d797c55f
commit 45a8cf7c10

View File

@@ -955,7 +955,7 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
<> <>
<div className="fixed inset-0 z-40" onClick={onClose} /> <div className="fixed inset-0 z-40" onClick={onClose} />
<div className="fixed right-0 top-0 bottom-0 z-50 w-full sm:w-[400px] bg-white dark:bg-slate-800 border-l border-slate-200 dark:border-slate-700 shadow-2xl flex flex-col"> <div className="fixed right-0 top-0 bottom-0 z-50 w-full sm:w-[560px] bg-white dark:bg-slate-800 border-l border-slate-200 dark:border-slate-700 shadow-2xl flex flex-col">
{/* Toast notification */} {/* Toast notification */}
{toast && ( {toast && (
@@ -1636,125 +1636,131 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
{/* ── Оплата ───────────────────────────────────────────────────────── */} {/* ── Оплата ───────────────────────────────────────────────────────── */}
{tab === 'payment' && ( {tab === 'payment' && (
<div className="space-y-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-start">
{/* Discount selector */}
<div>
<label className={lbl}>Скидка</label>
<select className="input text-sm" value={discountId} onChange={e => setDiscountId(e.target.value)}>
<option value=""> Без скидки </option>
{activeDiscs.map(d => (
<option key={d.id} value={d.id}>
{d.name} ({d.valueType === 'percent' ? `${d.value}%` : formatCurrency(d.value)})
</option>
))}
</select>
</div>
{/* Breakdown */} {/* Левая колонка — расчёт и приём оплаты */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 divide-y divide-slate-200 dark:divide-slate-600 text-sm"> <div className="space-y-4">
<div className="flex justify-between px-3 py-2"> {/* Discount selector */}
<span className="text-slate-500 dark:text-slate-400">Стоимость</span> <div>
<span className="font-medium text-slate-900 dark:text-slate-100">{formatCurrency(baseTotal)}</span> <label className={lbl}>Скидка</label>
<select className="input text-sm" value={discountId} onChange={e => setDiscountId(e.target.value)}>
<option value=""> Без скидки </option>
{activeDiscs.map(d => (
<option key={d.id} value={d.id}>
{d.name} ({d.valueType === 'percent' ? `${d.value}%` : formatCurrency(d.value)})
</option>
))}
</select>
</div> </div>
{discountAmt > 0 && (
{/* Breakdown */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 divide-y divide-slate-200 dark:divide-slate-600 text-sm">
<div className="flex justify-between px-3 py-2"> <div className="flex justify-between px-3 py-2">
<span className="text-slate-500 dark:text-slate-400">Скидка «{selDiscount?.name}»</span> <span className="text-slate-500 dark:text-slate-400">Стоимость</span>
<span className="font-medium text-emerald-600 dark:text-emerald-400">{formatCurrency(discountAmt)}</span> <span className="font-medium text-slate-900 dark:text-slate-100">{formatCurrency(baseTotal)}</span>
</div>
{discountAmt > 0 && (
<div className="flex justify-between px-3 py-2">
<span className="text-slate-500 dark:text-slate-400">Скидка «{selDiscount?.name}»</span>
<span className="font-medium text-emerald-600 dark:text-emerald-400">{formatCurrency(discountAmt)}</span>
</div>
)}
<div className="flex justify-between px-3 py-2 font-semibold">
<span className="text-slate-700 dark:text-slate-300">Итого</span>
<span className="text-slate-900 dark:text-slate-100">{formatCurrency(finalTotal)}</span>
</div>
<div className="flex justify-between px-3 py-2">
<span className="text-slate-500 dark:text-slate-400">Оплачено</span>
<span className="font-medium text-emerald-600 dark:text-emerald-400">{formatCurrency(totalPaid)}</span>
</div>
<div className={cn(
'flex justify-between px-3 py-2 font-bold',
balance > 0 ? 'text-red-600 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400',
)}>
<span>{balance > 0 ? 'Остаток' : 'Переплата'}</span>
<span>{balance > 0 ? formatCurrency(balance) : `+${formatCurrency(-balance)}`}</span>
</div> </div>
)}
<div className="flex justify-between px-3 py-2 font-semibold">
<span className="text-slate-700 dark:text-slate-300">Итого</span>
<span className="text-slate-900 dark:text-slate-100">{formatCurrency(finalTotal)}</span>
</div> </div>
<div className="flex justify-between px-3 py-2">
<span className="text-slate-500 dark:text-slate-400">Оплачено</span>
<span className="font-medium text-emerald-600 dark:text-emerald-400">{formatCurrency(totalPaid)}</span>
</div>
<div className={cn(
'flex justify-between px-3 py-2 font-bold',
balance > 0 ? 'text-red-600 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400',
)}>
<span>{balance > 0 ? 'Остаток' : 'Переплата'}</span>
<span>{balance > 0 ? formatCurrency(balance) : `+${formatCurrency(-balance)}`}</span>
</div>
</div>
{/* Accept payment */} {/* Accept payment */}
{!showPayForm ? ( {!showPayForm ? (
<button <button
onClick={() => { setPayAmount(balance > 0 ? String(balance) : ''); setShowPayForm(true) }} onClick={() => { setPayAmount(balance > 0 ? String(balance) : ''); setShowPayForm(true) }}
className="w-full btn-primary justify-center" className="w-full btn-primary justify-center"
disabled={balance <= 0} disabled={balance <= 0}
> >
<Plus size={15} /> Принять оплату <Plus size={15} /> Принять оплату
</button> </button>
) : ( ) : (
<div className="rounded-xl border border-brand-200 dark:border-brand-700 p-3 space-y-3 bg-brand-50/40 dark:bg-brand-900/10"> <div className="rounded-xl border border-brand-200 dark:border-brand-700 p-3 space-y-3 bg-brand-50/40 dark:bg-brand-900/10">
<p className="text-xs font-semibold text-brand-700 dark:text-brand-400">Новый платёж</p> <p className="text-xs font-semibold text-brand-700 dark:text-brand-400">Новый платёж</p>
<div> <div>
<label className={lbl}>Сумма, </label> <label className={lbl}>Сумма, </label>
<input type="number" className="input text-sm" value={payAmount} <input type="number" className="input text-sm" value={payAmount}
onChange={e => setPayAmount(e.target.value)} onChange={e => setPayAmount(e.target.value)}
onFocus={e => e.target.select()} onFocus={e => e.target.select()}
placeholder="0" min="0" /> placeholder="0" min="0" />
</div> </div>
<div> <div>
<label className={lbl}>Способ оплаты</label> <label className={lbl}>Способ оплаты</label>
<div className="grid grid-cols-3 gap-1.5"> <div className="grid grid-cols-3 gap-1.5">
{METHODS.map(m => ( {METHODS.map(m => (
<button <button
key={m.id} type="button" onClick={() => setPayMethod(m.id)} key={m.id} type="button" onClick={() => setPayMethod(m.id)}
className={cn( className={cn(
'flex flex-col items-center gap-1 py-2 rounded-lg border text-xs font-medium transition-colors', 'flex flex-col items-center gap-1 py-2 rounded-lg border text-xs font-medium transition-colors',
payMethod === m.id payMethod === m.id
? 'bg-brand-600 border-brand-600 text-white' ? 'bg-brand-600 border-brand-600 text-white'
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300', : 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300',
)} )}
> >
<m.icon size={14} /> {m.label} <m.icon size={14} /> {m.label}
</button> </button>
))} ))}
</div>
</div>
<div>
<label className={lbl}>Комментарий</label>
<input className="input text-sm" value={payNote}
onChange={e => setPayNote(e.target.value)} placeholder="Необязательно" />
</div>
<div className="flex gap-2">
<button onClick={() => setShowPayForm(false)} className="btn-secondary flex-1 justify-center text-sm py-2">Отмена</button>
<button onClick={addPayment} disabled={!payAmount || parseFloat(payAmount) <= 0 || payAdding}
className="btn-primary flex-1 justify-center text-sm py-2 flex items-center gap-1">
{payAdding && <Loader2 size={13} className="animate-spin" />}
Принять
</button>
</div> </div>
</div> </div>
)}
{/* Payment history */}
{payments.length > 0 && (
<div> <div>
<label className={lbl}>Комментарий</label> <p className={lbl + ' mt-1'}>История платежей</p>
<input className="input text-sm" value={payNote} <div className="space-y-1.5">
onChange={e => setPayNote(e.target.value)} placeholder="Необязательно" /> {payments.map(p => {
const M = METHODS.find(m => m.id === p.method) ?? METHODS[0]
return (
<div key={p.id} className="flex items-center gap-2 text-sm px-2.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<M.icon size={13} className="text-slate-400 shrink-0" />
<span className="text-slate-400 text-xs shrink-0">{format(new Date(p.createdAt), 'dd.MM.yyyy')}</span>
<span className="flex-1 text-slate-600 dark:text-slate-300 text-xs truncate">{p.note || M.label}</span>
<span className="font-semibold text-emerald-600 dark:text-emerald-400 shrink-0">{formatCurrency(Number(p.amount))}</span>
</div>
)
})}
</div>
</div> </div>
<div className="flex gap-2"> )}
<button onClick={() => setShowPayForm(false)} className="btn-secondary flex-1 justify-center text-sm py-2">Отмена</button> </div>
<button onClick={addPayment} disabled={!payAmount || parseFloat(payAmount) <= 0 || payAdding}
className="btn-primary flex-1 justify-center text-sm py-2 flex items-center gap-1">
{payAdding && <Loader2 size={13} className="animate-spin" />}
Принять
</button>
</div>
</div>
)}
{/* Payment history */} {/* Правая колонка — депозит */}
{payments.length > 0 && (
<div>
<p className={lbl + ' mt-1'}>История платежей</p>
<div className="space-y-1.5">
{payments.map(p => {
const M = METHODS.find(m => m.id === p.method) ?? METHODS[0]
return (
<div key={p.id} className="flex items-center gap-2 text-sm px-2.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<M.icon size={13} className="text-slate-400 shrink-0" />
<span className="text-slate-400 text-xs shrink-0">{format(new Date(p.createdAt), 'dd.MM.yyyy')}</span>
<span className="flex-1 text-slate-600 dark:text-slate-300 text-xs truncate">{p.note || M.label}</span>
<span className="font-semibold text-emerald-600 dark:text-emerald-400 shrink-0">{formatCurrency(Number(p.amount))}</span>
</div>
)
})}
</div>
</div>
)}
{/* Deposit widget */}
{slug && ( {slug && (
<DepositWidget slug={slug} bookingId={booking.id} /> <div>
<DepositWidget slug={slug} bookingId={booking.id} />
</div>
)} )}
</div> </div>
)} )}