import { useCallback, useEffect, useRef, useState } from 'react'; import { Monitor, Plus, RefreshCw, X, Link2, Save, Unplug, CheckCircle2, Clock3 } from 'lucide-react'; import { boardDevicesApi, stopsApi } from '../services/api'; import { useCity } from '../contexts/CityContext'; import './DevicesPage.css'; import { BoardPassengerSettings, passengerDefaults, cityFareDefaults } from '../components/BoardPassengerSettings'; const defaults = { stopId: 1, demo: false, mainSeconds: 25, infoSeconds: 8, holdNear: true, rowCount: 4, routeFilter: '', theme: 'dark', colorScheme: 'lime', lightColorScheme: 'blue', infoServiceEnabled: true, infoWeatherEnabled: true, infoQrEnabled: true, journeyTranslitEnabled: false, showNextSameRoute: true, longWaitEnabled: true, displayProfile: 'tv', viewingDistance: 'near', nightMode: 'auto', nightWakeMinutes: 60, adsEnabled: false, adSeconds: 7, adTitle: 'Ваше место в городе', adText: 'Короткая история. Большая аудитория.' }; const darkColors = [['lime', 'Лайм'], ['amber', 'Янтарь'], ['cyan', 'Голубой'], ['mint', 'Мята'], ['lavender', 'Лаванда'], ['white', 'Белый']]; const lightColors = [['blue', 'Синий'], ['forest', 'Зелёный'], ['crimson', 'Кармин'], ['violet', 'Фиолетовый'], ['teal', 'Бирюзовый'], ['graphite', 'Графит']]; const date = value => value ? new Date(value).toLocaleString('ru-RU', { timeZone: 'Europe/Minsk', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', second: '2-digit' }) : 'Ещё не выходило на связь'; const message = error => error.response?.data?.message || 'Не удалось выполнить запрос. Попробуйте ещё раз'; function DeviceForm({ device, stops, cityId, onClose, onSaved }) { const dialog = useRef(null); const [code, setCode] = useState(''), [name, setName] = useState(device?.name || ''); const [settings, setSettings] = useState({ ...defaults, ...passengerDefaults, fareByType: cityFareDefaults(cityId), stopId: stops[0]?.id || 1, ...device?.settings }); const [error, setError] = useState(''), [busy, setBusy] = useState(false); const [detached, setDetached] = useState(false), [routes, setRoutes] = useState([]); useEffect(() => { let alive = true; setRoutes([]); boardDevicesApi.routes(settings.stopId).then(({ data }) => { if (alive) setRoutes(data); }).catch(() => { if (alive) setError('Не удалось загрузить маршруты площадки'); }); return () => { alive = false; }; }, [settings.stopId]); useEffect(() => { const previous = document.activeElement; dialog.current?.showModal(); return () => { dialog.current?.close(); previous?.focus?.(); }; }, []); const set = (key, value) => setSettings(previous => ({ ...previous, [key]: value })); async function save(event) { event.preventDefault(); if (busy) return; setBusy(true); setError(''); try { const payload = { name, settings, ...(device ? { revision: device.revision } : { code }) }; if (device) await boardDevicesApi.update(device.id, payload); else await boardDevicesApi.claim(payload); onSaved(device ? 'Настройки сохранены' : 'Устройство подключено'); } catch (e) { setError(message(e)); } finally { setBusy(false); } } async function revoke() { setBusy(true); setError(''); try { await boardDevicesApi.revoke(device.id); onSaved('Устройство отвязано. Табло узнает об этом при следующем подключении'); } catch (e) { setError(message(e)); setBusy(false); } } const select = (key, label, values, numeric = false) => ; const check = (key, label) => ; return { e.preventDefault(); if (!busy) onClose(); }}>
RIDEBUS BOARD

{device ? 'Настройки устройства' : 'Подключить табло'}

{!device && } {select('routeFilter', 'Маршрут', [['', 'Все маршруты остановки'], ...routes.map(route => [String(route.id), `№${route.route_number} · ${route.name}`])])}

Оформление и читаемость

{select('theme', 'Тема', [['dark', 'Тёмная'], ['light', 'Светлая']])} {settings.theme === 'light' ? select('lightColorScheme', 'Цветовая схема', lightColors) : select('colorScheme', 'Цветовая схема', darkColors)} {select('displayProfile', 'Профиль экрана', [['tv', 'Телевизор'], ['street', 'Уличное табло']])} {select('viewingDistance', 'Расстояние просмотра', [['near', 'Близко'], ['medium', 'Средне'], ['far', 'Далеко']])} {select('rowCount', 'Следующие рейсы', [2, 3, 4, 5, 6].map(n => [n, n]), true)}

Экраны

{[['mainSeconds', 'Главный экран, сек', 5, 120], ['infoSeconds', 'Информация, сек', 5, 60]].map(([key, label, min, max]) => )}
{check('holdNear', 'Удерживать ближайший рейс за 2 минуты')}{check('showNextSameRoute', 'Следующий рейс того же направления')}{check('longWaitEnabled', 'Крупное время при долгом ожидании')}

Ночной режим

{select('nightMode', 'Ночной режим', [['off', 'Выключен'], ['auto', 'Автоматически']])} {settings.nightMode === 'auto' && select('nightWakeMinutes', 'Вернуть обычное табло', [[45, 'За 45 минут до рейса'], [60, 'За 60 минут до рейса']], true)}

Во время ночного перерыва длиннее двух часов. Обычный экран вернётся за выбранное время до первого рейса. По полному расписанию остановки.

Блоки информации

{check('infoServiceEnabled', 'Дальше по ближайшему рейсу')}{check('infoWeatherEnabled', 'Погода')}{check('infoQrEnabled', 'QR-код расписания')}{check('journeyTranslitEnabled', 'Транслит названий остановок')} {check('lastTripNoticeEnabled', 'Напоминание о последнем рейсе')}{check('weatherHintEnabled', 'Вероятность осадков')}{check('infoPlacesEnabled', 'До важных мест')}{check('infoFareEnabled', 'Оплата поездки')}

Реклама · предпросмотр

{check('adsEnabled', 'Добавить рекламу в цикл')} {settings.adsEnabled && <>