import { useState, useEffect, useCallback } from 'react' import { Tv2, Plug, Map, Activity, CheckCircle2, XCircle, Loader2, Save, Eye, EyeOff, RefreshCw, Trash2 } from 'lucide-react' import { cn } from '../lib/utils' import { api } from '../lib/api' import { useAuth } from '../contexts/AuthContext' type Tab = 'connection' | 'rooms' | 'log' const LANGUAGES = [ { value: 'ru_RU', label: 'Русский' }, { value: 'en_US', label: 'English' }, { value: 'de_DE', label: 'Deutsch' }, { value: 'zh_CN', label: '中文' }, { value: 'ar_AE', label: 'العربية' }, ] type PushEvent = { ts: string action: string roomNumber: string netupRoom: string url: string requestBody?: unknown status: 'ok' | 'error' | 'skipped' httpStatus?: number responseBody?: string error?: string } export function TvWelcomePage() { const { user } = useAuth() const slug = user?.hotelSlug ?? '' const [tab, setTab] = useState('connection') const [saving, setSaving] = useState(false) const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState<{ ok: boolean; msg: string } | null>(null) const [showPass, setShowPass] = useState(false) // Connection settings const [serverUrl, setServerUrl] = useState('') const [username, setUsername] = useState('admin') const [password, setPassword] = useState('') const [language, setLanguage] = useState('ru_RU') const [enabled, setEnabled] = useState(true) const [settingsLoaded, setSettingsLoaded] = useState(false) // Room mappings const [rooms, setRooms] = useState<{ id: string; number: string; type: string; netupRoomNumber: string }[]>([]) const [roomsSaving, setRoomsSaving] = useState(false) const [roomsSaved, setRoomsSaved] = useState(false) // Diagnostics const [pushEvents, setPushEvents] = useState([]) const [logLoading, setLogLoading] = useState(false) const [selectedPush, setSelectedPush] = useState(null) const [testingCheckin, setTestingCheckin] = useState(false) const [testCheckinResult, setTestCheckinResult] = useState<{ ok: boolean; msg: string } | null>(null) // Load settings useEffect(() => { if (!slug) return api.netup.getSettings(slug) .then(s => { setServerUrl(s.serverUrl) setUsername(s.username) setLanguage(s.defaultLanguage) setEnabled(s.enabled) setSettingsLoaded(true) }) .catch(() => setSettingsLoaded(true)) }, [slug]) // Load room mappings when tab switches (also needed on 'log' for test button) useEffect(() => { if ((tab !== 'rooms' && tab !== 'log') || !slug) return if (rooms.length > 0) return api.netup.getRoomMappings(slug).then(setRooms).catch(() => {}) }, [tab, slug, rooms.length]) // Load push log when diagnostics tab opens const loadLog = useCallback(() => { if (!slug) return setLogLoading(true) api.netup.getLog(slug) .then(r => setPushEvents(r.pushEvents)) .catch(() => {}) .finally(() => setLogLoading(false)) }, [slug]) useEffect(() => { if (tab !== 'log') return loadLog() }, [tab, loadLog]) const handleSaveConnection = async () => { setSaving(true) setTestResult(null) try { await api.netup.saveSettings(slug, { server_url: serverUrl, username, password: password || undefined, default_language: language, enabled, tl_token: '', }) setPassword('') } finally { setSaving(false) } } const handleTest = async () => { setTesting(true) setTestResult(null) try { await api.netup.saveSettings(slug, { server_url: serverUrl, username, password: password || undefined, default_language: language, enabled, tl_token: '', }) const res = await api.netup.testConnection(slug) setTestResult({ ok: true, msg: res.message }) } catch (err) { setTestResult({ ok: false, msg: err instanceof Error ? err.message : 'Ошибка подключения' }) } finally { setTesting(false) } } const handleSaveRooms = async () => { setRoomsSaving(true) setRoomsSaved(false) try { await api.netup.saveRoomMappings(slug, rooms.map(r => ({ pms_room_id: r.id, netup_room_number: r.netupRoomNumber, }))) setRoomsSaved(true) setTimeout(() => setRoomsSaved(false), 2500) } finally { setRoomsSaving(false) } } const handleClearLog = async () => { await api.netup.clearLog(slug).catch(() => {}) setPushEvents([]) setSelectedPush(null) } const handleTestCheckin = async () => { if (!rooms.length) return const room = rooms.find(r => r.netupRoomNumber) ?? rooms[0] setTestingCheckin(true) setTestCheckinResult(null) try { const res = await api.netup.testCheckin(slug, room.id) setTestCheckinResult({ ok: true, msg: res.message }) } catch (err) { setTestCheckinResult({ ok: false, msg: err instanceof Error ? err.message : 'Ошибка' }) } finally { setTestingCheckin(false) setTimeout(loadLog, 500) } } const updateRoomNetup = (id: string, val: string) => setRooms(prev => prev.map(r => r.id === id ? { ...r, netupRoomNumber: val } : r)) return (
{/* Header */}

Приветствие на TV

Интеграция с NetUP IPTV Middleware

{/* Tabs */}
{([ { id: 'connection', label: 'Подключение', icon: Plug }, { id: 'rooms', label: 'Сопоставление номеров', icon: Map }, { id: 'log', label: 'Диагностика', icon: Activity }, ] as const).map(t => ( ))}
{/* ── Tab: Connection ──────────────────────────────────────────────────── */} {tab === 'connection' && (
{/* Enabled toggle */}

Интеграция включена

При заселении/выезде автоматически уведомлять NetUP


{/* Server URL */}
setServerUrl(e.target.value)} placeholder="http://192.168.1.100:8880" className="input w-full font-mono text-sm" />

Внешний адрес и порт, на который пробрасывается 172.16.0.12:80

{/* Credentials */}
setUsername(e.target.value)} className="input w-full" />
setPassword(e.target.value)} placeholder="Не изменять" className="input w-full pr-9" />

