From d0dbf33701dcbb078b4cc3d083cf9e2d212752e5 Mon Sep 17 00:00:00 2001
From: HotelSync
Date: Tue, 14 Apr 2026 12:03:35 +0300
Subject: [PATCH] =?UTF-8?q?fix:=20chat=20=E2=80=94=20VARCHAR(10)=20overflo?=
=?UTF-8?q?w=20for=20notifications=20type,=20skeleton=20loading?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Migration 072: widen type column to VARCHAR(20), add partial unique index for general rooms
- Backend: replace brittle ON CONFLICT with explicit SELECT-then-INSERT ensureRoom helper
- Frontend: show room skeletons (Общий чат + Уведомления) during load instead of spinner,
hide '0 чатов' subtitle when list is empty
Co-Authored-By: Claude Sonnet 4.6
---
.../migrations/072_chat_room_type_length.sql | 6 +++
backend/src/routes/chat.ts | 44 +++++++------------
src/components/chat/ChatWidget.tsx | 26 ++++++++---
3 files changed, 44 insertions(+), 32 deletions(-)
create mode 100644 backend/migrations/072_chat_room_type_length.sql
diff --git a/backend/migrations/072_chat_room_type_length.sql b/backend/migrations/072_chat_room_type_length.sql
new file mode 100644
index 0000000..18278e7
--- /dev/null
+++ b/backend/migrations/072_chat_room_type_length.sql
@@ -0,0 +1,6 @@
+-- Increase type column length to fit 'notifications'
+ALTER TABLE chat_rooms ALTER COLUMN type TYPE VARCHAR(20);
+
+-- Partial unique index for general room per hotel (needed for ON CONFLICT)
+CREATE UNIQUE INDEX IF NOT EXISTS uniq_chat_rooms_general_hotel
+ ON chat_rooms(hotel_id) WHERE type = 'general';
diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts
index 5652bcc..5b87e48 100644
--- a/backend/src/routes/chat.ts
+++ b/backend/src/routes/chat.ts
@@ -13,39 +13,29 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
const canAccess = (userSlug: string | null, role: string, slug: string) =>
role === 'super_admin' || userSlug === slug
- // Ensure general room exists for hotel
- const ensureGeneralRoom = async (hotelId: string) => {
+ // Ensure a room of given type exists for hotel (idempotent)
+ const ensureRoom = async (hotelId: string, type: string, name: string) => {
+ const { rows: existing } = await db.query(
+ `SELECT id FROM chat_rooms WHERE hotel_id = $1 AND type = $2 LIMIT 1`,
+ [hotelId, type],
+ )
+ if (existing[0]) return existing[0].id as string
const { rows } = await db.query(
- `INSERT INTO chat_rooms (hotel_id, type, name)
- VALUES ($1, 'general', 'Общий чат')
- ON CONFLICT (hotel_id) WHERE type = 'general' DO NOTHING
- RETURNING id`,
- [hotelId],
+ `INSERT INTO chat_rooms (hotel_id, type, name) VALUES ($1, $2, $3)
+ ON CONFLICT DO NOTHING RETURNING id`,
+ [hotelId, type, name],
)
if (rows[0]) return rows[0].id as string
- const { rows: existing } = await db.query(
- `SELECT id FROM chat_rooms WHERE hotel_id = $1 AND type = 'general'`,
- [hotelId],
+ // Concurrent insert — fetch again
+ const { rows: r2 } = await db.query(
+ `SELECT id FROM chat_rooms WHERE hotel_id = $1 AND type = $2 LIMIT 1`,
+ [hotelId, type],
)
- return existing[0]?.id as string
+ return r2[0]?.id as string
}
- // Ensure notifications room exists for hotel
- const ensureNotificationsRoom = async (hotelId: string) => {
- const { rows } = await db.query(
- `INSERT INTO chat_rooms (hotel_id, type, name)
- VALUES ($1, 'notifications', 'Уведомления')
- ON CONFLICT DO NOTHING
- RETURNING id`,
- [hotelId],
- )
- if (rows[0]) return rows[0].id as string
- const { rows: existing } = await db.query(
- `SELECT id FROM chat_rooms WHERE hotel_id = $1 AND type = 'notifications'`,
- [hotelId],
- )
- return existing[0]?.id as string
- }
+ const ensureGeneralRoom = (hotelId: string) => ensureRoom(hotelId, 'general', 'Общий чат')
+ const ensureNotificationsRoom = (hotelId: string) => ensureRoom(hotelId, 'notifications', 'Уведомления')
// GET /api/hotels/:slug/chat/rooms — list rooms (general + notifications + directs for current user)
fastify.get(
diff --git a/src/components/chat/ChatWidget.tsx b/src/components/chat/ChatWidget.tsx
index e0703ee..c61ca0f 100644
--- a/src/components/chat/ChatWidget.tsx
+++ b/src/components/chat/ChatWidget.tsx
@@ -322,7 +322,7 @@ export function ChatWidget() {
{view === 'search' && 'Поиск'}
{view === 'settings' && 'Настройки чата'}
- {view === 'rooms' && (
+ {view === 'rooms' && visibleRooms.length > 0 && (
{visibleRooms.length} чатов
)}
{view === 'messages' && activeRoom?.type === 'notifications' && (
@@ -348,10 +348,12 @@ export function ChatWidget() {
{/* ── Rooms list ── */}
{view === 'rooms' && (
- {loadingRooms ? (
-
-
-
+ {loadingRooms && visibleRooms.length === 0 ? (
+ // Skeleton placeholders while loading
+ <>
+
} bg="bg-brand-100 dark:bg-brand-900/30" label="Общий чат" />
+ {settings.notifVisible &&
} bg="bg-amber-100 dark:bg-amber-900/30" label="Уведомления" />}
+ >
) : visibleRooms.length === 0 ? (
Нет чатов. Нажмите
чтобы начать.
@@ -681,3 +683,17 @@ function ToggleRow({ icon, label, checked, onChange }: {
)
}
+
+function RoomSkeleton({ icon, bg, label }: { icon: React.ReactNode; bg: string; label: string }) {
+ return (
+
+ )
+}