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:
@@ -1,11 +1,43 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { format } from 'date-fns'
|
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 { Modal } from '../ui/Modal'
|
||||||
import { cn, BOOKING_STATUS_LABELS } from '../../lib/utils'
|
import { cn, BOOKING_STATUS_LABELS } from '../../lib/utils'
|
||||||
import type { Booking, DraftBooking, Room, BookingStatus } from '../../types'
|
import type { Booking, DraftBooking, Room, BookingStatus } from '../../types'
|
||||||
import { FloorMapModal } from '../floormap/FloorMapModal'
|
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 GUEST_TAGS = ['VIP', 'Постоянный гость', 'Корпоративный', 'Медовый месяц', 'День рождения', 'Особые пожелания']
|
||||||
|
|
||||||
const ADDITIONAL_SERVICES = [
|
const ADDITIONAL_SERVICES = [
|
||||||
@@ -48,6 +80,13 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
|||||||
const [showFloorMap, setShowFloorMap] = useState(false)
|
const [showFloorMap, setShowFloorMap] = useState(false)
|
||||||
const [addedServices, setAddedServices] = useState<AddedService[]>([])
|
const [addedServices, setAddedServices] = useState<AddedService[]>([])
|
||||||
const [selectedServiceId, setSelectedServiceId] = useState(ADDITIONAL_SERVICES[0].id)
|
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
|
// Hourly booking state
|
||||||
const [isHourly, setIsHourly] = useState(false)
|
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="space-y-4">
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
{/* ── LEFT COLUMN ── */}
|
{/* ── LEFT COLUMN ── */}
|
||||||
@@ -534,6 +602,150 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</Modal>
|
||||||
|
|
||||||
{showFloorMap && (
|
{showFloorMap && (
|
||||||
|
|||||||
Reference in New Issue
Block a user