BookingModal: replace guestName with FIO+phone fields, remove passport tab
- Add lastName/firstName/middleName/phone fields to booking form - Remove 'Документы гостя' tab entirely from BookingModal - Auto-parse existing guestName into FIO parts when editing - Add guestPhone to Booking type and propagate through CalendarPage/BookingsPage API calls - Add middle_name to guests table (migration 011) - Update guests backend route to include middle_name in all queries - Add middleName to GuestApiType in api.ts - Fix selectAcGuest in BookingDetailPanel to also fill middle_name - Auto-open guest form pre-filled with booking FIO when no guests exist - Add passport series+number lookup in BookingDetailPanel guest form Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
backend/migrations/011_guests_middle_name.sql
Normal file
3
backend/migrations/011_guests_middle_name.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add middle_name to guests table for autocomplete support
|
||||
ALTER TABLE guests
|
||||
ADD COLUMN IF NOT EXISTS middle_name VARCHAR(100);
|
||||
@@ -5,7 +5,7 @@ type SlugParam = { Params: { slug: string } }
|
||||
type SlugIdParam = { Params: { slug: string; id: string } }
|
||||
|
||||
const GUEST_FIELDS = `
|
||||
g.id, g.hotel_id, g.first_name, g.last_name, g.email, g.phone,
|
||||
g.id, g.hotel_id, g.first_name, g.last_name, g.middle_name, g.email, g.phone,
|
||||
g.passport, g.passport_series, g.passport_number,
|
||||
g.birth_date, g.nationality, g.gender, g.city, g.notes, g.tags,
|
||||
g.loyalty_tier, g.loyalty_points, g.rating,
|
||||
@@ -111,7 +111,7 @@ const guests: FastifyPluginAsync = async (fastify) => {
|
||||
// ── POST /api/hotels/:slug/guests ────────────────────────────────────────────
|
||||
// Creates a new guest or returns existing one if passport matches
|
||||
fastify.post<SlugParam & { Body: {
|
||||
first_name: string; last_name: string
|
||||
first_name: string; last_name: string; middle_name?: string
|
||||
email?: string; phone?: string
|
||||
passport?: string; passport_series?: string; passport_number?: string
|
||||
birth_date?: string; nationality?: string; gender?: string; city?: string
|
||||
@@ -140,12 +140,13 @@ const guests: FastifyPluginAsync = async (fastify) => {
|
||||
`UPDATE guests SET
|
||||
first_name = COALESCE($3, first_name),
|
||||
last_name = COALESCE($4, last_name),
|
||||
email = COALESCE($5, email),
|
||||
phone = COALESCE($6, phone),
|
||||
middle_name = COALESCE($5, middle_name),
|
||||
email = COALESCE($6, email),
|
||||
phone = COALESCE($7, phone),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1 AND hotel_id = $2
|
||||
RETURNING *`,
|
||||
[existing[0].id, hotelId, b.first_name || null, b.last_name || null, b.email || null, b.phone || null],
|
||||
[existing[0].id, hotelId, b.first_name || null, b.last_name || null, b.middle_name || null, b.email || null, b.phone || null],
|
||||
)
|
||||
return reply.code(200).send(updated)
|
||||
}
|
||||
@@ -153,13 +154,13 @@ const guests: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
const { rows: [guest] } = await db.query(
|
||||
`INSERT INTO guests
|
||||
(hotel_id, first_name, last_name, email, phone,
|
||||
(hotel_id, first_name, last_name, middle_name, email, phone,
|
||||
passport, passport_series, passport_number,
|
||||
birth_date, nationality, gender, city, notes, tags, rating)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
||||
RETURNING *`,
|
||||
[
|
||||
hotelId, b.first_name, b.last_name,
|
||||
hotelId, b.first_name, b.last_name, b.middle_name ?? null,
|
||||
b.email ?? null, b.phone ?? null,
|
||||
b.passport ?? null, b.passport_series ?? null, b.passport_number ?? null,
|
||||
b.birth_date ?? null, b.nationality ?? null, b.gender ?? null, b.city ?? null,
|
||||
@@ -172,7 +173,7 @@ const guests: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// ── PATCH /api/hotels/:slug/guests/:id ──────────────────────────────────────
|
||||
fastify.patch<SlugIdParam & { Body: Partial<{
|
||||
first_name: string; last_name: string
|
||||
first_name: string; last_name: string; middle_name: string
|
||||
email: string; phone: string
|
||||
passport: string; passport_series: string; passport_number: string
|
||||
birth_date: string; nationality: string; gender: string; city: string
|
||||
@@ -198,6 +199,7 @@ const guests: FastifyPluginAsync = async (fastify) => {
|
||||
}
|
||||
add('first_name', b.first_name)
|
||||
add('last_name', b.last_name)
|
||||
add('middle_name', b.middle_name)
|
||||
add('email', b.email)
|
||||
add('phone', b.phone)
|
||||
add('passport', b.passport)
|
||||
|
||||
@@ -96,6 +96,17 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
||||
setBgGuests(guests)
|
||||
setRequireDocs(Boolean((settings as { require_guest_docs?: boolean }).require_guest_docs))
|
||||
setBgLoaded(true)
|
||||
// Auto-open add form pre-filled with booking FIO when no guests yet
|
||||
if (guests.length === 0 && booking.status !== 'cancelled') {
|
||||
const parts = (booking.guestName ?? '').split(' ')
|
||||
setBgForm({
|
||||
last_name: parts[0] ?? '',
|
||||
first_name: parts[1] ?? '',
|
||||
middle_name: parts.slice(2).join(' ') || undefined,
|
||||
is_main: true,
|
||||
})
|
||||
setShowGuestForm(true)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setBgLoading(false))
|
||||
@@ -130,15 +141,34 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
||||
...f,
|
||||
first_name: g.firstName,
|
||||
last_name: g.lastName,
|
||||
middle_name: g.middleName ?? undefined,
|
||||
birth_date: g.birthDate ?? undefined,
|
||||
passport_series: g.passportSeries ?? undefined,
|
||||
passport_number: g.passportNumber ?? undefined,
|
||||
nationality: g.nationality ?? undefined,
|
||||
}))
|
||||
setAcQuery('')
|
||||
setAcQuery(g.lastName)
|
||||
setAcResults([])
|
||||
}
|
||||
|
||||
const passportTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const handlePassportInput = (field: 'passport_series' | 'passport_number', val: string) => {
|
||||
setBgForm(f => ({ ...f, [field]: val }))
|
||||
if (passportTimerRef.current) clearTimeout(passportTimerRef.current)
|
||||
const series = field === 'passport_series' ? val : (bgForm.passport_series ?? '')
|
||||
const number = field === 'passport_number' ? val : (bgForm.passport_number ?? '')
|
||||
const query = (series + number).trim()
|
||||
if (query.length < 4 || !slug) return
|
||||
passportTimerRef.current = setTimeout(() => {
|
||||
api.guests.list(slug, query)
|
||||
.then(results => {
|
||||
if (results.length === 1) selectAcGuest(results[0])
|
||||
})
|
||||
.catch(() => {/* ignore */})
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const openAddForm = () => {
|
||||
setEditingBg(null)
|
||||
setBgForm({ is_main: bgGuests.length === 0 })
|
||||
@@ -704,11 +734,11 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className={lbl}>Серия</label>
|
||||
<input className={`${fld} font-mono uppercase`} value={bgForm.passport_series ?? ''} onChange={e => setBgForm(f => ({ ...f, passport_series: e.target.value.toUpperCase() }))} placeholder="4510" maxLength={10} />
|
||||
<input className={`${fld} font-mono uppercase`} value={bgForm.passport_series ?? ''} onChange={e => handlePassportInput('passport_series', e.target.value.toUpperCase())} placeholder="4510" maxLength={10} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Номер</label>
|
||||
<input className={`${fld} font-mono`} value={bgForm.passport_number ?? ''} onChange={e => setBgForm(f => ({ ...f, passport_number: e.target.value }))} placeholder="123456" maxLength={10} />
|
||||
<input className={`${fld} font-mono`} value={bgForm.passport_number ?? ''} onChange={e => handlePassportInput('passport_number', e.target.value)} placeholder="123456" maxLength={10} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine, Minus, BedDouble, Tv2, Send, Loader2, CheckCircle2 } from 'lucide-react'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, Tag, Minus, BedDouble, Tv2, Send, Loader2, CheckCircle2 } from 'lucide-react'
|
||||
import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage'
|
||||
import type { Discount } from '../../pages/DiscountsPage'
|
||||
import { Modal } from '../ui/Modal'
|
||||
@@ -11,37 +11,6 @@ import { useModules } from '../../contexts/ModulesContext'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { api } from '../../lib/api'
|
||||
|
||||
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', 'Постоянный гость', 'Корпоративный', 'Медовый месяц', 'День рождения', 'Особые пожелания']
|
||||
|
||||
@@ -110,10 +79,14 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
}
|
||||
}
|
||||
|
||||
const _nameParts = (existing?.guestName ?? '').split(' ')
|
||||
const [form, setForm] = useState({
|
||||
roomId: existing?.roomId ?? draft.roomId,
|
||||
guestName: existing?.guestName ?? '',
|
||||
guestEmail:existing?.guestEmail ?? '',
|
||||
lastName: _nameParts[0] ?? '',
|
||||
firstName: _nameParts[1] ?? '',
|
||||
middleName: _nameParts.slice(2).join(' '),
|
||||
phone: existing?.guestPhone ?? '',
|
||||
guestEmail: existing?.guestEmail ?? '',
|
||||
checkIn: existing?.checkIn ?? draft.checkIn,
|
||||
checkOut: existing?.checkOut ?? draft.checkOut,
|
||||
adults: existing?.adults ?? 2,
|
||||
@@ -127,17 +100,10 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
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 [selectedDiscountId, setSelectedDiscountId] = useState<string>('')
|
||||
const activeDiscounts = MOCK_DISCOUNTS.filter(d => d.isActive)
|
||||
const selectedDiscount: Discount | undefined = activeDiscounts.find(d => d.id === selectedDiscountId)
|
||||
|
||||
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)
|
||||
const [hourlyDate, setHourlyDate] = useState(draft.checkIn)
|
||||
@@ -190,13 +156,14 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
setGuestTags(prev => prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag])
|
||||
|
||||
const handleSave = () => {
|
||||
if (!form.guestName) return
|
||||
if (!form.lastName || !form.firstName) return
|
||||
if (isHourly && room?.allowHourly) {
|
||||
if (!hourlyDate) return
|
||||
} else {
|
||||
if (!form.checkIn || !form.checkOut) return
|
||||
}
|
||||
|
||||
const guestName = [form.lastName, form.firstName, form.middleName].filter(Boolean).join(' ')
|
||||
const hourlyPrefix = isHourly && room?.allowHourly
|
||||
? `[Почасово: ${String(startHour).padStart(2, '0')}:00–${String(endHour).padStart(2, '0')}:00] `
|
||||
: ''
|
||||
@@ -206,10 +173,15 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
const checkOut = isHourly && room?.allowHourly ? hourlyDate : form.checkOut
|
||||
|
||||
onSave({
|
||||
...form,
|
||||
source: 'direct',
|
||||
roomId: form.roomId,
|
||||
guestName,
|
||||
guestEmail: form.guestEmail,
|
||||
guestPhone: form.phone,
|
||||
checkIn,
|
||||
checkOut,
|
||||
adults: form.adults,
|
||||
children: form.children,
|
||||
status: form.status,
|
||||
notes: hourlyPrefix + extraBedsPrefix + form.notes,
|
||||
totalAmount: total,
|
||||
paidAmount,
|
||||
@@ -217,6 +189,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
hotelId: 'hotel-1',
|
||||
guestId: existing?.guestId ?? `g-${Date.now()}`,
|
||||
createdAt: existing?.createdAt ?? format(new Date(), 'yyyy-MM-dd'),
|
||||
source: 'direct',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -232,41 +205,12 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
footer={
|
||||
<>
|
||||
<button onClick={onClose} className="btn-secondary">Отмена</button>
|
||||
<button onClick={handleSave} className="btn-primary" disabled={!form.guestName}>
|
||||
<button onClick={handleSave} className="btn-primary" disabled={!form.lastName || !form.firstName}>
|
||||
{existing ? 'Сохранить' : 'Создать бронирование'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{/* ── 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 flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
{/* ── LEFT COLUMN ── */}
|
||||
@@ -330,18 +274,57 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Guest name + email */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* ФИО гостя */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<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="Иван Иванов"
|
||||
value={form.guestName}
|
||||
onChange={e => set('guestName', e.target.value)}
|
||||
placeholder="Иванов"
|
||||
value={form.lastName}
|
||||
onChange={e => set('lastName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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="Иван"
|
||||
value={form.firstName}
|
||||
onChange={e => set('firstName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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="Иванович"
|
||||
value={form.middleName}
|
||||
onChange={e => set('middleName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Телефон + Email */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Телефон
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="input"
|
||||
placeholder="+7 (999) 000-00-00"
|
||||
value={form.phone}
|
||||
onChange={e => set('phone', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -801,159 +784,6 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)} {/* end booking tab */}
|
||||
|
||||
{/* ── PASSPORT TAB ── */}
|
||||
{activeTab === 'passport' && (
|
||||
<div className="space-y-4">
|
||||
{/* Document type + scan */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">Тип документа</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => alert('Для сканирования подключите RFID/OCR сканер паспорта')}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-dashed border-brand-300 dark:border-brand-600 text-brand-600 dark:text-brand-400 text-xs font-medium hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors shrink-0"
|
||||
>
|
||||
<ScanLine size={13} /> Сканировать
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-0" />
|
||||
<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>
|
||||
|
||||
|
||||
@@ -459,6 +459,7 @@ export interface GuestApiType {
|
||||
hotelId: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
middleName: string | null
|
||||
email: string | null
|
||||
phone: string | null
|
||||
passport: string | null
|
||||
@@ -493,6 +494,7 @@ export interface GuestApiType {
|
||||
export interface GuestPayload {
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
middle_name?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
passport?: string
|
||||
|
||||
@@ -108,7 +108,7 @@ export function BookingsPage() {
|
||||
const handleCreateBooking = async (data: Partial<Booking>) => {
|
||||
try {
|
||||
const created = await api.bookings.create(slug, {
|
||||
roomId: data.roomId, guestName: data.guestName, guestEmail: data.guestEmail,
|
||||
roomId: data.roomId, guestName: data.guestName, guestEmail: data.guestEmail, guestPhone: data.guestPhone,
|
||||
checkIn: data.checkIn, checkOut: data.checkOut,
|
||||
adults: data.adults, children: data.children,
|
||||
status: data.status, source: data.source,
|
||||
@@ -124,7 +124,7 @@ export function BookingsPage() {
|
||||
const handleUpdateBooking = async (id: string, data: Partial<Booking>) => {
|
||||
try {
|
||||
const updated = await api.bookings.update(slug, id, {
|
||||
guestName: data.guestName, guestEmail: data.guestEmail,
|
||||
guestName: data.guestName, guestEmail: data.guestEmail, guestPhone: data.guestPhone,
|
||||
checkIn: data.checkIn, checkOut: data.checkOut,
|
||||
adults: data.adults, children: data.children,
|
||||
status: data.status, source: data.source,
|
||||
|
||||
@@ -63,7 +63,7 @@ export function CalendarPage() {
|
||||
const handleCreate = async (data: Partial<Booking>) => {
|
||||
try {
|
||||
const created = await api.bookings.create(slug, {
|
||||
roomId: data.roomId, guestName: data.guestName, guestEmail: data.guestEmail,
|
||||
roomId: data.roomId, guestName: data.guestName, guestEmail: data.guestEmail, guestPhone: data.guestPhone,
|
||||
checkIn: data.checkIn, checkOut: data.checkOut,
|
||||
adults: data.adults, children: data.children,
|
||||
status: data.status, source: data.source,
|
||||
|
||||
@@ -147,6 +147,7 @@ export interface Booking {
|
||||
guestId: string
|
||||
guestName: string
|
||||
guestEmail: string
|
||||
guestPhone?: string
|
||||
checkIn: string // ISO date YYYY-MM-DD
|
||||
checkOut: string // ISO date YYYY-MM-DD
|
||||
status: BookingStatus
|
||||
|
||||
Reference in New Issue
Block a user