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:
@@ -617,7 +617,7 @@ export const api = {
|
||||
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test`, { workstationId }),
|
||||
|
||||
testApi: (slug: string, workstationId: string) =>
|
||||
req<{ ok: boolean; lockCount?: number }>('POST', `/api/hotels/${slug}/ttlock/test-api`, { workstation_id: workstationId }),
|
||||
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test-api`, { workstation_id: workstationId }),
|
||||
|
||||
findApiConfig: (slug: string, workstationId: string) =>
|
||||
req<{ ok: boolean; appDir?: string; results?: { path: string; matches: string[] }[] }>('POST', `/api/hotels/${slug}/ttlock/find-api-config`, { workstation_id: workstationId }),
|
||||
@@ -628,7 +628,7 @@ export const api = {
|
||||
getRoomLocks: (slug: string) =>
|
||||
req<RoomLockMapping[]>('GET', `/api/hotels/${slug}/ttlock/room-locks`),
|
||||
|
||||
mapRoom: (slug: string, data: { roomId?: string; lockMac: string; lockName?: string }) =>
|
||||
mapRoom: (slug: string, data: { roomId?: string; lockMac: string; lockName?: string; buildNo?: number; floorNo?: number }) =>
|
||||
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/room-locks`, data),
|
||||
|
||||
unmapRoom: (slug: string, roomId: string) =>
|
||||
@@ -1094,21 +1094,18 @@ export interface Workstation {
|
||||
}
|
||||
|
||||
export interface TTLockConfig {
|
||||
isEnabled: boolean
|
||||
clientId: string
|
||||
cardSectors: string
|
||||
apiServer: string
|
||||
ttlockUsername: string
|
||||
isEnabled: boolean
|
||||
clientId: string
|
||||
cardSectors: string
|
||||
apiServer: string
|
||||
}
|
||||
|
||||
export interface TTLockConfigUpdate {
|
||||
isEnabled?: boolean
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
ttlock_username?: string
|
||||
ttlock_password?: string
|
||||
cardSectors?: string
|
||||
apiServer?: string
|
||||
isEnabled?: boolean
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
cardSectors?: string
|
||||
apiServer?: string
|
||||
}
|
||||
|
||||
export interface RoomLockMapping {
|
||||
@@ -1117,6 +1114,8 @@ export interface RoomLockMapping {
|
||||
roomNumber: string
|
||||
lockMac: string
|
||||
lockName: string | null
|
||||
buildNo: number
|
||||
floorNo: number
|
||||
}
|
||||
|
||||
export interface CardIssuanceResult {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user