fix: loyalty tier crash, chat duplicate rooms, sidebar defaults
- LoyaltyPage: levelBadge() now falls back to bronze for unknown tiers (DB had loyalty_tier='standard' from old seed data) - migration 040: deduplicate general chat rooms + add unique index, normalize loyalty_tier to bronze for non-standard values - chat.ts: ON CONFLICT clause now targets the new partial unique index - Sidebar: DEFAULT_GROUPS only opens 'basics' by default; bump localStorage key to sidebarGroups_v2 to clear stale stored state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
20
backend/migrations/040_fixes.sql
Normal file
20
backend/migrations/040_fixes.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- Fix 1: unique general chat room per hotel (prevent duplicates on every open)
|
||||||
|
-- First deduplicate: keep the oldest general room per hotel, delete the rest
|
||||||
|
DELETE FROM chat_rooms
|
||||||
|
WHERE type = 'general'
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT DISTINCT ON (hotel_id) id
|
||||||
|
FROM chat_rooms
|
||||||
|
WHERE type = 'general'
|
||||||
|
ORDER BY hotel_id, created_at ASC
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Now add the unique constraint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_chat_rooms_general_per_hotel
|
||||||
|
ON chat_rooms (hotel_id)
|
||||||
|
WHERE type = 'general';
|
||||||
|
|
||||||
|
-- Fix 2: normalize loyalty_tier values that don't match our 4 tiers
|
||||||
|
UPDATE guests
|
||||||
|
SET loyalty_tier = 'bronze'
|
||||||
|
WHERE loyalty_tier NOT IN ('bronze', 'silver', 'gold', 'platinum');
|
||||||
@@ -18,7 +18,7 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
|
|||||||
const { rows } = await db.query(
|
const { rows } = await db.query(
|
||||||
`INSERT INTO chat_rooms (hotel_id, type, name)
|
`INSERT INTO chat_rooms (hotel_id, type, name)
|
||||||
VALUES ($1, 'general', 'Общий чат')
|
VALUES ($1, 'general', 'Общий чат')
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT (hotel_id) WHERE type = 'general' DO NOTHING
|
||||||
RETURNING id`,
|
RETURNING id`,
|
||||||
[hotelId],
|
[hotelId],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,16 +33,16 @@ type GroupKey = 'basics' | 'prices' | 'service' | 'management' | 'settingsGroup'
|
|||||||
|
|
||||||
const DEFAULT_GROUPS: Record<GroupKey, boolean> = {
|
const DEFAULT_GROUPS: Record<GroupKey, boolean> = {
|
||||||
basics: true,
|
basics: true,
|
||||||
prices: true,
|
prices: false,
|
||||||
service: true,
|
service: false,
|
||||||
management: true,
|
management: false,
|
||||||
settingsGroup: true,
|
settingsGroup: false,
|
||||||
devGroup: true,
|
devGroup: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGroups(): Record<GroupKey, boolean> {
|
function loadGroups(): Record<GroupKey, boolean> {
|
||||||
try {
|
try {
|
||||||
const stored = JSON.parse(localStorage.getItem('sidebarGroups') || '{}')
|
const stored = JSON.parse(localStorage.getItem('sidebarGroups_v2') || '{}')
|
||||||
return { ...DEFAULT_GROUPS, ...stored }
|
return { ...DEFAULT_GROUPS, ...stored }
|
||||||
} catch {
|
} catch {
|
||||||
return { ...DEFAULT_GROUPS }
|
return { ...DEFAULT_GROUPS }
|
||||||
@@ -50,7 +50,7 @@ function loadGroups(): Record<GroupKey, boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveGroups(groups: Record<GroupKey, boolean>) {
|
function saveGroups(groups: Record<GroupKey, boolean>) {
|
||||||
localStorage.setItem('sidebarGroups', JSON.stringify(groups))
|
localStorage.setItem('sidebarGroups_v2', JSON.stringify(groups))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── NavItem with star ──────────────────────────────────────────────────────
|
// ── NavItem with star ──────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -214,8 +214,9 @@ const MANUAL_REASONS = [
|
|||||||
'Другое',
|
'Другое',
|
||||||
]
|
]
|
||||||
|
|
||||||
function levelBadge(levelId: LevelId) {
|
function levelBadge(levelId: string) {
|
||||||
const level = DEFAULT_LEVELS.find(l => l.id === levelId)!
|
const level = DEFAULT_LEVELS.find(l => l.id === levelId) ?? DEFAULT_LEVELS[0]
|
||||||
|
const safeId = (DEFAULT_LEVELS.find(l => l.id === levelId)?.id ?? 'bronze') as LevelId
|
||||||
const bgs: Record<LevelId, string> = {
|
const bgs: Record<LevelId, string> = {
|
||||||
bronze: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
bronze: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
||||||
silver: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300',
|
silver: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300',
|
||||||
@@ -223,7 +224,7 @@ function levelBadge(levelId: LevelId) {
|
|||||||
platinum: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400',
|
platinum: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400',
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className={cn('inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold', bgs[levelId])}>
|
<span className={cn('inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold', bgs[safeId])}>
|
||||||
<level.icon size={10} />
|
<level.icon size={10} />
|
||||||
{level.name}
|
{level.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user