// ScreenMain.jsx — home / dashboard // WX_CODES and useWeather are now defined in Data.jsx and exported via window. // ── Screen ─────────────────────────────────────────────────────────── function ScreenMain({ go, t, authed }) { const [tick, setTick] = React.useState(0); const [isFullscreen, setIsFullscreen] = React.useState(false); React.useEffect(() => { const i = setInterval(() => setTick(x => x + 1), 90); return () => clearInterval(i); }, []); React.useEffect(() => { const fn = () => setIsFullscreen(!!document.fullscreenElement); document.addEventListener('fullscreenchange', fn); return () => document.removeEventListener('fullscreenchange', fn); }, []); const toggleFullscreen = () => { if (!document.fullscreenElement) { (document.documentElement.requestFullscreen || document.documentElement.webkitRequestFullscreen || (() => {})).call(document.documentElement); } else { (document.exitFullscreen || document.webkitExitFullscreen || (() => {})).call(document); } }; const lineStatus = useLiveStatus(); const wx = useWeather(); // Exclude lines that are simply out-of-service-hours — those aren't "alerts" const alerts = Object.entries(lineStatus).filter(([id, s]) => s.state !== 'normal' && getServiceState(id).state !== 'closed' ); const mainLines = LINES.filter(l => ['A','B','C','D','E','H'].includes(l.id)); const extraLines = LINES.filter(l => ['U','P'].includes(l.id)); // ¿Toda la red cerrada? → próximo inicio const allClosedInfo = getAllClosedInfo(); // Anuncios desestimables (Emova X). Persistido en localStorage. const [dismissed, setDismissed] = React.useState(() => loadDismissed()); const activeAnns = React.useMemo(() => { const list = getActiveAnnouncements(dismissed); // Agregar anuncios del CMS (guardados en window.CMS_ANNOUNCEMENTS) if (window.CMS_ANNOUNCEMENTS && Array.isArray(window.CMS_ANNOUNCEMENTS)) { window.CMS_ANNOUNCEMENTS.forEach(a => { if (!dismissed.has(a.id)) list.push(a); }); } // Inyección automática: si el clima actual sugiere lluvia/tormenta y el // anuncio sintético no fue descartado, lo agregamos al tope. const wxId = 'auto-weather-' + (wx?.code ?? '?'); if (wx && wx.code >= 51 && wx.code <= 99 && !dismissed.has(wxId)) { list.unshift({ id: wxId, kind: 'weather', lineId: null, text: `${wx.icon} ${wx.desc} en CABA. Pueden producirse demoras y filtraciones en estaciones a cielo abierto.`, images: [], url: 'https://www.smn.gob.ar/', source: SOURCES.smn, createdAt: new Date().toISOString(), }); } return list; }, [dismissed, tick, wx]); const dismissAnn = (id) => { const next = new Set(dismissed); next.add(id); saveDismissed(next); setDismissed(next); }; // Status events — loaded once on mount, independent of push prefs const [statusEvents, setStatusEvents] = React.useState([]); React.useEffect(() => { loadStatusEvents(5).then(evs => setStatusEvents(evs || [])); }, []); return (
{/* weather icon + temp */} {wx ? ( <> {wx.icon}
{wx.temp}° ST {wx.feels}°
{wx.desc}
) : ( Buenos Aires )} {/* live update time — same horizontal row, pushed right */}
{new Date().toLocaleTimeString('es-AR',{hour:'2-digit',minute:'2-digit'})}
} subtitle={null} left={} right={
} /> {/* Summary banner */}
{allClosedInfo.allClosed ? (
Servicio finalizado en toda la red
Inicio del servicio nuevamente {allClosedInfo.opensDay} a las {allClosedInfo.opensAt}
) : alerts.length === 0 && CLOSED_TODAY.length === 0 ? (
{t('allNormal')}
) : alerts.length === 0 && CLOSED_TODAY.length > 0 ? (
{CLOSED_TODAY.length} estación{CLOSED_TODAY.length > 1 ? 'es' : ''} cerrada{CLOSED_TODAY.length > 1 ? 's' : ''}
{CLOSED_TODAY.join(' · ')}
) : (
{alerts.length} {t('linesWithIssues')}
{alerts.map(([id]) => id).join(' · ')}
)}
{/* Anuncios trascendentales (Emova X) — desestimables */} {activeAnns.length > 0 && (
{activeAnns.map(a => dismissAnn(a.id)} />)}
)} {/* Main line cards (A-H) — 2 cols mobile, 3 cols ≥540px */}
{mainLines.map(line => { const st = lineStatus[line.id]; const isY = line.id === 'H'; const txt = isY ? '#0B1220' : '#fff'; const sub = isY ? 'rgba(11,18,32,.7)' : 'rgba(255,255,255,.85)'; const hasClosed = line.stations.some(s => CLOSED_TODAY.includes(s)); const svc = getServiceState(line.id); const closed = svc.state === 'closed'; const closingSoon = svc.state === 'closing-soon'; const minsLeft = closingSoon ? Math.max(1, Math.round(svc.minsUntilClose)) : null; return ( ); })}
{/* Secondary lines: Urquiza + Premetro — wrap to column on narrow screens */}
{extraLines.map(line => { const st = lineStatus[line.id]; const svc = getServiceState(line.id); const closed = svc.state === 'closed'; const closingSoon = svc.state === 'closing-soon'; const minsLeft = closingSoon ? Math.max(1, Math.round(svc.minsUntilClose)) : null; return ( ); })}
{/* Mini map preview — Leaflet real map, non-interactive */}
{/* {t('liveTrains')} */}
{/* Click overlay routes to full map */}
go('map')} style={{ position: 'absolute', inset: 0, zIndex: 500, cursor: 'pointer' }} />
{/* Quick actions */}
{/* News teaser — últimos 5 días de cambios de estado (infosubte) */}

