import { useState, useEffect, useCallback } from 'react' import { KeyRound, Save, RefreshCw, Check, X, AlertCircle, ChevronDown, ChevronRight, Trash2, Plus, Eye, EyeOff, Lock, Unlock, Info, HardDriveDownload, Zap, Terminal, } from 'lucide-react' import { api, type TTLockConfig, type RoomLockMapping, type Workstation } from '../lib/api' import { useAuth } from '../contexts/AuthContext' import { cn } from '../lib/utils' import type { Room } from '../types' // ── Helpers ─────────────────────────────────────────────────────────────────── function Section({ title, children, defaultOpen = true }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) { const [open, setOpen] = useState(defaultOpen) return (
{open &&
{children}
}
) } function PasswordInput({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder?: string }) { const [show, setShow] = useState(false) return (
onChange(e.target.value)} placeholder={placeholder} className="input pr-10 w-full" />
) } // ── Hint box ────────────────────────────────────────────────────────────────── function Hint({ children }: { children: React.ReactNode }) { return (
{children}
) } // ── Sector Picker ───────────────────────────────────────────────────────────── function SectorPicker({ value, onChange }: { value: string onChange: (v: string) => void }) { // Parse comma-separated string into a Set of numbers const selected = new Set( value.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n) && n >= 1 && n <= 16) ) const toggle = (n: number) => { const next = new Set(selected) if (next.has(n)) { next.delete(n) } else { next.add(n) } // Sort numerically and join const sorted = Array.from(next).sort((a, b) => a - b) onChange(sorted.join(',')) } return (
{Array.from({ length: 16 }, (_, i) => i + 1).map(n => ( ))}

