From 566c6a5e41fff858e4a34eac00e49575c7ac72c7 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Thu, 26 Mar 2026 00:56:53 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Russian=20market=20channels=20=E2=80=94?= =?UTF-8?q?=20=D0=AF=D0=BD=D0=B4=D0=B5=D0=BA=D1=81,=20=D0=9E=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BE=D0=BA,=20=D0=A1=D1=83=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=BE,=20=D0=90=D0=B2=D0=B8=D1=82=D0=BE,=20Bronevi?= =?UTF-8?q?k,=20101hotels=20+=20role-based=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/migrations/036_channels_ru.sql | 31 ++++++++++++++++++ backend/src/seed.ts | 2 +- src/pages/ChannelsPage.tsx | 45 ++++++++++++++++---------- 3 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 backend/migrations/036_channels_ru.sql diff --git a/backend/migrations/036_channels_ru.sql b/backend/migrations/036_channels_ru.sql new file mode 100644 index 0000000..6cfa18b --- /dev/null +++ b/backend/migrations/036_channels_ru.sql @@ -0,0 +1,31 @@ +-- Replace old channel constraint with Russian market channels +ALTER TABLE channels DROP CONSTRAINT IF EXISTS channels_name_check; +ALTER TABLE channels ADD CONSTRAINT channels_name_check + CHECK (name IN ( + 'booking_com', + 'yandex_travel', + 'ostrovok', + 'sutochno', + 'avito', + 'bronevik', + 'hotels101', + 'onetwotrip' + )); + +-- Remove old non-RU channels from all hotels +DELETE FROM channels WHERE name IN ('airbnb', 'expedia', 'vrbo'); + +-- Add new Russian channels for all existing hotels (skip if already exists) +INSERT INTO channels (hotel_id, name) +SELECT h.id, c.name +FROM hotels h +CROSS JOIN (VALUES + ('yandex_travel'), + ('ostrovok'), + ('sutochno'), + ('avito'), + ('bronevik'), + ('hotels101'), + ('onetwotrip') +) AS c(name) +ON CONFLICT (hotel_id, name) DO NOTHING; diff --git a/backend/src/seed.ts b/backend/src/seed.ts index b27264d..e805d23 100644 --- a/backend/src/seed.ts +++ b/backend/src/seed.ts @@ -47,7 +47,7 @@ export async function seedIfEmpty() { } // Insert channels (disabled by default) - for (const name of ['booking_com', 'airbnb', 'expedia', 'vrbo']) { + for (const name of ['booking_com', 'yandex_travel', 'ostrovok', 'sutochno', 'avito', 'bronevik', 'hotels101', 'onetwotrip']) { await db.query( `INSERT INTO channels (hotel_id, name) VALUES ($1, $2)`, [hotelId, name], diff --git a/src/pages/ChannelsPage.tsx b/src/pages/ChannelsPage.tsx index 090a18a..45545e1 100644 --- a/src/pages/ChannelsPage.tsx +++ b/src/pages/ChannelsPage.tsx @@ -6,19 +6,17 @@ import type { Channel, SyncStatus } from '../types' import { cn } from '../lib/utils' import { Badge } from '../components/ui/Badge' -const CHANNEL_ICONS: Record = { - booking_com: '🔵', - airbnb: '🔴', - expedia: '🟡', - vrbo: '🟢', - yandex_travel: '🔴', - ostrovok: '🟣', - sutochno: '🟠', - onetwotrip: '🔵', +const CHANNEL_META: Record = { + booking_com: { icon: '🔵', label: 'Booking.com', description: 'Крупнейшая международная OTA', integration: 'XML API' }, + yandex_travel: { icon: '🔴', label: 'Яндекс Путешествия', description: 'Ведущая платформа бронирования в РФ', integration: 'TravelLine' }, + ostrovok: { icon: '🟣', label: 'Островок', description: 'Один из крупнейших российских OTA', integration: 'REST API' }, + sutochno: { icon: '🟠', label: 'Суточно.ру', description: 'Посуточная аренда и гостиницы', integration: 'iCal / TravelLine' }, + avito: { icon: '🟢', label: 'Авито', description: 'Крупнейшая площадка объявлений в РФ', integration: 'XML фид / REST' }, + bronevik: { icon: '🔷', label: 'Bronevik.com', description: 'Корпоративное бронирование командировок', integration: 'SOAP/XML API' }, + hotels101: { icon: '🟡', label: '101hotels.ru', description: 'Российский агрегатор отелей', integration: 'OTA XML' }, + onetwotrip: { icon: '🩵', label: 'OneTwoTrip', description: 'Онлайн-сервис для путешествий', integration: 'XML API' }, } -const RUSSIAN_CHANNELS = new Set(['yandex_travel', 'ostrovok', 'sutochno', 'onetwotrip']) - function SyncStatusBadge({ status }: { status: SyncStatus }) { const map: Record = { idle: { icon: Clock, label: 'Не настроен', cls: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300' }, @@ -75,8 +73,8 @@ export function ChannelsPage() { const totalImported = channels.reduce((s, c) => s + c.bookingsImported, 0) const activeCount = channels.filter(c => c.isEnabled).length - const internationalChannels = channels.filter(c => !RUSSIAN_CHANNELS.has(c.name)) - const russianChannels = channels.filter(c => RUSSIAN_CHANNELS.has(c.name)) + const internationalChannels = channels.filter(c => c.name === 'booking_com') + const russianChannels = channels.filter(c => c.name !== 'booking_com') if (loading) return (
Загрузка...
@@ -148,8 +146,10 @@ export function ChannelsPage() {

Демо-режим

- В демо-режиме синхронизация имитируется. В продакшн-версии используется webhook-интеграция - с реальными API каналов через менеджер каналов (Staah, SiteMinder, HotelRunner и др.) + В демо-режиме синхронизация имитируется. В продакшн-версии используется прямая интеграция + с API каждой площадки или через TravelLine как единую точку подключения. + Для Яндекс Путешествий, Островка и Суточно.ру доступно подключение через TravelLine. + Авито — через XML-фид и REST API. Bronevik — через SOAP XML API.

@@ -170,10 +170,15 @@ function ChannelCard({ channel, onSync, onToggle }: {
- {CHANNEL_ICONS[channel.name]} + {CHANNEL_META[channel.name]?.icon ?? '🌐'}
-

{channel.displayName}

+

+ {CHANNEL_META[channel.name]?.label ?? channel.displayName} +

+

+ {CHANNEL_META[channel.name]?.description} +

@@ -231,6 +236,12 @@ function ChannelCard({ channel, onSync, onToggle }: {
)} + {CHANNEL_META[channel.name]?.integration && ( +

+ Интеграция: {CHANNEL_META[channel.name].integration} +

+ )} +