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