Выбрано: {value || '(нет)'} {' — '}секторы Mifare-карты, которые использует TTHotel. Обычно 1–10.

) } // ── DLL error note ──────────────────────────────────────────────────────────── function isDllEntryError(msg: string): boolean { return msg.includes('точку входа') || msg.toLowerCase().includes('entrypoint') || msg.toLowerCase().includes('entry point') } // ── Main Page ───────────────────────────────────────────────────────────────── export function TTLockPage() { const { user } = useAuth() const slug = user?.hotelSlug ?? '' const [config, setConfig] = useState(null) const [mappings, setMappings] = useState([]) const [rooms, setRooms] = useState([]) const [workstations, setWorkstations] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) // Форма настроек const [isEnabled, setIsEnabled] = useState(false) const [clientId, setClientId] = useState('') const [clientSecret, setClientSecret] = useState('') const [cardSectors, setCardSectors] = useState('1,2,3,4,5,6,7,8,9,10') // Per-workstation COM port state const [wsComPorts, setWsComPorts] = useState>({}) const [wsSaving, setWsSaving] = useState>({}) const [wsTesting, setWsTesting] = useState>({}) // Per-workstation port list state const [wsPorts, setWsPorts] = useState>({}) const [wsPortsLoading, setWsPortsLoading] = useState>({}) // Форма привязки замков const [addRoomId, setAddRoomId] = useState('') const [addLockMac, setAddLockMac] = useState('') const [addLockName, setAddLockName] = useState('') const [addLoading, setAddLoading] = useState(false) const showSuccess = (msg: string) => { setSuccess(msg); setTimeout(() => setSuccess(null), 3500) } const load = useCallback(async () => { if (!slug) return setLoading(true) try { const [cfg, maps, roomList, wsList] = await Promise.all([ api.ttlock.getConfig(slug), api.ttlock.getRoomLocks(slug), api.rooms.list(slug), api.workstations.list(slug), ]) setConfig(cfg) setIsEnabled(cfg.isEnabled) setClientId(cfg.clientId) setCardSectors(cfg.cardSectors || '1,2,3,4,5,6,7,8,9,10') setMappings(maps) setRooms(roomList) setWorkstations(wsList) setWsComPorts(Object.fromEntries(wsList.map(w => [w.id, w.ttlockComPort ?? '']))) } catch { setError('Не удалось загрузить настройки') } finally { setLoading(false) } }, [slug]) useEffect(() => { load() }, [load]) const handleSave = async () => { setSaving(true) setError(null) try { await api.ttlock.updateConfig(slug, { isEnabled, clientId: clientId.trim(), clientSecret: clientSecret.trim() || undefined, cardSectors: cardSectors.trim(), }) showSuccess('Настройки сохранены') setClientSecret('') await load() } catch { setError('Ошибка сохранения') } finally { setSaving(false) } } const handleAddMapping = async () => { if (!addRoomId || !addLockMac.trim()) return setAddLoading(true) setError(null) try { await api.ttlock.mapRoom(slug, { roomId: addRoomId, lockMac: addLockMac.trim(), lockName: addLockName.trim() || undefined, }) setAddRoomId('') setAddLockMac('') setAddLockName('') await load() showSuccess('Замок привязан к номеру') } catch (e) { setError(e instanceof Error ? e.message : 'Ошибка привязки замка') } finally { setAddLoading(false) } } const handleUnmap = async (roomId: string) => { try { await api.ttlock.unmapRoom(slug, roomId) await load() showSuccess('Привязка удалена') } catch { setError('Ошибка удаления привязки') } } const handleLoadPorts = async (wsId: string) => { setWsPortsLoading(prev => ({ ...prev, [wsId]: true })) try { const result = await api.workstations.listPorts(slug, wsId) setWsPorts(prev => ({ ...prev, [wsId]: result.ports })) } catch { // If agent offline or error, leave wsPorts[wsId] undefined — fallback to text input } finally { setWsPortsLoading(prev => ({ ...prev, [wsId]: false })) } } if (loading) { return (
) } const unmappedRooms = rooms.filter(r => !mappings.some(m => m.roomId === r.id)) return (
{/* Header */}

Электронные замки TTLock

Автоматическая выдача карт-ключей через программу TTHotel

{/* ── Шаг 1: Подключение к TTHotel ── */}
Откройте программу TTHotel на Windows-компьютере →{' '} Настройки → Интеграции. Скопируйте поля Client ID и Client Secret. {/* Включить/выключить */}

Активировать модуль

Включает кнопку «Выдать ключ» в карточке бронирования

setClientId(e.target.value)} placeholder="Из TTHotel → Настройки → Интеграции" className="input w-full font-mono text-sm" />
{/* Секторы карты — визуальный пикер */}
{/* ── Шаг 2: Рабочие места и энкодеры ── */}
Для каждого рабочего места укажите COM-порт USB-энкодера карт (найти в Диспетчере устройств → Порты). Нажмите рядом с полем, чтобы загрузить список портов с агента. Агент должен быть онлайн. {/* Alerts */} {error && (
{error} {isDllEntryError(error) && (
Имена функций в DLL не совпадают.{' '} Откройте файл{' '} C:\Program Files (x86)\TTHotel\resources\libs\index.js{' '} и сообщите содержимое разработчику.
)}
)} {success && (
{success}
)} {workstations.length === 0 && (

Нет рабочих мест. Добавьте их на странице «Оборудование».

)}
{workstations.map(ws => { const hasPort = !!wsComPorts[ws.id]?.trim() const portList = wsPorts[ws.id] const portsLoading = wsPortsLoading[ws.id] ?? false return (
{/* Online dot */}
{/* Name */}

{ws.name}

{ws.hostname &&

{ws.hostname}

} {!hasPort && (

Энкодер не настроен

)}
{/* COM port — dropdown if ports loaded, text input otherwise */}
{portList && portList.length > 0 ? ( ) : portList && portList.length === 0 ? ( Порты не найдены ) : ( setWsComPorts(prev => ({ ...prev, [ws.id]: e.target.value }))} /> )} {/* Load ports button */}
{/* Save COM port */} {/* Test encoder */} {/* Diagnose DLL */}
) })}
{/* ── Шаг 3: Привязка номеров к замкам ── */}
MAC-адрес замка: войдите на lock.ttlock.com →{' '} нажмите Manage у нужного замка → иконка шестерёнки →{' '} Basic → скопируйте поле MAC. Введите без двоеточий: 42A6BBF5ECE5 {/* Форма добавления */} {unmappedRooms.length > 0 && (

Добавить привязку

setAddLockMac(e.target.value.replace(/[^0-9A-Fa-f:]/g, '').toUpperCase())} placeholder="42A6BBF5ECE5" maxLength={17} className="input w-full font-mono text-sm" />
setAddLockName(e.target.value)} placeholder="Замок номера 101" className="input w-full" />
)} {/* Список привязок */} {mappings.length > 0 ? (
{mappings.map(m => (

Номер {m.roomNumber}{m.roomName ? ` — ${m.roomName}` : ''}

{m.lockMac} {m.lockName && {m.lockName}}

))}
) : (

Нет привязанных замков

Добавьте MAC-адреса замков для каждого номера

)} {rooms.length > 0 && unmappedRooms.length === 0 && mappings.length > 0 && (

Все номера привязаны к замкам

)}
) } export default TTLockPage