egorin: макет горэлектросетей — ТП + автопарк бригад (автовышка) на карте, рация с доской объявлений (мигание+чекбоксы) и списком чтения, 2ГИС по умолчанию

This commit is contained in:
german
2026-07-08 22:52:21 +03:00
parent b1c52ab748
commit b2836af333
9 changed files with 218 additions and 50 deletions
+55 -2
View File
@@ -1,5 +1,5 @@
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Radio, X, Volume2, VolumeX, Play } from 'lucide-react'; import { Radio, X, Volume2, VolumeX, Play, Megaphone, ListChecks, Trash2, CheckSquare, Square, Volume1 } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useRadio, speak, beep } from '@/core/radio'; import { useRadio, speak, beep } from '@/core/radio';
@@ -8,9 +8,11 @@ const DOT: Record<string, string> = { critical: 'bg-crit', warning: 'bg-warn', i
// Мини-окно «Рация»: включение/отключение оповещения + эфир. Открывается кнопкой в шапке. // Мини-окно «Рация»: включение/отключение оповещения + эфир. Открывается кнопкой в шапке.
export function RadioMiniWindow({ open, onClose }: { open: boolean; onClose: () => void }) { export function RadioMiniWindow({ open, onClose }: { open: boolean; onClose: () => void }) {
const { on, ether, toggle } = useRadio(); const { on, ether, toggle, board, readList, toggleTask, removeBoard, enqueueRead, removeRead, clearRead, readAll } = useRadio();
if (!open) return null; if (!open) return null;
const openTasks = board.filter((b) => b.kind === 'task' && !b.done).length;
const test = () => { const test = () => {
beep(on ? 880 : 440, 0.14, 0.25); beep(on ? 880 : 440, 0.14, 0.25);
if (on) speak('Проверка связи. Рация в эфире.'); if (on) speak('Проверка связи. Рация в эфире.');
@@ -64,6 +66,57 @@ export function RadioMiniWindow({ open, onClose }: { open: boolean; onClose: ()
))} ))}
</div> </div>
</div> </div>
{/* Доска объявлений */}
<div className="border-t border-border">
<div className="flex items-center justify-between px-3 py-1.5">
<span className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-fg-muted"><Megaphone className="h-3 w-3" /> Доска объявлений</span>
{openTasks > 0 && <span className="rounded-full bg-warn/20 px-1.5 text-[10px] font-semibold text-warn">{openTasks} задач</span>}
</div>
<div className="max-h-52 space-y-1 overflow-y-auto px-2 pb-2">
{board.map((b) => (
<div key={b.id} className={cn('group flex items-start gap-2 rounded-lg px-2 py-1.5', b.urgent && !b.done ? 'sm-blink bg-crit/10' : 'hover:bg-muted/50')}>
{b.kind === 'task' ? (
<button onClick={() => toggleTask(b.id)} title={b.done ? 'Открыть задачу' : 'Закрыть задачу'} className={cn('mt-0.5 shrink-0', b.done ? 'text-ok' : 'text-fg-muted hover:text-fg')}>
{b.done ? <CheckSquare className="h-4 w-4" /> : <Square className="h-4 w-4" />}
</button>
) : (
<span className={cn('mt-1 h-2 w-2 shrink-0 rounded-full', b.urgent ? 'bg-crit' : 'bg-info')} />
)}
<div className="min-w-0 flex-1">
<div className={cn('text-[11px] leading-snug', b.done ? 'text-fg-muted line-through' : 'text-fg')}>{b.text}</div>
</div>
<div className="flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<button onClick={() => enqueueRead(b.text)} title="В список чтения (озвучить)" className="text-fg-muted hover:text-accent"><Volume1 className="h-3.5 w-3.5" /></button>
<button onClick={() => removeBoard(b.id)} title="Удалить" className="text-fg-muted hover:text-crit"><Trash2 className="h-3.5 w-3.5" /></button>
</div>
</div>
))}
</div>
</div>
{/* Список чтения — голосовые уведомления */}
<div className="border-t border-border">
<div className="flex items-center justify-between px-3 py-1.5">
<span className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-fg-muted"><ListChecks className="h-3 w-3" /> Список чтения · {readList.length}</span>
<div className="flex items-center gap-1">
<button onClick={readAll} disabled={readList.length === 0} className="flex items-center gap-1 rounded-md bg-accent/15 px-2 py-0.5 text-[10px] font-semibold text-accent hover:bg-accent/25 disabled:opacity-40">
<Volume2 className="h-3 w-3" /> Читать
</button>
<button onClick={clearRead} disabled={readList.length === 0} title="Очистить" className="text-fg-muted hover:text-crit disabled:opacity-40"><Trash2 className="h-3.5 w-3.5" /></button>
</div>
</div>
<div className="max-h-36 space-y-0.5 overflow-y-auto px-2 pb-2">
{readList.length === 0 && <div className="px-2 py-3 text-center text-[11px] text-fg-muted">Список пуст. Добавьте объявление кнопкой 🔈.</div>}
{readList.map((t, i) => (
<div key={i} className="flex items-start gap-2 rounded-lg px-2 py-1 hover:bg-muted/50">
<button onClick={() => speak(t)} title="Прочитать" className="mt-0.5 shrink-0 text-fg-muted hover:text-accent"><Play className="h-3.5 w-3.5" /></button>
<div className="min-w-0 flex-1 text-[11px] leading-snug text-fg-muted">{t}</div>
<button onClick={() => removeRead(i)} title="Убрать" className="shrink-0 text-fg-muted hover:text-crit"><X className="h-3.5 w-3.5" /></button>
</div>
))}
</div>
</div>
</div>, </div>,
document.body, document.body,
); );
+1 -1
View File
@@ -59,7 +59,7 @@ export const USERS: User[] = [
id: 'energetik-ro', id: 'energetik-ro',
name: 'Сидоров А.', name: 'Сидоров А.',
role: 'Энергетик · часть, только просмотр', role: 'Энергетик · часть, только просмотр',
access: { energy: { caps: ['view'], objects: ['cab-07'] } }, access: { energy: { caps: ['view'], objects: ['tp-07'] } },
}, },
{ {
id: 'energetik-rw', id: 'energetik-rw',
+68
View File
@@ -68,11 +68,51 @@ export function radioReport(ev: MonitorEvent) {
setTimeout(() => speak(`${prefix}${ev.callsign}. ${ev.text}`), ev.severity === 'critical' ? 500 : 200); setTimeout(() => speak(`${prefix}${ev.callsign}. ${ev.text}`), ev.severity === 'critical' ? 500 : 200);
} }
// Доска объявлений: объявления и задачи бригад. Задачи закрываются чекбоксом (зачёркивание).
export interface BoardItem {
id: string;
text: string;
kind: 'announce' | 'task';
urgent?: boolean; // мигающее уведомление
done?: boolean; // задача закрыта → текст перечёркнут
ts: string;
}
const nowISO = () => new Date().toISOString();
const uid = () => 'b' + Math.random().toString(36).slice(2, 9);
const BOARD_SEED: BoardItem[] = [
{ id: 'brd-1', text: 'АВАРИЯ: обесточен ТП-14 (мкр. Бештау). Бригада ВЛ-1 в пути.', kind: 'announce', urgent: true, ts: nowISO() },
{ id: 'brd-2', text: 'Плановое отключение ТП-7 сегодня 14:00–16:00 (замена трансформатора).', kind: 'announce', ts: nowISO() },
{ id: 'brd-3', text: 'Заменить изолятор на опоре №42, ВЛ-10кВ «Центр».', kind: 'task', ts: nowISO() },
{ id: 'brd-4', text: 'Автовышке АГП-22: обрезка веток у ЛЭП, ул. Лесная.', kind: 'task', urgent: true, ts: nowISO() },
{ id: 'brd-5', text: 'Снять показания счётчиков ТП-3, ТП-9.', kind: 'task', done: true, ts: nowISO() },
];
const READ_SEED: string[] = [
'Внимание бригадам. На ТП-14 пропало сетевое питание, потребители переведены на резерв.',
'Диспетчер: автовышке АГП-22 подтвердить прибытие на улицу Лесная.',
];
function loadBoard(): BoardItem[] {
try { const j = localStorage.getItem('sm-radio-board'); if (j) return JSON.parse(j); } catch { /* ignore */ }
return BOARD_SEED;
}
function saveBoard(v: BoardItem[]) { try { localStorage.setItem('sm-radio-board', JSON.stringify(v)); } catch { /* ignore */ } }
interface RadioState { interface RadioState {
on: boolean; on: boolean;
ether: MonitorEvent[]; // лента «эфир» ether: MonitorEvent[]; // лента «эфир»
board: BoardItem[]; // доска объявлений
readList: string[]; // список чтения (очередь голосовых уведомлений)
toggle: () => void; toggle: () => void;
push: (ev: MonitorEvent) => void; push: (ev: MonitorEvent) => void;
toggleTask: (id: string) => void; // закрыть/открыть задачу (зачёркивание)
addBoard: (text: string, kind?: BoardItem['kind'], urgent?: boolean) => void;
removeBoard: (id: string) => void;
enqueueRead: (text: string) => void; // добавить голосовое уведомление в список чтения
removeRead: (i: number) => void;
clearRead: () => void;
readAll: () => void; // озвучить весь список чтения по очереди
} }
const initOn = (() => { const initOn = (() => {
@@ -86,6 +126,34 @@ const initOn = (() => {
export const useRadio = create<RadioState>((set, get) => ({ export const useRadio = create<RadioState>((set, get) => ({
on: initOn, on: initOn,
ether: [], ether: [],
board: loadBoard(),
readList: READ_SEED,
toggleTask: (id) =>
set((s) => {
const board = s.board.map((b) => (b.id === id ? { ...b, done: !b.done } : b));
saveBoard(board);
return { board };
}),
addBoard: (text, kind = 'announce', urgent = false) =>
set((s) => {
const board = [{ id: uid(), text, kind, urgent, ts: nowISO() }, ...s.board].slice(0, 60);
saveBoard(board);
return { board };
}),
removeBoard: (id) =>
set((s) => {
const board = s.board.filter((b) => b.id !== id);
saveBoard(board);
return { board };
}),
enqueueRead: (text) => set((s) => ({ readList: [...s.readList, text].slice(0, 40) })),
removeRead: (i) => set((s) => ({ readList: s.readList.filter((_, idx) => idx !== i) })),
clearRead: () => set({ readList: [] }),
readAll: () => {
ctx(); // прайм аудио на жесте
const list = get().readList;
list.forEach((t, i) => setTimeout(() => speak(t), i * 200)); // speechSynthesis сам ставит в очередь
},
toggle: () => { toggle: () => {
const on = !get().on; const on = !get().on;
try { try {
+48 -38
View File
@@ -63,70 +63,80 @@ export const wbChannels: WbChannel[] = [
{ id: 'mr6c_214/IN1', device: 'WB-MR6C #214', name: 'Датчик двери', kind: 'input', on: false }, { id: 'mr6c_214/IN1', device: 'WB-MR6C #214', name: 'Датчик двери', kind: 'input', on: false },
]; ];
// --- Transport / GPS: объекты Navtelecom (FLEX/NTCB) --- // --- Городские электросети: бригадный автопарк (GPS) + трансформаторные подстанции (энерго) ---
const iso = (minAgo = 0) => new Date(Date.now() - minAgo * 60000).toISOString();
export const entities: MonitoredEntity[] = [ export const entities: MonitoredEntity[] = [
// Бригадный автопарк — 5 машин (включая автовышку)
{ {
id: 'veh-123', id: 'veh-agp22', clientId: 'tempelhoff', name: 'Автовышка АГП-22', type: 'transport', online: true,
clientId: 'tempelhoff', telemetry: { speed: 28, sat: 12, fuel: 64, voltage: 13.9 }, meta: { boom: 'поднята' }, lat: 44.046, lon: 43.062, updatedAt: iso(0),
name: 'ГАЗель-123',
type: 'transport',
online: true,
telemetry: { speed: 64, sat: 11, fuel: 58, voltage: 13.8 },
lat: 44.05,
lon: 43.06,
updatedAt: new Date().toISOString(),
}, },
{ {
id: 'veh-204', id: 'veh-vl1', clientId: 'tempelhoff', name: 'Бригада ВЛ-1 (ГАЗель)', type: 'transport', online: true,
clientId: 'tempelhoff', telemetry: { speed: 0, sat: 10, fuel: 47, voltage: 12.8 }, lat: 44.081, lon: 43.001, updatedAt: iso(1),
name: 'МТЗ-82 трактор',
type: 'transport',
online: true,
telemetry: { speed: 0, sat: 9, fuel: 31, voltage: 12.9 },
lat: 44.02,
lon: 43.11,
updatedAt: new Date().toISOString(),
}, },
{ {
id: 'cab-07', id: 'veh-avar', clientId: 'tempelhoff', name: 'Аварийная бригада (УАЗ)', type: 'transport', online: true,
clientId: 'tempelhoff', telemetry: { speed: 54, sat: 11, fuel: 38, voltage: 13.7 }, lat: 44.058, lon: 43.041, updatedAt: iso(0),
name: 'Щит КТП-7', },
type: 'energy', {
online: false, id: 'veh-kabel', clientId: 'tempelhoff', name: 'Кабельщики (ГАЗ-3309)', type: 'transport', online: true,
telemetry: { pwr_ext: 0, pwr_int: 3.9, temp: 24 }, telemetry: { speed: 12, sat: 9, fuel: 57, voltage: 13.6 }, lat: 44.033, lon: 43.089, updatedAt: iso(0),
lat: 44.08, },
lon: 43.0, {
updatedAt: new Date(Date.now() - 9 * 60000).toISOString(), id: 'veh-master', clientId: 'tempelhoff', name: 'Мастер участка (Нива)', type: 'transport', online: true,
telemetry: { speed: 41, sat: 12, fuel: 72, voltage: 13.8 }, lat: 44.069, lon: 43.073, updatedAt: iso(0),
},
// Трансформаторные подстанции — узлы энергомониторинга
{
id: 'tp-14', clientId: 'tempelhoff', name: 'ТП-14 «Бештау»', type: 'energy', online: false,
telemetry: { load: 0, u: 0, i: 0, temp: 23 }, meta: { feeder: 'ВЛ-10 «Север»' }, lat: 44.082, lon: 43.002, updatedAt: iso(9),
},
{
id: 'tp-07', clientId: 'tempelhoff', name: 'ТП-7 «Центр»', type: 'energy', online: true,
telemetry: { load: 184, u: 402, i: 265, temp: 41 }, meta: { feeder: 'ВЛ-10 «Центр»' }, lat: 44.048, lon: 43.058, updatedAt: iso(0),
},
{
id: 'tp-03', clientId: 'tempelhoff', name: 'ТП-3 «Западный»', type: 'energy', online: true,
telemetry: { load: 96, u: 398, i: 139, temp: 33 }, meta: { feeder: 'ВЛ-10 «Запад»' }, lat: 44.041, lon: 43.02, updatedAt: iso(0),
},
{
id: 'tp-09', clientId: 'tempelhoff', name: 'ТП-9 «Южный»', type: 'energy', online: true,
telemetry: { load: 142, u: 401, i: 205, temp: 37 }, meta: { feeder: 'ВЛ-10 «Юг»' }, lat: 44.018, lon: 43.07, updatedAt: iso(0),
},
{
id: 'rp-02', clientId: 'tempelhoff', name: 'РП-2 (распределительный пункт)', type: 'energy', online: true,
telemetry: { load: 310, u: 6300, i: 29, temp: 29 }, meta: { feeder: 'ПС 110/10 «Город»' }, lat: 44.062, lon: 43.045, updatedAt: iso(0),
}, },
]; ];
export const events: MonitorEvent[] = [ export const events: MonitorEvent[] = [
{ {
id: 'e1', id: 'e1',
entityId: 'cab-07', entityId: 'tp-14',
callsign: 'Щит КТП-7', callsign: 'ТП-14 «Бештау»',
severity: 'critical', severity: 'critical',
type: 'power_loss', type: 'power_loss',
text: 'Пропадание сетевого питания, переход на ИБП.', text: 'Пропадание питания на ВЛ-10 «Север». Потребители обесточены, выехала ВЛ-1.',
ts: new Date(Date.now() - 9 * 60000).toISOString(), ts: new Date(Date.now() - 9 * 60000).toISOString(),
ackRequired: true, ackRequired: true,
}, },
{ {
id: 'e2', id: 'e2',
entityId: 'veh-204', entityId: 'tp-07',
callsign: 'МТЗ-82 трактор', callsign: 'ТП-7 «Центр»',
severity: 'warning', severity: 'warning',
type: 'fuel_low', type: 'overload',
text: 'Низкий уровень топлива (31%).', text: 'Перегрев трансформатора 41 °C при нагрузке 184 кВт.',
ts: new Date(Date.now() - 42 * 60000).toISOString(), ts: new Date(Date.now() - 42 * 60000).toISOString(),
}, },
{ {
id: 'e3', id: 'e3',
entityId: 'veh-123', entityId: 'veh-agp22',
callsign: 'ГАЗель-123', callsign: 'Автовышка АГП-22',
severity: 'info', severity: 'info',
type: 'geofence_in', type: 'geofence_in',
text: 'Вход в геозону «База».', text: 'Прибытие на объект: ул. Лесная (обрезка веток у ЛЭП).',
ts: new Date(Date.now() - 78 * 60000).toISOString(), ts: new Date(Date.now() - 78 * 60000).toISOString(),
}, },
]; ];
+5 -5
View File
@@ -16,11 +16,11 @@ export interface Trip {
const h = (n: number) => new Date(Date.now() - n * 3600000).toISOString(); const h = (n: number) => new Date(Date.now() - n * 3600000).toISOString();
export const trips: Trip[] = [ export const trips: Trip[] = [
{ id: 't1', entityId: 'veh-123', start: h(6), end: h(5.2), from: 'База «Тэмпельхоф»', to: 'Пятигорск, рынок', distanceKm: 18.4, durationMin: 48, maxSpeed: 78, avgSpeed: 41, route: [[44.07, 43.05], [44.061, 43.058], [44.052, 43.064], [44.043, 43.071]] }, { id: 't1', entityId: 'veh-avar', start: h(6), end: h(5.2), from: 'База «Тэмпельхоф»', to: 'Пятигорск, рынок', distanceKm: 18.4, durationMin: 48, maxSpeed: 78, avgSpeed: 41, route: [[44.07, 43.05], [44.061, 43.058], [44.052, 43.064], [44.043, 43.071]] },
{ id: 't2', entityId: 'veh-123', start: h(4.5), end: h(3.7), from: 'Пятигорск, рынок', to: 'Ессентуки, склад', distanceKm: 22.1, durationMin: 49, maxSpeed: 84, avgSpeed: 38, route: [[44.043, 43.071], [44.05, 43.0], [44.05, 42.93], [44.045, 42.86]] }, { id: 't2', entityId: 'veh-avar', start: h(4.5), end: h(3.7), from: 'Пятигорск, рынок', to: 'Ессентуки, склад', distanceKm: 22.1, durationMin: 49, maxSpeed: 84, avgSpeed: 38, route: [[44.043, 43.071], [44.05, 43.0], [44.05, 42.93], [44.045, 42.86]] },
{ id: 't3', entityId: 'veh-123', start: h(2), end: h(1.4), from: 'Ессентуки, склад', to: 'База «Тэмпельхоф»', distanceKm: 31.6, durationMin: 37, maxSpeed: 91, avgSpeed: 51, route: [[44.045, 42.86], [44.052, 42.95], [44.062, 43.0], [44.07, 43.05]] }, { id: 't3', entityId: 'veh-avar', start: h(2), end: h(1.4), from: 'Ессентуки, склад', to: 'База «Тэмпельхоф»', distanceKm: 31.6, durationMin: 37, maxSpeed: 91, avgSpeed: 51, route: [[44.045, 42.86], [44.052, 42.95], [44.062, 43.0], [44.07, 43.05]] },
{ id: 't4', entityId: 'veh-204', start: h(8), end: h(6.5), from: 'Поле №3', to: 'МТМ (ремонт)', distanceKm: 6.2, durationMin: 90, maxSpeed: 24, avgSpeed: 9, route: [[44.02, 43.11], [44.025, 43.1], [44.03, 43.095]] }, { id: 't4', entityId: 'veh-agp22', start: h(8), end: h(6.5), from: 'Поле №3', to: 'МТМ (ремонт)', distanceKm: 6.2, durationMin: 90, maxSpeed: 24, avgSpeed: 9, route: [[44.02, 43.11], [44.025, 43.1], [44.03, 43.095]] },
{ id: 't5', entityId: 'veh-204', start: h(3), end: h(2.2), from: 'МТМ', to: 'Поле №5', distanceKm: 8.8, durationMin: 48, maxSpeed: 28, avgSpeed: 12, route: [[44.03, 43.095], [44.038, 43.085], [44.045, 43.08]] }, { id: 't5', entityId: 'veh-agp22', start: h(3), end: h(2.2), from: 'МТМ', to: 'Поле №5', distanceKm: 8.8, durationMin: 48, maxSpeed: 28, avgSpeed: 12, route: [[44.03, 43.095], [44.038, 43.085], [44.045, 43.08]] },
]; ];
export function tripsOf(entityId: string): Trip[] { export function tripsOf(entityId: string): Trip[] {
+15
View File
@@ -61,6 +61,21 @@
} }
} }
/* Мигающее уведомление на доске объявлений */
.sm-blink {
animation: smblink 1.05s steps(1, end) infinite;
}
@keyframes smblink {
0%, 55% {
box-shadow: inset 3px 0 0 0 rgb(var(--crit, 239 68 68));
background-color: rgba(239, 68, 68, 0.14);
}
56%, 100% {
box-shadow: inset 3px 0 0 0 transparent;
background-color: rgba(239, 68, 68, 0.04);
}
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { * {
animation-duration: 0.001ms !important; animation-duration: 0.001ms !important;
+2 -1
View File
@@ -92,7 +92,8 @@ export function MapView({
if (!elRef.current || mapRef.current) return; if (!elRef.current || mapRef.current) return;
const map = L.map(elRef.current, { center: [44.05, 43.06], zoom: 11, zoomControl: true }); const map = L.map(elRef.current, { center: [44.05, 43.06], zoom: 11, zoomControl: true });
const bases = baseLayers(); const bases = baseLayers();
bases[theme === 'dark' ? 'Тёмная' : 'Светлая'].addTo(map); // По умолчанию — 2ГИС на всех разделах (переключение слоёв остаётся в контроле)
(bases['2ГИС'] ?? bases[theme === 'dark' ? 'Тёмная' : 'Светлая']).addTo(map);
L.control.layers(bases, undefined, { position: 'topright', collapsed: true }).addTo(map); L.control.layers(bases, undefined, { position: 'topright', collapsed: true }).addTo(map);
mapRef.current = map; mapRef.current = map;
+23 -2
View File
@@ -27,8 +27,29 @@ interface Props {
events?: Array<{ id: string; lat: number; lon: number; color: string; char: string; label: string; sub?: string }>; events?: Array<{ id: string; lat: number; lon: number; color: string; char: string; label: string; sub?: string }>;
} }
const STYLE = (dark: boolean) => // По умолчанию — 2ГИС (растровый источник) на всех разделах. dark/light не влияет на тайлы 2ГИС.
`https://basemaps.cartocdn.com/gl/${dark ? 'dark-matter' : 'positron'}-gl-style/style.json`; const STYLE = (_dark: boolean): any => ({
version: 8,
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
sources: {
'2gis': {
type: 'raster',
tiles: [
'https://tile0.maps.2gis.com/tiles?x={x}&y={y}&z={z}',
'https://tile1.maps.2gis.com/tiles?x={x}&y={y}&z={z}',
'https://tile2.maps.2gis.com/tiles?x={x}&y={y}&z={z}',
'https://tile3.maps.2gis.com/tiles?x={x}&y={y}&z={z}',
],
tileSize: 256,
attribution: '© 2ГИС',
maxzoom: 18,
},
},
layers: [
{ id: 'bg', type: 'background', paint: { 'background-color': '#0b0f14' } },
{ id: '2gis', type: 'raster', source: '2gis' },
],
});
function objColor(e: MonitoredEntity, colors?: Record<string, string>): string { function objColor(e: MonitoredEntity, colors?: Record<string, string>): string {
if (colors?.[e.id]) return colors[e.id]; if (colors?.[e.id]) return colors[e.id];
+1 -1
View File
@@ -27,7 +27,7 @@ export function useSmartData(user: User): { channels: WbChannel[]; events: Monit
[ [
{ {
id: 'door-' + new Date().toISOString(), id: 'door-' + new Date().toISOString(),
entityId: 'cab-07', entityId: 'tp-07',
callsign: 'Датчик двери · WB-MR6C #214', callsign: 'Датчик двери · WB-MR6C #214',
severity: (open ? 'warning' : 'info') as MonitorEvent['severity'], severity: (open ? 'warning' : 'info') as MonitorEvent['severity'],
type: open ? 'door_open' : 'door_closed', type: open ? 'door_open' : 'door_closed',