Оставьте пустым, чтобы не менять

{/* Default language */}

Язык меню на TV, если для гостя не указан язык

{/* Test result */} {testResult && (
{testResult.ok ? : } {testResult.msg}
)} {/* Actions */}
)} {/* ── Tab: Room Mapping ────────────────────────────────────────────────── */} {tab === 'rooms' && (

Сопоставление номеров

Укажите номер в NetUP, соответствующий каждому номеру в PMS

{rooms.length === 0 ? (
Нет номеров. Сначала добавьте номера в разделе «Номера».
) : ( {rooms.map((room, i) => ( ))}
Номер в PMS Тип Номер в NetUP
{room.number} {room.type} updateRoomNetup(room.id, e.target.value)} placeholder="Не задан" className="input py-1.5 w-36 text-sm" />
)}
{roomsSaved && ( Сохранено )}
)} {/* ── Tab: Diagnostics ─────────────────────────────────────────────────── */} {tab === 'log' && (

Вызовы в NetUP

Заселения и выезды, отправленные в NetUP. Нажмите на строку для деталей.

{pushEvents.length > 0 && ( )}
{/* Test check-in */}
{rooms.length === 0 &&

Сначала настройте сопоставление номеров

} {testCheckinResult && ( {testCheckinResult.ok ? : } {testCheckinResult.msg} )}
{/* Events list */} {pushEvents.length === 0 ? (

Вызовов ещё не было

Измените статус брони на «Заселён» или нажмите «Тест» выше

) : (
{pushEvents.map((e, i) => (
{selectedPush === e && (

URL запроса

{e.url}

{e.requestBody != null && (

Тело запроса

{JSON.stringify(e.requestBody, null, 2)}
)} {e.responseBody && (

Ответ NetUP

{e.responseBody}
)}
)}
))}
)}
)}
) }