import { useState, useEffect, useRef } from 'react' import { format, differenceInDays } from 'date-fns' import { X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle, Printer, ScanLine, Banknote, Building2, Plus, Pencil, FileText, FileCheck, Receipt, IdCard, AlertTriangle, Trash2, Loader2, UserCheck, Baby, Search, LogIn, KeyRound, ShieldCheck, QrCode, Copy, RefreshCw, Ban, } from 'lucide-react' import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount, } from '../../lib/utils' import type { Booking, Room } from '../../types' import { api, type BookingGuest, type BookingGuestPayload, type GuestApiType, type BookingDeposit, type DepositSettings, type BookingPayment, type DepositPreset } from '../../lib/api' import { getIdentity, type AgentIdentity } from '../../lib/agent' const fmtDate = (iso: string) => new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(iso)) import { Badge } from '../ui/Badge' import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage' // ─── Local types ────────────────────────────────────────────────────────────── interface PassportData { lastName: string; firstName: string; middleName: string dob: string; series: string; number: string issuedBy: string; issueDate: string; regAddress: string } interface Payment { id: string; date: string; amount: number method: 'cash' | 'card' | 'transfer'; note: string } type Tab = 'booking' | 'guest' | 'payment' | 'docs' const TABS: { id: Tab; label: string }[] = [ { id: 'booking', label: 'Бронь' }, { id: 'guest', label: 'Гости' }, { id: 'payment', label: 'Оплата' }, { id: 'docs', label: 'Документы' }, ] const METHODS: { id: 'cash' | 'card' | 'transfer'; label: string; icon: React.ElementType }[] = [ { id: 'cash', label: 'Наличные', icon: Banknote }, { id: 'card', label: 'Карта', icon: CreditCard }, { id: 'transfer', label: 'Перевод', icon: Building2 }, ] const DOCUMENTS = [ { id: 'reg', label: 'Регистрационная карта', desc: 'Персональные данные гостя', icon: IdCard, always: true }, { id: 'contract', label: 'Договор на проживание', desc: 'Договор между гостем и отелем', icon: FileText, always: true }, { id: 'invoice', label: 'Счёт на оплату', desc: 'Счёт для оплаты проживания', icon: Receipt, always: true }, { id: 'act', label: 'Акт об оказании услуг', desc: 'Закрывающий документ при выезде', icon: FileCheck, always: false }, ] // ─── DepositWidget ──────────────────────────────────────────────────────────── type ReleaseItem = { id: string; name: string; amount: string } function DepositWidget({ slug, bookingId }: { slug: string; bookingId: string }) { const [depositSettings, setDepositSettings] = useState(null) const [deposit, setDeposit] = useState(null) const [depositLoading, setDepositLoading] = useState(true) const [depositCreating, setDepositCreating] = useState<'cash' | 'yookassa' | null>(null) const [depositCancelling, setDepositCancelling] = useState(false) const [depositReleasing, setDepositReleasing] = useState(false) const [showReleaseForm, setShowReleaseForm] = useState(false) const [releaseItems, setReleaseItems] = useState([]) const [releaseComment, setReleaseComment] = useState('') const [yookassaMsg, setYookassaMsg] = useState(null) const [releaseError, setReleaseError] = useState(null) const [presets, setPresets] = useState([]) const [minibarItems, setMinibarItems] = useState>([]) const [minibarTotal, setMinibarTotal] = useState(0) useEffect(() => { Promise.all([ api.deposits.getSettings(slug), api.deposits.getBookingDeposit(slug, bookingId).catch(() => null), api.deposits.getPresets(slug).catch(() => []), api.deposits.getMinibarForBooking(slug, bookingId).catch(() => ({ items: [], total: 0 })), ]).then(([settings, dep, presetList, minibar]) => { setDepositSettings(settings) setDeposit(dep) setPresets(presetList) setMinibarItems(minibar.items) setMinibarTotal(minibar.total) }).catch(() => { // silently ignore — deposit module may not be available }).finally(() => setDepositLoading(false)) }, [slug, bookingId]) const handleCreateCash = async () => { setDepositCreating('cash') try { const dep = await api.deposits.payByCash(slug, bookingId) setDeposit(dep) } catch { // ignore } finally { setDepositCreating(null) } } const handleCreateYookassa = async () => { setDepositCreating('yookassa') try { const dep = await api.deposits.createYookassaHold(slug, bookingId) setDeposit(dep) setYookassaMsg('QR-код активирован. Гость может сканировать QR на стойке ресепшена.') } catch { // ignore } finally { setDepositCreating(null) } } const handleRefresh = async () => { try { const dep = await api.deposits.getBookingDeposit(slug, bookingId) setDeposit(dep) } catch { // ignore } } const releaseTotalAmount = releaseItems.reduce((s, i) => s + (parseFloat(i.amount) || 0), 0) const handleRelease = async () => { setDepositReleasing(true) setReleaseError(null) try { const items = releaseItems .filter(i => parseFloat(i.amount) > 0) .map(i => ({ name: i.name, amount: parseFloat(i.amount) })) const dep = await api.deposits.release(slug, bookingId, releaseTotalAmount, releaseComment || undefined, items) setDeposit(dep) setShowReleaseForm(false) } catch { setReleaseError('Не удалось выполнить операцию') } finally { setDepositReleasing(false) } } const addReleaseItem = (name: string, amount: number) => { setReleaseItems(prev => [...prev, { id: `item-${Date.now()}`, name, amount: String(amount) }]) } const removeReleaseItem = (id: string) => { setReleaseItems(prev => prev.filter(i => i.id !== id)) } const handleCancel = async () => { if (!window.confirm('Отменить депозит?')) return setDepositCancelling(true) try { await api.deposits.cancel(slug, bookingId) setDeposit(null) setYookassaMsg(null) } catch { // ignore } finally { setDepositCancelling(false) } } const copyPayLink = () => { navigator.clipboard.writeText(`https://app.hotelsync.ru/${slug}/pay`).catch(() => {}) } if (depositLoading) { return } if (!depositSettings?.isEnabled) return null const badgeBase = 'inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full font-medium' return (

