feat: blacklist, guest lookup, bank transfer payment page

- Add is_blacklisted column to guests (migration 068)
- POST /widget/bookings: block blacklisted guests (403, generic message)
- GET /widget/:slug/guests/lookup: find existing guest by email/phone for auto-fill
- Widget form: debounce guest lookup on email/phone input, show "Заполнить" suggestion
- Widget payment screen: replace fake card form with proper YooKassa redirect screen or bank transfer details page
- Add bank transfer details field to widget settings (saved as widget_bank_details)
- GuestsPage: blacklist toggle button in edit modal + 🚫 ЧС badge in table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 01:31:10 +03:00
parent 08d6629ae8
commit 2b20fc18b3
7 changed files with 277 additions and 90 deletions

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import {
Search, Star, Phone, Mail, User, TrendingUp, Award, Users,
Calendar, CreditCard, X, ChevronRight, MessageSquare, Tag,
Repeat2, ShieldCheck, Plus, ChevronsUpDown, AlertCircle, Loader2,
Repeat2, Shield, ShieldCheck, Plus, ChevronsUpDown, AlertCircle, Loader2,
} from 'lucide-react'
import { cn } from '../lib/utils'
import { api, type GuestApiType } from '../lib/api'
@@ -100,6 +100,7 @@ function GuestModal({ guestId, slug, onClose, onUpdated }: GuestModalProps) {
const [rating, setRating] = useState(3)
const [notes, setNotes] = useState('')
const [tags, setTags] = useState<string[]>([])
const [isBlacklisted, setIsBlacklisted] = useState(false)
// Passport
const [passportSeries, setPassportSeries] = useState('')
const [passportNumber, setPassportNumber] = useState('')
@@ -120,6 +121,7 @@ function GuestModal({ guestId, slug, onClose, onUpdated }: GuestModalProps) {
setRating(g.rating)
setNotes(g.notes ?? '')
setTags(g.tags ?? [])
setIsBlacklisted(g.isBlacklisted ?? false)
setPassportSeries(g.passportSeries ?? '')
setPassportNumber(g.passportNumber ?? '')
setBirthDate(g.birthDate?.slice(0, 10) ?? '')
@@ -143,6 +145,7 @@ function GuestModal({ guestId, slug, onClose, onUpdated }: GuestModalProps) {
rating,
notes,
tags,
is_blacklisted: isBlacklisted,
passport_series: passportSeries || undefined,
passport_number: passportNumber || undefined,
birth_date: birthDate || undefined,
@@ -317,6 +320,30 @@ function GuestModal({ guestId, slug, onClose, onUpdated }: GuestModalProps) {
</div>
</div>
{/* Blacklist */}
<div>
<button
onClick={() => setIsBlacklisted(v => !v)}
className={cn(
'w-full flex items-center justify-between px-4 py-3 rounded-xl border transition-colors',
isBlacklisted
? 'bg-red-50 border-red-300 text-red-700 dark:bg-red-900/20 dark:border-red-700 dark:text-red-400'
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-400 hover:border-red-300 hover:text-red-600',
)}
>
<div className="flex items-center gap-2 text-sm font-medium">
<Shield size={14} className={isBlacklisted ? 'text-red-500' : 'text-slate-400'} />
{isBlacklisted ? '🚫 Гость в чёрном списке' : 'Добавить в чёрный список'}
</div>
<span className="text-xs opacity-60">{isBlacklisted ? 'Нажмите чтобы убрать' : 'Нажмите чтобы добавить'}</span>
</button>
{isBlacklisted && (
<p className="text-xs text-red-600 dark:text-red-400 mt-1 px-1">
Этот гость не сможет забронировать номер через виджет онлайн-бронирования.
</p>
)}
</div>
{/* Notes */}
<div>
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
@@ -915,6 +942,11 @@ export function GuestsPage() {
</td>
<td className="px-4 py-3 hidden sm:table-cell">
<div className="flex flex-wrap gap-1">
{(guest as any).isBlacklisted && (
<span className="flex items-center gap-0.5 px-2 py-0.5 rounded-md text-xs font-medium border bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800">
🚫 ЧС
</span>
)}
{guest.tags.map(tag => (
<span
key={tag}