feat: Russian market channels — Яндекс, Островок, Суточно, Авито, Bronevik, 101hotels + role-based sidebar

This commit is contained in:
2026-03-26 00:56:53 +03:00
parent 18358b64b3
commit 566c6a5e41
3 changed files with 60 additions and 18 deletions

View File

@@ -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<string, string> = {
booking_com: '🔵',
airbnb: '🔴',
expedia: '🟡',
vrbo: '🟢',
yandex_travel: '🔴',
ostrovok: '🟣',
sutochno: '🟠',
onetwotrip: '🔵',
const CHANNEL_META: Record<string, { icon: string; label: string; description: string; integration: string }> = {
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<SyncStatus, { icon: React.ElementType; label: string; cls: string }> = {
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 (
<div className="p-6 flex items-center justify-center text-slate-400 text-sm">Загрузка...</div>
@@ -148,8 +146,10 @@ export function ChannelsPage() {
<div>
<p className="font-medium text-amber-800 dark:text-amber-300">Демо-режим</p>
<p className="text-sm text-amber-700 dark:text-amber-400 mt-0.5">
В демо-режиме синхронизация имитируется. В продакшн-версии используется webhook-интеграция
с реальными API каналов через менеджер каналов (Staah, SiteMinder, HotelRunner и др.)
В демо-режиме синхронизация имитируется. В продакшн-версии используется прямая интеграция
с API каждой площадки или через TravelLine как единую точку подключения.
Для Яндекс Путешествий, Островка и Суточно.ру доступно подключение через TravelLine.
Авито через XML-фид и REST API. Bronevik через SOAP XML API.
</p>
</div>
</div>
@@ -170,10 +170,15 @@ function ChannelCard({ channel, onSync, onToggle }: {
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-2xl">
{CHANNEL_ICONS[channel.name]}
{CHANNEL_META[channel.name]?.icon ?? '🌐'}
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-slate-100">{channel.displayName}</p>
<p className="font-semibold text-slate-900 dark:text-slate-100">
{CHANNEL_META[channel.name]?.label ?? channel.displayName}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">
{CHANNEL_META[channel.name]?.description}
</p>
<SyncStatusBadge status={channel.lastSyncStatus} />
</div>
</div>
@@ -231,6 +236,12 @@ function ChannelCard({ channel, onSync, onToggle }: {
</div>
)}
{CHANNEL_META[channel.name]?.integration && (
<p className="text-xs text-slate-400 dark:text-slate-500">
Интеграция: {CHANNEL_META[channel.name].integration}
</p>
)}
<button
onClick={onSync}
disabled={isSyncing}