Add passport data tab to booking modal

Two tabs in the booking modal: «Бронирование» (existing) and «Документы гостя» (new).

Passport tab includes:
- Document type selector: Паспорт РФ / Загранпаспорт / Иной документ
- Full name: Фамилия / Имя / Отчество
- Date of birth, place of birth, nationality
- Document details: series + number (RF passport), or just number (other); issued by, issue date, division code (RF passport)
- Registration address
- Green dot indicator on tab when data is entered

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 22:28:43 +03:00
parent 96dfd755ed
commit 42f15b6c5b

View File

@@ -1,11 +1,43 @@
import { useState } from 'react'
import { format } from 'date-fns'
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2 } from 'lucide-react'
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays } from 'lucide-react'
import { Modal } from '../ui/Modal'
import { cn, BOOKING_STATUS_LABELS } from '../../lib/utils'
import type { Booking, DraftBooking, Room, BookingStatus } from '../../types'
import { FloorMapModal } from '../floormap/FloorMapModal'
type DocType = 'rf_passport' | 'foreign_passport' | 'other'
interface PassportData {
docType: DocType
lastName: string
firstName: string
patronymic: string
birthDate: string
birthPlace: string
nationality: string
series: string
number: string
issuedBy: string
issuedDate: string
divisionCode: string
registrationAddress: string
}
const EMPTY_PASSPORT: PassportData = {
docType: 'rf_passport',
lastName: '', firstName: '', patronymic: '',
birthDate: '', birthPlace: '', nationality: 'Россия',
series: '', number: '', issuedBy: '', issuedDate: '',
divisionCode: '', registrationAddress: '',
}
const DOC_TYPES: { id: DocType; label: string }[] = [
{ id: 'rf_passport', label: 'Паспорт РФ' },
{ id: 'foreign_passport', label: 'Загранпаспорт' },
{ id: 'other', label: 'Иной документ' },
]
const GUEST_TAGS = ['VIP', 'Постоянный гость', 'Корпоративный', 'Медовый месяц', 'День рождения', 'Особые пожелания']
const ADDITIONAL_SERVICES = [
@@ -48,6 +80,13 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
const [showFloorMap, setShowFloorMap] = useState(false)
const [addedServices, setAddedServices] = useState<AddedService[]>([])
const [selectedServiceId, setSelectedServiceId] = useState(ADDITIONAL_SERVICES[0].id)
const [activeTab, setActiveTab] = useState<'booking' | 'passport'>('booking')
const [passport, setPassport] = useState<PassportData>(EMPTY_PASSPORT)
const setP = <K extends keyof PassportData>(k: K, v: PassportData[K]) =>
setPassport(prev => ({ ...prev, [k]: v }))
const passportFilled = !!(passport.lastName || passport.number)
// Hourly booking state
const [isHourly, setIsHourly] = useState(false)
@@ -137,6 +176,35 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
</>
}
>
{/* ── Tabs ── */}
<div className="flex border-b border-slate-200 dark:border-slate-700 mb-4 -mt-1">
{([
{ id: 'booking' as const, label: 'Бронирование', icon: CalendarDays },
{ id: 'passport' as const, label: 'Документы гостя', icon: IdCard,
badge: passportFilled },
]).map(t => (
<button
key={t.id}
type="button"
onClick={() => setActiveTab(t.id)}
className={cn(
'flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
activeTab === t.id
? 'border-brand-600 text-brand-600 dark:text-brand-400 dark:border-brand-400'
: 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300',
)}
>
<t.icon size={14} />
{t.label}
{t.badge && (
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
)}
</button>
))}
</div>
{/* ── BOOKING TAB ── */}
{activeTab === 'booking' && (
<div className="space-y-4">
<div className="flex gap-6">
{/* ── LEFT COLUMN ── */}
@@ -534,6 +602,150 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
)}
</div>
</div>
)} {/* end booking tab */}
{/* ── PASSPORT TAB ── */}
{activeTab === 'passport' && (
<div className="space-y-4">
{/* Document type */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Тип документа</label>
<div className="flex gap-2">
{DOC_TYPES.map(dt => (
<button
key={dt.id}
type="button"
onClick={() => setP('docType', dt.id)}
className={cn(
'flex-1 py-2 rounded-lg text-sm font-medium border transition-colors',
passport.docType === dt.id
? 'bg-brand-600 border-brand-600 text-white'
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:border-brand-400',
)}
>
{dt.label}
</button>
))}
</div>
</div>
{/* Name fields */}
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Фамилия</label>
<input type="text" className="input" placeholder="Иванов"
value={passport.lastName} onChange={e => setP('lastName', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Имя</label>
<input type="text" className="input" placeholder="Иван"
value={passport.firstName} onChange={e => setP('firstName', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Отчество</label>
<input type="text" className="input" placeholder="Иванович"
value={passport.patronymic} onChange={e => setP('patronymic', e.target.value)} />
</div>
</div>
{/* Birth + nationality */}
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Дата рождения</label>
<input type="date" className="input"
value={passport.birthDate} onChange={e => setP('birthDate', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Место рождения</label>
<input type="text" className="input" placeholder="г. Москва"
value={passport.birthPlace} onChange={e => setP('birthPlace', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Гражданство</label>
<input type="text" className="input" placeholder="Россия"
value={passport.nationality} onChange={e => setP('nationality', e.target.value)} />
</div>
</div>
{/* Document fields */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
<div className="px-4 py-2.5 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600">
<p className="text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wide">Реквизиты документа</p>
</div>
<div className="p-4 space-y-3">
{passport.docType === 'rf_passport' && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Серия</label>
<input
type="text" className="input" placeholder="45 16" maxLength={5}
value={passport.series} onChange={e => setP('series', e.target.value)}
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Номер</label>
<input
type="text" className="input" placeholder="123456" maxLength={6}
value={passport.number} onChange={e => setP('number', e.target.value)}
/>
</div>
</div>
)}
{passport.docType !== 'rf_passport' && (
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Номер документа</label>
<input
type="text" className="input" placeholder="Номер документа"
value={passport.number} onChange={e => setP('number', e.target.value)}
/>
</div>
)}
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Кем выдан</label>
<input
type="text" className="input" placeholder="ОУМВД России по г. Москве"
value={passport.issuedBy} onChange={e => setP('issuedBy', e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Дата выдачи</label>
<input type="date" className="input"
value={passport.issuedDate} onChange={e => setP('issuedDate', e.target.value)} />
</div>
{passport.docType === 'rf_passport' && (
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Код подразделения</label>
<input
type="text" className="input" placeholder="770-001" maxLength={7}
value={passport.divisionCode} onChange={e => setP('divisionCode', e.target.value)}
/>
</div>
)}
</div>
</div>
</div>
{/* Registration address */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Адрес регистрации</label>
<input
type="text" className="input"
placeholder="г. Москва, ул. Примерная, д. 1, кв. 1"
value={passport.registrationAddress}
onChange={e => setP('registrationAddress', e.target.value)}
/>
</div>
{/* Hint */}
<p className="text-xs text-slate-400 dark:text-slate-500">
Данные используются для формирования регистрационной карты и уведомления УМВД (при необходимости)
</p>
</div>
)} {/* end passport tab */}
</Modal>
{showFloorMap && (