feat: TTLockPage — import locks from TTHotel XLS file
- Parse .xls/.xlsx via SheetJS (xlsx package) - Auto-match Door Name → room number; unmatched rows kept with empty room - Preview table: green=matched, amber=unmatched, grey=already mapped - Per-row room dropdown for manual correction before import - room_id nullable in room_lock_mappings (migration 052) - Conflict key changed from (hotel_id, room_id) to (hotel_id, lock_mac) - LEFT JOIN in GET room-locks to show unmatched locks - DELETE by UUID or MAC depending on whether room is assigned Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
18
backend/migrations/052_ttlock_nullable_room.sql
Normal file
18
backend/migrations/052_ttlock_nullable_room.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- TTLock: room_id в room_lock_mappings становится необязательным
|
||||
-- Нужно для импорта замков из TTHotel без привязки к номеру в системе
|
||||
|
||||
ALTER TABLE room_lock_mappings
|
||||
ALTER COLUMN room_id DROP NOT NULL;
|
||||
|
||||
-- Добавляем lock_name если не было
|
||||
ALTER TABLE room_lock_mappings
|
||||
ADD COLUMN IF NOT EXISTS lock_name TEXT;
|
||||
|
||||
-- Уникальность теперь по lock_mac (один MAC — одна запись)
|
||||
-- Старый unique(hotel_id, room_id) оставляем (работает с NULL через NULLS NOT DISTINCT в PG15+
|
||||
-- или просто убираем и заменяем на lock_mac)
|
||||
ALTER TABLE room_lock_mappings
|
||||
DROP CONSTRAINT IF EXISTS room_lock_mappings_hotel_id_room_id_key;
|
||||
|
||||
ALTER TABLE room_lock_mappings
|
||||
ADD CONSTRAINT room_lock_mappings_hotel_id_lock_mac_key UNIQUE (hotel_id, lock_mac);
|
||||
@@ -177,9 +177,9 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
`SELECT m.room_id, m.lock_mac, m.lock_name,
|
||||
r.name AS room_name, r.number AS room_number
|
||||
FROM room_lock_mappings m
|
||||
JOIN rooms r ON r.id = m.room_id
|
||||
LEFT JOIN rooms r ON r.id = m.room_id
|
||||
WHERE m.hotel_id = $1
|
||||
ORDER BY r.number`,
|
||||
ORDER BY r.number NULLS LAST, m.lock_name`,
|
||||
[hotelId],
|
||||
)
|
||||
return rows.map(r => ({
|
||||
@@ -192,7 +192,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
})
|
||||
|
||||
// ── POST /api/hotels/:slug/ttlock/room-locks ─────────────────────────────────
|
||||
fastify.post<SlugParam & { Body: { room_id: string; lock_mac: string; lock_name?: string } }>(
|
||||
fastify.post<SlugParam & { Body: { room_id?: string; lock_mac: string; lock_name?: string } }>(
|
||||
'/api/hotels/:slug/ttlock/room-locks',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
async (req, reply) => {
|
||||
@@ -203,7 +203,6 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||
|
||||
const { room_id: roomId, lock_mac: lockMac, lock_name: lockName } = req.body
|
||||
// Нормализуем MAC: убираем двоеточия, приводим к верхнему регистру
|
||||
const mac = (lockMac ?? '').replace(/:/g, '').toUpperCase()
|
||||
if (!/^[0-9A-F]{12}$/.test(mac)) {
|
||||
return reply.code(400).send({ error: 'MAC-адрес должен быть 12 HEX-символов (напр. 42A6BBF5ECE5)' })
|
||||
@@ -212,8 +211,8 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
await db.query(
|
||||
`INSERT INTO room_lock_mappings (hotel_id, room_id, lock_mac, lock_name)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (hotel_id, room_id) DO UPDATE SET lock_mac = $3, lock_name = $4`,
|
||||
[hotelId, roomId, mac, lockName ?? null],
|
||||
ON CONFLICT (hotel_id, lock_mac) DO UPDATE SET room_id = $2, lock_name = $4`,
|
||||
[hotelId, roomId ?? null, mac, lockName ?? null],
|
||||
)
|
||||
return { ok: true }
|
||||
},
|
||||
@@ -227,7 +226,14 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
const hotelId = await getHotelId(slug)
|
||||
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||
|
||||
await db.query('DELETE FROM room_lock_mappings WHERE hotel_id = $1 AND room_id = $2', [hotelId, roomId])
|
||||
// roomId может быть UUID (привязан к комнате) или MAC-адрес (без комнаты)
|
||||
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(roomId)
|
||||
if (isUuid) {
|
||||
await db.query('DELETE FROM room_lock_mappings WHERE hotel_id = $1 AND room_id = $2', [hotelId, roomId])
|
||||
} else {
|
||||
const mac = roomId.replace(/:/g, '').toUpperCase()
|
||||
await db.query('DELETE FROM room_lock_mappings WHERE hotel_id = $1 AND lock_mac = $2', [hotelId, mac])
|
||||
}
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user