Add NetUP IPTV integration module (TV Welcome settings page)
- DB: migrations/002_netup.sql — netup_settings + netup_room_mapping tables - Backend: routes/netup.ts — GET/PATCH settings, POST test, GET/POST room mappings, POST send TV message; notifyNetupCheckin/Checkout helpers - Backend: bookings PATCH — fire-and-forget NetUP check-in/out on status change - Frontend: TvWelcomePage — connection settings tab + room mapping tab - Frontend: sidebarItem added to tv-welcome module (shows in sidebar menu) - Frontend: ModulesPage Settings button navigates to /tv-welcome - Frontend: BookingModal — "Сообщение гостю на TV" panel for existing bookings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { useState } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine, Minus, BedDouble } from 'lucide-react'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine, 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'
|
||||
import { cn, BOOKING_STATUS_LABELS } from '../../lib/utils'
|
||||
import type { Booking, DraftBooking, Room, BookingStatus } from '../../types'
|
||||
import { FloorMapModal } from '../floormap/FloorMapModal'
|
||||
import { useModules } from '../../contexts/ModulesContext'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { api } from '../../lib/api'
|
||||
|
||||
type DocType = 'rf_passport' | 'foreign_passport' | 'other'
|
||||
|
||||
@@ -80,6 +83,33 @@ function availableRooms(rooms: Room[], bookings: Booking[], checkIn: string, che
|
||||
}
|
||||
|
||||
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing }: BookingModalProps) {
|
||||
const { statuses } = useModules()
|
||||
const { user } = useAuth()
|
||||
const tvEnabled = statuses['tv-welcome'] === 'active'
|
||||
|
||||
// TV message state
|
||||
const [tvMsg, setTvMsg] = useState('')
|
||||
const [tvSending, setTvSending] = useState(false)
|
||||
const [tvSent, setTvSent] = useState(false)
|
||||
const [tvError, setTvError] = useState('')
|
||||
|
||||
const handleSendTv = async () => {
|
||||
if (!tvMsg.trim() || !existing?.roomId || !user?.hotelSlug) return
|
||||
setTvSending(true)
|
||||
setTvError('')
|
||||
setTvSent(false)
|
||||
try {
|
||||
await api.netup.sendMessage(user.hotelSlug, existing.roomId, tvMsg, existing.guestName)
|
||||
setTvSent(true)
|
||||
setTvMsg('')
|
||||
setTimeout(() => setTvSent(false), 3000)
|
||||
} catch (err) {
|
||||
setTvError(err instanceof Error ? err.message : 'Ошибка отправки')
|
||||
} finally {
|
||||
setTvSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
const [form, setForm] = useState({
|
||||
roomId: existing?.roomId ?? draft.roomId,
|
||||
guestName: existing?.guestName ?? '',
|
||||
@@ -470,6 +500,46 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TV Message — только для существующих броней с активным модулем */}
|
||||
{tvEnabled && existing && existing.roomId && (
|
||||
<div className="rounded-xl border border-violet-200 dark:border-violet-800/60 bg-violet-50 dark:bg-violet-900/20 p-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tv2 size={14} className="text-violet-600 dark:text-violet-400 shrink-0" />
|
||||
<span className="text-sm font-medium text-violet-700 dark:text-violet-300">
|
||||
Сообщение гостю на TV
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={tvMsg}
|
||||
onChange={e => setTvMsg(e.target.value)}
|
||||
placeholder="Введите сообщение для гостя..."
|
||||
className="w-full rounded-lg border border-violet-200 dark:border-violet-700 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-slate-900 dark:text-slate-100 placeholder-slate-400 resize-none focus:outline-none focus:ring-2 focus:ring-violet-400"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendTv}
|
||||
disabled={tvSending || !tvMsg.trim()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-violet-600 hover:bg-violet-700 disabled:opacity-50 text-white text-xs font-medium transition-colors"
|
||||
>
|
||||
{tvSending
|
||||
? <Loader2 size={12} className="animate-spin" />
|
||||
: <Send size={12} />}
|
||||
Отправить на TV
|
||||
</button>
|
||||
{tvSent && (
|
||||
<span className="flex items-center gap-1 text-xs text-emerald-600 dark:text-emerald-400">
|
||||
<CheckCircle2 size={12} /> Отправлено
|
||||
</span>
|
||||
)}
|
||||
{tvError && (
|
||||
<span className="text-xs text-red-500">{tvError}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extra beds */}
|
||||
<div className="flex items-center justify-between px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-700/40 border border-slate-200 dark:border-slate-600">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user