feat: Russian market channels — Яндекс, Островок, Суточно, Авито, Bronevik, 101hotels + role-based sidebar
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user