Redesign booking detail panel with 4 tabs
- Бронь: room/dates/guests/contact (existing info) - Гость: passport data form + scan button - Оплата: discount selector, accept payment form, payment history - Документы: print buttons for reg card/contract/invoice/act - Footer: debt warning banner before check-in Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,53 @@
|
||||
import { X, Mail, Phone, Calendar, Users, CreditCard, Tag, Edit2, CheckCircle, XCircle } from 'lucide-react'
|
||||
import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount } from '../../lib/utils'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle,
|
||||
Printer, ScanLine, Banknote, Building2, Plus,
|
||||
FileText, FileCheck, Receipt, IdCard, AlertTriangle,
|
||||
} 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 { 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 },
|
||||
]
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface BookingDetailPanelProps {
|
||||
booking: Booking
|
||||
@@ -11,23 +57,85 @@ interface BookingDetailPanelProps {
|
||||
}
|
||||
|
||||
export function BookingDetailPanel({ booking, room, onClose, onUpdate }: BookingDetailPanelProps) {
|
||||
const [tab, setTab] = useState<Tab>('booking')
|
||||
|
||||
// Passport data
|
||||
const nameParts = booking.guestName.split(' ')
|
||||
const [passport, setPassport] = useState<PassportData>({
|
||||
lastName: nameParts[0] ?? '', firstName: nameParts[1] ?? '', middleName: nameParts[2] ?? '',
|
||||
dob: '', series: '', number: '', issuedBy: '', issueDate: '', regAddress: '',
|
||||
})
|
||||
const setP = <K extends keyof PassportData>(k: K, v: PassportData[K]) =>
|
||||
setPassport(prev => ({ ...prev, [k]: v }))
|
||||
|
||||
// Payments
|
||||
const [payments, setPayments] = useState<Payment[]>(() =>
|
||||
booking.paidAmount > 0
|
||||
? [{ id: 'init', date: booking.createdAt, amount: booking.paidAmount, method: 'card', note: 'Предоплата при бронировании' }]
|
||||
: []
|
||||
)
|
||||
const [showPayForm, setShowPayForm] = useState(false)
|
||||
const [payAmount, setPayAmount] = useState('')
|
||||
const [payMethod, setPayMethod] = useState<'cash' | 'card' | 'transfer'>('cash')
|
||||
const [payNote, setPayNote] = useState('')
|
||||
|
||||
// Discount
|
||||
const [discountId, setDiscountId] = useState('')
|
||||
|
||||
// Toast
|
||||
const [toast, setToast] = useState('')
|
||||
const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 2500) }
|
||||
|
||||
// Calculations
|
||||
const nights = nightsCount(booking.checkIn, booking.checkOut)
|
||||
const balance = booking.totalAmount - booking.paidAmount
|
||||
const baseTotal = booking.totalAmount
|
||||
const activeDiscs = MOCK_DISCOUNTS.filter(d => d.isActive)
|
||||
const selDiscount = activeDiscs.find(d => d.id === discountId)
|
||||
const discountAmt = selDiscount
|
||||
? selDiscount.valueType === 'percent'
|
||||
? Math.round(baseTotal * Math.min(100, selDiscount.value) / 100)
|
||||
: Math.min(baseTotal, selDiscount.value)
|
||||
: 0
|
||||
const finalTotal = baseTotal - discountAmt
|
||||
const totalPaid = payments.reduce((s, p) => s + p.amount, 0)
|
||||
const balance = finalTotal - totalPaid
|
||||
|
||||
const setStatus = (status: typeof booking.status) => onUpdate(booking.id, { status })
|
||||
|
||||
const addPayment = () => {
|
||||
const amt = parseFloat(payAmount)
|
||||
if (!amt || amt <= 0) return
|
||||
setPayments(prev => [...prev, {
|
||||
id: `p-${Date.now()}`,
|
||||
date: new Date().toLocaleDateString('ru-RU'),
|
||||
amount: amt, method: payMethod, note: payNote,
|
||||
}])
|
||||
setPayAmount(''); setPayNote('')
|
||||
setShowPayForm(false)
|
||||
showToast(`✓ Оплата ${formatCurrency(amt)} принята`)
|
||||
}
|
||||
|
||||
const printDoc = (label: string) => showToast(`🖨 «${label}» отправлен на печать`)
|
||||
|
||||
const fld = 'input text-sm'
|
||||
const lbl = 'block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1'
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 z-40" onClick={onClose} />
|
||||
|
||||
{/* Panel */}
|
||||
<div className="fixed right-0 top-0 bottom-0 z-50 w-96 bg-white dark:bg-slate-800 border-l border-slate-200 dark:border-slate-700 shadow-2xl flex flex-col animate-slide-in">
|
||||
<div className="fixed right-0 top-0 bottom-0 z-50 w-[400px] bg-white dark:bg-slate-800 border-l border-slate-200 dark:border-slate-700 shadow-2xl flex flex-col">
|
||||
|
||||
{/* Toast notification */}
|
||||
{toast && (
|
||||
<div className="absolute top-3 left-4 right-4 z-20 bg-slate-900 dark:bg-slate-700 text-white text-sm rounded-xl px-4 py-2.5 text-center shadow-lg pointer-events-none">
|
||||
{toast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className={cn(
|
||||
'px-5 py-4 border-b border-slate-200 dark:border-slate-700',
|
||||
'flex items-center justify-between shrink-0',
|
||||
)}>
|
||||
<div className="px-5 pt-4 pb-3 border-b border-slate-200 dark:border-slate-700 shrink-0">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900 dark:text-slate-100">
|
||||
{booking.guestName}
|
||||
@@ -41,25 +149,48 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="btn-ghost p-1.5">
|
||||
<button onClick={onClose} className="btn-ghost p-1.5 mt-0.5">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
|
||||
{/* Room */}
|
||||
{/* Tab bar */}
|
||||
<div className="flex gap-0.5 mt-3">
|
||||
{TABS.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setTab(t.id)}
|
||||
className={cn(
|
||||
'flex-1 py-1.5 text-xs font-medium rounded-lg transition-colors',
|
||||
tab === t.id
|
||||
? 'bg-brand-600 text-white'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700',
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||
|
||||
{/* ── Бронь ────────────────────────────────────────────────────────── */}
|
||||
{tab === 'booking' && (
|
||||
<div className="space-y-4">
|
||||
{room && (
|
||||
<div className="rounded-xl bg-slate-50 dark:bg-slate-700/40 p-3">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1">Номер</p>
|
||||
<p className="font-semibold text-slate-900 dark:text-slate-100">
|
||||
№{room.number} — {room.type}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">Этаж {room.floor} · {room.bedType} bed</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Этаж {room.floor} · {room.bedType} bed
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
|
||||
@@ -78,11 +209,10 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-3">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'}
|
||||
</p>
|
||||
|
||||
{/* Guests */}
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
|
||||
<Users size={12} /> Гости
|
||||
@@ -92,46 +222,19 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
{booking.guestEmail && (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium">Контакты</p>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Mail size={13} className="text-slate-400" />
|
||||
{booking.guestEmail || '—'}
|
||||
{booking.guestEmail}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment */}
|
||||
<div className="rounded-xl border border-slate-200 dark:border-slate-600 p-3 space-y-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1">
|
||||
<CreditCard size={12} /> Оплата
|
||||
</p>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-600 dark:text-slate-400">Стоимость</span>
|
||||
<span className="font-semibold text-slate-900 dark:text-slate-100">
|
||||
{formatCurrency(booking.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-600 dark:text-slate-400">Оплачено</span>
|
||||
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
|
||||
{formatCurrency(booking.paidAmount)}
|
||||
</span>
|
||||
</div>
|
||||
{balance > 0 && (
|
||||
<div className="flex justify-between text-sm border-t border-slate-200 dark:border-slate-600 pt-2">
|
||||
<span className="text-slate-600 dark:text-slate-400">Остаток</span>
|
||||
<span className="font-bold text-red-600 dark:text-red-400">
|
||||
{formatCurrency(balance)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
{booking.notes && (
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1 flex items-center gap-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
|
||||
<Tag size={12} /> Примечания
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300 bg-slate-50 dark:bg-slate-700/40 rounded-lg p-2.5">
|
||||
@@ -140,7 +243,6 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Channel ID */}
|
||||
{booking.channelBookingId && (
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-0.5">ID канала</p>
|
||||
@@ -150,25 +252,240 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Гость ────────────────────────────────────────────────────────── */}
|
||||
{tab === 'guest' && (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label className={lbl}>Фамилия</label>
|
||||
<input className={fld} value={passport.lastName} onChange={e => setP('lastName', e.target.value)} placeholder="Иванов" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className={lbl}>Имя</label>
|
||||
<input className={fld} value={passport.firstName} onChange={e => setP('firstName', e.target.value)} placeholder="Иван" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Отчество</label>
|
||||
<input className={fld} value={passport.middleName} onChange={e => setP('middleName', e.target.value)} placeholder="Иванович" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Дата рождения</label>
|
||||
<input type="date" className={fld} value={passport.dob} onChange={e => setP('dob', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-400 mb-3">Паспорт</p>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className={lbl}>Серия</label>
|
||||
<input className={fld} value={passport.series} onChange={e => setP('series', e.target.value)} placeholder="4510" maxLength={4} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Номер</label>
|
||||
<input className={fld} value={passport.number} onChange={e => setP('number', e.target.value)} placeholder="123456" maxLength={6} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Кем выдан</label>
|
||||
<input className={fld} value={passport.issuedBy} onChange={e => setP('issuedBy', e.target.value)} placeholder="УФМС России по г. Москве" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Дата выдачи</label>
|
||||
<input type="date" className={fld} value={passport.issueDate} onChange={e => setP('issueDate', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Адрес регистрации</label>
|
||||
<input className={fld} value={passport.regAddress} onChange={e => setP('regAddress', e.target.value)} placeholder="г. Москва, ул. Примерная, д. 1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="shrink-0 px-5 py-4 border-t border-slate-200 dark:border-slate-700 space-y-2">
|
||||
{booking.status === 'confirmed' && (
|
||||
<button
|
||||
onClick={() => setStatus('checked_in')}
|
||||
onClick={() => showToast('Для сканирования подключите RFID/OCR сканер')}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl border-2 border-dashed border-brand-300 dark:border-brand-700 text-brand-600 dark:text-brand-400 text-sm font-medium hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
|
||||
>
|
||||
<ScanLine size={16} /> Сканировать паспорт
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => showToast('✓ Данные гостя сохранены')}
|
||||
className="w-full btn-primary justify-center"
|
||||
>
|
||||
<CheckCircle size={15} />
|
||||
Заселить
|
||||
Сохранить данные
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Оплата ───────────────────────────────────────────────────────── */}
|
||||
{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>
|
||||
|
||||
{/* 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>
|
||||
{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>
|
||||
|
||||
{/* 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)} 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}
|
||||
className="btn-primary flex-1 justify-center text-sm py-2">Принять</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)!
|
||||
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">{p.date}</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(p.amount)}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Документы ────────────────────────────────────────────────────── */}
|
||||
{tab === 'docs' && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-3">
|
||||
Распечатайте документы для гостя
|
||||
</p>
|
||||
{DOCUMENTS
|
||||
.filter(d => d.always || booking.status === 'checked_in' || booking.status === 'checked_out')
|
||||
.map(doc => (
|
||||
<div key={doc.id} className="flex items-center gap-3 p-3 rounded-xl border border-slate-200 dark:border-slate-600 hover:border-brand-300 dark:hover:border-brand-600 transition-colors">
|
||||
<div className="w-9 h-9 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center shrink-0">
|
||||
<doc.icon size={17} className="text-slate-500 dark:text-slate-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{doc.label}</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{doc.desc}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => printDoc(doc.label)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-brand-600 text-white text-xs font-medium hover:bg-brand-700 transition-colors shrink-0"
|
||||
>
|
||||
<Printer size={12} /> Печать
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="shrink-0 px-5 py-4 border-t border-slate-200 dark:border-slate-700 space-y-2">
|
||||
{balance > 0 && booking.status === 'confirmed' && (
|
||||
<div className="flex items-center gap-2 p-2.5 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700">
|
||||
<AlertTriangle size={14} className="text-amber-500 shrink-0" />
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||||
Долг {formatCurrency(balance)} — получите оплату перед заселением
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{booking.status === 'confirmed' && (
|
||||
<button onClick={() => setStatus('checked_in')} className="w-full btn-primary justify-center">
|
||||
<CheckCircle size={15} /> Заселить
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'checked_in' && (
|
||||
<button
|
||||
onClick={() => setStatus('checked_out')}
|
||||
className="w-full btn-primary justify-center bg-emerald-600 hover:bg-emerald-700"
|
||||
>
|
||||
<CheckCircle size={15} />
|
||||
Выселить
|
||||
<button onClick={() => setStatus('checked_out')} className="w-full btn-primary justify-center bg-emerald-600 hover:bg-emerald-700">
|
||||
<CheckCircle size={15} /> Выселить
|
||||
</button>
|
||||
)}
|
||||
{(booking.status === 'confirmed' || booking.status === 'inquiry') && (
|
||||
@@ -176,10 +493,10 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking
|
||||
onClick={() => setStatus('cancelled')}
|
||||
className="w-full btn-secondary justify-center text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<XCircle size={15} />
|
||||
Отменить
|
||||
<XCircle size={15} /> Отменить
|
||||
</button>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-center text-slate-400 dark:text-slate-500">
|
||||
ID: {booking.id} · Создано: {booking.createdAt}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user