ui: боковая панель — ширина 560px, вкладка «Оплата» двухколоночная
- w-[400px] → w-[560px] для боковой панели бронирования - Вкладка «Оплата»: расчёт + платежи слева, депозит справа (grid 2 cols) - На мобиле — одна колонка как прежде Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 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 && (
|
||||
@@ -1636,125 +1636,131 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
||||
|
||||
{/* ── Оплата ───────────────────────────────────────────────────────── */}
|
||||
{tab === 'payment' && (
|
||||
<div className="space-y-4">
|
||||
{/* 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>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-start">
|
||||
|
||||
{/* 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">
|
||||
<span className="text-slate-500 dark:text-slate-400">Стоимость</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">{formatCurrency(baseTotal)}</span>
|
||||
{/* Левая колонка — расчёт и приём оплаты */}
|
||||
<div className="space-y-4">
|
||||
{/* 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>
|
||||
{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">
|
||||
<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>
|
||||
<span className="text-slate-500 dark:text-slate-400">Стоимость</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 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>
|
||||
|
||||
{/* Accept payment */}
|
||||
{!showPayForm ? (
|
||||
<button
|
||||
onClick={() => { setPayAmount(balance > 0 ? String(balance) : ''); setShowPayForm(true) }}
|
||||
className="w-full btn-primary justify-center"
|
||||
disabled={balance <= 0}
|
||||
>
|
||||
<Plus size={15} /> Принять оплату
|
||||
</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">
|
||||
<p className="text-xs font-semibold text-brand-700 dark:text-brand-400">Новый платёж</p>
|
||||
<div>
|
||||
<label className={lbl}>Сумма, ₽</label>
|
||||
<input type="number" className="input text-sm" value={payAmount}
|
||||
onChange={e => setPayAmount(e.target.value)}
|
||||
onFocus={e => e.target.select()}
|
||||
placeholder="0" min="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Способ оплаты</label>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{METHODS.map(m => (
|
||||
<button
|
||||
key={m.id} type="button" onClick={() => setPayMethod(m.id)}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-1 py-2 rounded-lg border text-xs font-medium transition-colors',
|
||||
payMethod === m.id
|
||||
? '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',
|
||||
)}
|
||||
>
|
||||
<m.icon size={14} /> {m.label}
|
||||
</button>
|
||||
))}
|
||||
{/* Accept payment */}
|
||||
{!showPayForm ? (
|
||||
<button
|
||||
onClick={() => { setPayAmount(balance > 0 ? String(balance) : ''); setShowPayForm(true) }}
|
||||
className="w-full btn-primary justify-center"
|
||||
disabled={balance <= 0}
|
||||
>
|
||||
<Plus size={15} /> Принять оплату
|
||||
</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">
|
||||
<p className="text-xs font-semibold text-brand-700 dark:text-brand-400">Новый платёж</p>
|
||||
<div>
|
||||
<label className={lbl}>Сумма, ₽</label>
|
||||
<input type="number" className="input text-sm" value={payAmount}
|
||||
onChange={e => setPayAmount(e.target.value)}
|
||||
onFocus={e => e.target.select()}
|
||||
placeholder="0" min="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Способ оплаты</label>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{METHODS.map(m => (
|
||||
<button
|
||||
key={m.id} type="button" onClick={() => setPayMethod(m.id)}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-1 py-2 rounded-lg border text-xs font-medium transition-colors',
|
||||
payMethod === m.id
|
||||
? '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',
|
||||
)}
|
||||
>
|
||||
<m.icon size={14} /> {m.label}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Payment history */}
|
||||
{payments.length > 0 && (
|
||||
<div>
|
||||
<label className={lbl}>Комментарий</label>
|
||||
<input className="input text-sm" value={payNote}
|
||||
onChange={e => setPayNote(e.target.value)} placeholder="Необязательно" />
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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 && (
|
||||
<DepositWidget slug={slug} bookingId={booking.id} />
|
||||
<div>
|
||||
<DepositWidget slug={slug} bookingId={booking.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user