- DepositSettingsPage: deduction presets management (add/edit/delete inline) - DepositHistoryPage: "← Назад" link back to settings - Amount inputs: onFocus selects all text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
170 lines
8.1 KiB
TypeScript
170 lines
8.1 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { Loader2, ShieldCheck, CreditCard, Banknote, RefreshCw, ArrowLeft } from 'lucide-react'
|
||
import { Link } from 'react-router-dom'
|
||
import { api, type BookingDeposit } from '../lib/api'
|
||
import { useAuth } from '../contexts/AuthContext'
|
||
import { cn, formatCurrency } from '../lib/utils'
|
||
import { format } from 'date-fns'
|
||
|
||
const STATUS_LABEL: Record<string, { label: string; cls: string }> = {
|
||
hold_created: { label: 'Ожидает оплаты', cls: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400' },
|
||
hold_confirmed: { label: 'Холд подтверждён', cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' },
|
||
paid_cash: { label: 'Наличными', cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' },
|
||
captured: { label: 'Списано', cls: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300' },
|
||
refunded: { label: 'Возвращён', cls: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' },
|
||
cancelled: { label: 'Отменён', cls: 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400' },
|
||
}
|
||
|
||
export function DepositHistoryPage() {
|
||
const { user } = useAuth()
|
||
const slug = user?.hotelSlug ?? ''
|
||
|
||
const [deposits, setDeposits] = useState<BookingDeposit[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [filter, setFilter] = useState<string>('all')
|
||
|
||
const load = () => {
|
||
if (!slug) return
|
||
setLoading(true)
|
||
api.deposits.history(slug)
|
||
.then(setDeposits)
|
||
.catch(() => setDeposits([]))
|
||
.finally(() => setLoading(false))
|
||
}
|
||
|
||
useEffect(() => { load() }, [slug])
|
||
|
||
const filtered = filter === 'all'
|
||
? deposits
|
||
: deposits.filter(d => d.status === filter)
|
||
|
||
return (
|
||
<div className="p-4 md:p-6 space-y-5 max-w-4xl">
|
||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||
<div className="flex items-center gap-3">
|
||
<Link to="/settings/deposit" className="flex items-center gap-1.5 text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors">
|
||
<ArrowLeft size={16} />
|
||
Назад
|
||
</Link>
|
||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
|
||
<ShieldCheck size={22} className="text-brand-600" />
|
||
История депозитов
|
||
</h1>
|
||
</div>
|
||
<button onClick={load} className="btn-secondary flex items-center gap-1.5 text-sm">
|
||
<RefreshCw size={14} />
|
||
Обновить
|
||
</button>
|
||
</div>
|
||
|
||
{/* Filter tabs */}
|
||
<div className="flex gap-1 flex-wrap">
|
||
{[
|
||
{ id: 'all', label: 'Все' },
|
||
{ id: 'hold_created', label: 'Ожидают' },
|
||
{ id: 'hold_confirmed', label: 'Холд' },
|
||
{ id: 'paid_cash', label: 'Наличные' },
|
||
{ id: 'captured', label: 'Списаны' },
|
||
{ id: 'refunded', label: 'Возвращены' },
|
||
].map(f => (
|
||
<button
|
||
key={f.id}
|
||
onClick={() => setFilter(f.id)}
|
||
className={cn(
|
||
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
||
filter === f.id
|
||
? 'bg-brand-600 text-white'
|
||
: 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600',
|
||
)}
|
||
>
|
||
{f.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="flex justify-center py-10">
|
||
<Loader2 size={24} className="animate-spin text-brand-600" />
|
||
</div>
|
||
) : filtered.length === 0 ? (
|
||
<p className="text-sm text-slate-400 dark:text-slate-500 py-8 text-center">
|
||
Нет депозитов
|
||
</p>
|
||
) : (
|
||
<div className="card overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-slate-100 dark:border-slate-700 text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wide">
|
||
<th className="text-left px-4 py-3 font-medium">Дата</th>
|
||
<th className="text-left px-4 py-3 font-medium">Номер</th>
|
||
<th className="text-left px-4 py-3 font-medium">Гость</th>
|
||
<th className="text-left px-4 py-3 font-medium">Метод</th>
|
||
<th className="text-right px-4 py-3 font-medium">Сумма</th>
|
||
<th className="text-left px-4 py-3 font-medium">Статус</th>
|
||
<th className="text-right px-4 py-3 font-medium">Удержано</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-50 dark:divide-slate-800">
|
||
{filtered.map(dep => {
|
||
const st = STATUS_LABEL[dep.status] ?? { label: dep.status, cls: 'bg-slate-100 text-slate-600' }
|
||
return (
|
||
<tr key={dep.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-colors">
|
||
<td className="px-4 py-3 text-slate-600 dark:text-slate-300 whitespace-nowrap">
|
||
{format(new Date(dep.createdAt), 'dd.MM.yyyy')}
|
||
</td>
|
||
<td className="px-4 py-3 text-slate-700 dark:text-slate-200 font-medium whitespace-nowrap">
|
||
{dep.roomNumber ? `№${dep.roomNumber}` : '—'}
|
||
</td>
|
||
<td className="px-4 py-3 text-slate-700 dark:text-slate-200">
|
||
<div>{dep.guestName ?? '—'}</div>
|
||
{dep.guestEmail && (
|
||
<div className="text-xs text-slate-400">{dep.guestEmail}</div>
|
||
)}
|
||
</td>
|
||
<td className="px-4 py-3 whitespace-nowrap">
|
||
{dep.paymentMethod === 'yookassa_hold' ? (
|
||
<div className="flex items-center gap-1.5 text-slate-600 dark:text-slate-300">
|
||
<CreditCard size={13} />
|
||
<span>
|
||
{dep.cardBrand ? `${dep.cardBrand} ` : ''}
|
||
{dep.cardLast4 ? `•••• ${dep.cardLast4}` : 'ЮКасса'}
|
||
</span>
|
||
</div>
|
||
) : dep.paymentMethod === 'cash' ? (
|
||
<div className="flex items-center gap-1.5 text-slate-600 dark:text-slate-300">
|
||
<Banknote size={13} />
|
||
<span>Наличные</span>
|
||
</div>
|
||
) : '—'}
|
||
</td>
|
||
<td className="px-4 py-3 text-right font-medium text-slate-900 dark:text-slate-100 whitespace-nowrap">
|
||
{formatCurrency(dep.amount)}
|
||
</td>
|
||
<td className="px-4 py-3">
|
||
<span className={cn('inline-flex items-center text-xs px-2 py-0.5 rounded-full font-medium', st.cls)}>
|
||
{st.label}
|
||
</span>
|
||
{dep.retentionReason && (
|
||
<div className="text-xs text-slate-400 mt-0.5 max-w-[160px] truncate" title={dep.retentionReason}>
|
||
{dep.retentionReason}
|
||
</div>
|
||
)}
|
||
</td>
|
||
<td className="px-4 py-3 text-right text-slate-600 dark:text-slate-300 whitespace-nowrap">
|
||
{dep.capturedAmount != null && dep.capturedAmount > 0
|
||
? formatCurrency(dep.capturedAmount)
|
||
: dep.status === 'refunded' ? '0 ₽' : '—'}
|
||
</td>
|
||
</tr>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|