{t('news')}

{statusEvents.length === 0 ? (
Sin novedades en los últimos 5 días.
) : ( statusEvents.slice(0, 5).map((ev, i) => ( )) )}
); } // ── StatusEventCard ────────────────────────────────────────────────── // Muestra un evento de cambio de estado del infosubte (origen: ops.infosubte). function StatusEventCard({ event: ev }) { const isAlert = !!ev.isCurrent; return (
{isAlert && ( En curso )} {timeAgoLabel(ev.startedAt)} {ev.endedAt && !ev.isCurrent && ` · duró ${timeAgoLabel(ev.startedAt).replace('hace ','')}`}
{ev.text}
{ev.endedAt && !ev.isCurrent && (
Resuelto {timeAgoLabel(ev.endedAt)}
)}
); } // ── AnnouncementCard ───────────────────────────────────────────────── // Tarjeta destacada para anuncios trascendentales (Emova X). Muestra texto // + imágenes + link a la fuente. Se cierra con la X y se persiste en // localStorage; reaparece en "Novedades" abajo. function AnnouncementCard({ ann, onDismiss }) { const meta = ANNOUNCEMENT_KIND[ann.kind] || ANNOUNCEMENT_KIND.advisory; const tone = meta.tone; const bg = tone === 'error' ? 'oklch(0.96 0.06 25)' : tone === 'warn' ? 'oklch(0.96 0.05 75)' : tone === 'info' ? 'oklch(0.96 0.04 240)' : 'oklch(0.94 0.04 155)'; const bd = tone === 'error' ? 'oklch(0.78 0.18 25)' : tone === 'warn' ? 'oklch(0.85 0.12 75)' : tone === 'info' ? 'oklch(0.82 0.10 240)' : 'oklch(0.82 0.10 155)'; const fg = tone === 'error' ? 'oklch(0.40 0.18 25)' : tone === 'warn' ? 'oklch(0.35 0.15 75)' : tone === 'info' ? 'oklch(0.35 0.14 240)' : 'oklch(0.30 0.10 155)'; return (
{meta.label}
{ann.lineId && }
{ann.source && ( {ann.source.label}{ann.source.handle ? ` · ${ann.source.handle}` : ''} )} · {timeAgoLabel(ann.createdAt)}
{ann.text}
{ann.images && ann.images.length > 0 && (
{ann.images.map((src, i) => ( ))}
)} {ann.url && ( {(ann.source && ann.source.linkLabel) || 'Ver fuente'} )}
); } Object.assign(window, { ScreenMain, AnnouncementCard });