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 ( +
+
+ {icon} +
+
+

{label}

+
+
+
+ ) +}