Депозит

{yookassaMsg && (
{yookassaMsg}
)} {/* No deposit yet */} {deposit === null && (

Сумма: {formatCurrency(depositSettings.amount)}

{depositSettings.yookassaShopId && ( )}
)} {/* hold_created — awaiting card payment */} {deposit?.status === 'hold_created' && (
Ожидает оплаты по карте
https://app.hotelsync.ru/{slug}/pay

QR-код на ресепшене активен

)} {/* hold_confirmed — held on card */} {deposit?.status === 'hold_confirmed' && (
✓ Холд подтверждён

{formatCurrency(deposit.amount)}

{!showReleaseForm && ( )}
)} {/* hold_confirmed — show card info */} {deposit?.status === 'hold_confirmed' && deposit.cardLast4 && (
{deposit.cardBrand ? `${deposit.cardBrand} ` : ''}•••• {deposit.cardLast4}
)} {/* paid_cash */} {deposit?.status === 'paid_cash' && (
✓ Наличными

{formatCurrency(deposit.amount)}

{!showReleaseForm && (
)}
)} {/* Release form */} {showReleaseForm && deposit && (deposit.status === 'hold_confirmed' || deposit.status === 'paid_cash') && (

Возврат / Удержание депозита

{/* Minibar items breakdown */} {minibarItems.length > 0 && (
Минибар — {formatCurrency(minibarTotal)}
{minibarItems.map((it, i) => { const priceChanged = Math.abs(it.currentPrice - it.recordedPrice) > 0.001 return (
{it.itemName} × {it.quantity} {' '} ({formatCurrency(it.currentPrice)} / шт.) {priceChanged && ( )} {formatCurrency(it.lineTotal)}
) })}
)} {/* Quick presets from settings */} {(presets.length > 0 || minibarTotal === 0) && (

Добавить позицию:

{presets.map(p => ( ))}
)} {presets.length === 0 && minibarTotal > 0 && ( )} {/* Added items */} {releaseItems.length > 0 && (
{releaseItems.map(item => (
{item.name} e.target.select()} onChange={e => setReleaseItems(prev => prev.map(i => i.id === item.id ? { ...i, amount: e.target.value } : i))} className="input text-xs w-24 text-right" placeholder="0" />
))}
Итого удержание: {formatCurrency(releaseTotalAmount)}
)} {/* No items — full return */} {releaseItems.length === 0 && (

✓ Полный возврат депозита

)}