feat: TTLock — correct DLL flow with buildNo/floorNo per lock

Backend: room-locks now store/return build_no, floor_no (required by
CE_WriteCard). issue-card passes buildNo/floorNo to agent instead of
cardSectors/username/password. testApi route simplified (no OAuth).

Frontend: remove username/password fields (not needed), add buildNo/floorNo
fields to lock mapping form, show building/floor in lock list.

Migration 056: adds build_no, floor_no columns to room_lock_mappings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:59:38 +03:00
parent 8c215f2dcd
commit 1ad2a8057a
4 changed files with 88 additions and 85 deletions

View File

@@ -139,13 +139,11 @@ export function TTLockPage() {
const [success, setSuccess] = useState<string | null>(null)
// Форма настроек
const [isEnabled, setIsEnabled] = useState(false)
const [clientId, setClientId] = useState('')
const [clientSecret, setClientSecret] = useState('')
const [ttlockUsername, setTtlockUsername] = useState('')
const [ttlockPassword, setTtlockPassword] = useState('')
const [cardSectors, setCardSectors] = useState('1,2,3,4,5,6,7,8,9,10')
const [apiServer, setApiServer] = useState('https://euapi.ttlock.com')
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')
const [apiServer, setApiServer] = useState('https://euapi.ttlock.com')
// Per-workstation COM port state
const [wsComPorts, setWsComPorts] = useState<Record<string, string>>({})
@@ -157,10 +155,12 @@ export function TTLockPage() {
const [wsPortsLoading, setWsPortsLoading] = useState<Record<string, boolean>>({})
// Форма привязки замков
const [addRoomId, setAddRoomId] = useState('')
const [addLockMac, setAddLockMac] = useState('')
const [addRoomId, setAddRoomId] = useState('')
const [addLockMac, setAddLockMac] = useState('')
const [addLockName, setAddLockName] = useState('')
const [addLoading, setAddLoading] = useState(false)
const [addBuildNo, setAddBuildNo] = useState('1')
const [addFloorNo, setAddFloorNo] = useState('1')
const [addLoading, setAddLoading] = useState(false)
// Импорт из XLS
interface ImportRow {
@@ -244,7 +244,6 @@ export function TTLockPage() {
setConfig(cfg)
setIsEnabled(cfg.isEnabled)
setClientId(cfg.clientId)
setTtlockUsername(cfg.ttlockUsername || '')
setCardSectors(cfg.cardSectors || '1,2,3,4,5,6,7,8,9,10')
setApiServer(cfg.apiServer || 'https://euapi.ttlock.com')
setMappings(maps)
@@ -266,16 +265,13 @@ export function TTLockPage() {
try {
await api.ttlock.updateConfig(slug, {
isEnabled,
clientId: clientId.trim(),
clientSecret: clientSecret.trim() || undefined,
ttlock_username: ttlockUsername.trim() || undefined,
ttlock_password: ttlockPassword.trim() || undefined,
cardSectors: cardSectors.trim(),
apiServer: apiServer,
clientId: clientId.trim(),
clientSecret: clientSecret.trim() || undefined,
cardSectors: cardSectors.trim(),
apiServer: apiServer,
})
showSuccess('Настройки сохранены')
setClientSecret('')
setTtlockPassword('')
await load()
} catch {
setError('Ошибка сохранения')
@@ -290,8 +286,8 @@ export function TTLockPage() {
setTestingApi(true)
setError(null)
try {
const result = await api.ttlock.testApi(slug, ws.id)
showSuccess(`TTLock Cloud API работает — найдено замков: ${result.lockCount ?? 0}`)
await api.ttlock.testApi(slug, ws.id)
showSuccess('TTLock Cloud API работает — учётные данные верны')
} catch (e) {
setError(e instanceof Error ? e.message : 'Ошибка проверки API')
} finally {
@@ -308,10 +304,14 @@ export function TTLockPage() {
roomId: addRoomId,
lockMac: addLockMac.trim(),
lockName: addLockName.trim() || undefined,
buildNo: parseInt(addBuildNo) || 1,
floorNo: parseInt(addFloorNo) || 1,
})
setAddRoomId('')
setAddLockMac('')
setAddLockName('')
setAddBuildNo('1')
setAddFloorNo('1')
await load()
showSuccess('Замок привязан к номеру')
} catch (e) {
@@ -416,27 +416,6 @@ export function TTLockPage() {
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="form-label">Учетная запись <span className="font-normal text-slate-400">(поле «Учетная запись» в TTHotel)</span></label>
<input
type="text"
value={ttlockUsername}
onChange={e => setTtlockUsername(e.target.value)}
placeholder="h_1754382851163"
className="input w-full font-mono text-sm"
/>
</div>
<div>
<label className="form-label">Пароль <span className="font-normal text-slate-400">(поле «Пароль» в TTHotel)</span></label>
<PasswordInput
value={ttlockPassword}
onChange={setTtlockPassword}
placeholder={ttlockUsername && !ttlockPassword ? '••••••• (не изменится)' : 'Пароль от учетной записи TTHotel'}
/>
</div>
</div>
{/* Сервер TTLock API */}
<div>
<label className="form-label">Сервер TTLock API</label>
@@ -468,7 +447,7 @@ export function TTLockPage() {
))}
</div>
<p className="text-xs text-slate-400 mt-1.5">
OAuth авторизация всегда через api.sciener.com. Выберите регион для /v3/ API.
Выберите регион API. EU для большинства стран, China для Китая.
</p>
</div>
@@ -863,6 +842,28 @@ export function TTLockPage() {
/>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="form-label">Здание <span className="font-normal text-slate-400">(buildNo из TTHotel)</span></label>
<input
type="number"
min={1}
value={addBuildNo}
onChange={e => setAddBuildNo(e.target.value)}
className="input w-full"
/>
</div>
<div>
<label className="form-label">Этаж <span className="font-normal text-slate-400">(floorNo из TTHotel)</span></label>
<input
type="number"
min={1}
value={addFloorNo}
onChange={e => setAddFloorNo(e.target.value)}
className="input w-full"
/>
</div>
</div>
<button
onClick={handleAddMapping}
disabled={!addRoomId || !addLockMac.trim() || addLoading}
@@ -906,6 +907,7 @@ export function TTLockPage() {
<p className="text-xs text-slate-500 font-mono">
{m.lockMac}
{m.roomId && m.lockName && <span className="text-slate-400 font-sans ml-2">{m.lockName}</span>}
<span className="text-slate-400 font-sans ml-2">здание {m.buildNo ?? 1}, этаж {m.floorNo ?? 1}</span>
</p>
</div>
</div>