Files
hotelsync/src/pages/DepositHistoryPage.tsx
HotelSync d686f5abe6 feat: deposit presets UI, back button in history, onFocus select on inputs
- 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>
2026-04-06 13:03:20 +03:00

170 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}