// ScreenLineDetail.jsx — dual-track strip, station amenity popups, closed/extended states, transfer tags // Device tipo labels const TIPO_LABEL = { 0: 'Ascensor', 1: 'Escalera mecánica', 2: 'Salvaescaleras' }; function StationPopup({ station, lineId, onClose }) { const detail = { ...STATION_DEFAULT, ...(STATION_DETAIL[station] || {}) }; const crossings = CROSSINGS[station] || []; const isClosed = CLOSED_TODAY.includes(station); const isExtended = lineId === 'B' && B_EXTENDED_STATIONS.includes(station); const closedReason = CLOSED_REASONS[station]; const singleDir = detail.singleDir || 0; // Only show device panel for stations with actual Emova mappings const hasElevation = (detail.elevator || detail.escalator) && (EMOVA_STATION_ID[lineId]?.[station] != null); // ── Emova device state ──────────────────────────────────────────────────── const [devices, setDevices] = React.useState(undefined); // undefined=loading, []=loaded const [showDevices, setShowDevices] = React.useState(false); const [showInfo, setShowInfo] = React.useState(false); React.useEffect(() => { let cancelled = false; if (!hasElevation) { setDevices([]); return; } const cached = getEmovaDevices(lineId, station); if (cached !== null) { setDevices(cached); return; } loadEmovaStatus().then(() => { if (cancelled) return; setDevices(getEmovaDevices(lineId, station) || []); }); return () => { cancelled = true; }; }, [lineId, station]); // Derive presence + warning states from loaded Emova devices. // Emova is authoritative: if the API says a device is there, we show it // even if the static STATION_DETAIL doesn't list it. const hasElevatorDevices = Array.isArray(devices) && devices.some(d => d.device_tipo === 0); const hasEscalatorDevices = Array.isArray(devices) && devices.some(d => d.device_tipo === 1); const showElevator = detail.elevator || hasElevatorDevices; const showEscalator = detail.escalator || hasEscalatorDevices; const elevatorDown = hasElevatorDevices && devices.some(d => d.device_tipo === 0 && !d.funcionando); const escalatorDown = hasEscalatorDevices && devices.some(d => d.device_tipo === 1 && !d.funcionando); const anyDown = elevatorDown || escalatorDown; const fetchedAt = Array.isArray(devices) && devices[0]?.fetched_at; // ── Helpers ─────────────────────────────────────────────────────────────── const fmtDate = (iso) => { if (!iso) return null; const d = new Date(iso); return d.toLocaleDateString('es-AR', { day: 'numeric', month: 'numeric', year: 'numeric' }) + ' ' + d.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }); }; // Amenity icon: state = 'ok' | 'warn' | 'off' const amenityIcon = (present, warn, label, icon) => { const state = !present ? 'off' : warn ? 'warn' : 'ok'; const bg = state === 'ok' ? 'oklch(0.94 0.08 155)' : state === 'warn' ? 'oklch(0.96 0.12 85)' : 'var(--ink-100)'; const clr = state === 'ok' ? 'var(--ok)' : state === 'warn' ? 'oklch(0.55 0.18 75)' : 'var(--text-dim)'; return (
{stInfo.desc}
)} {artworks.length > 0 && (