import { useEffect, useState, useRef } from "react"; import { useParams, Link } from "react-router"; import StopDataProvider from "../data/StopDataProvider"; import { ArrowLeft, Eye, EyeOff } from "lucide-react"; import { TimetableTable, type TimetableEntry } from "../components/TimetableTable"; import LineIcon from "../components/LineIcon"; import { useTranslation } from "react-i18next"; import "./timetable-$id.css"; const loadTimetableData = async (stopId: string) => { const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format try { const resp = await fetch(`/api/GetStopTimetable?date=${today}&stopId=${stopId}`, { headers: { Accept: "application/json", }, }); if (!resp.ok) { throw new Error(`HTTP error! status: ${resp.status}`); } return await resp.json(); } catch (error) { console.error('Error loading timetable data:', error); return []; } }; // Utility function to compare times const timeToMinutes = (time: string): number => { const [hours, minutes] = time.split(':').map(Number); return hours * 60 + minutes; }; // Filter past entries (keep only a few recent past ones) const filterTimetableData = (data: TimetableEntry[], currentTime: string, showPast: boolean = false): TimetableEntry[] => { if (showPast) return data; const currentMinutes = timeToMinutes(currentTime); const sortedData = [...data].sort((a, b) => timeToMinutes(a.departure_time) - timeToMinutes(b.departure_time) ); // Find the current position const currentIndex = sortedData.findIndex(entry => timeToMinutes(entry.departure_time) >= currentMinutes ); if (currentIndex === -1) { // All entries are in the past, show last 3 return sortedData.slice(-3); } // Show 3 past entries + all future entries const startIndex = Math.max(0, currentIndex - 3); return sortedData.slice(startIndex); }; // Utility function to parse service ID and get the turn number const parseServiceId = (serviceId: string): string => { const parts = serviceId.split('_'); if (parts.length === 0) return ''; const lastPart = parts[parts.length - 1]; if (lastPart.length < 6) return ''; const last6 = lastPart.slice(-6); const lineCode = last6.slice(0, 3); const turnCode = last6.slice(-3); // Remove leading zeros from turn const turnNumber = parseInt(turnCode, 10).toString(); // Parse line number with special cases const lineNumber = parseInt(lineCode, 10); let displayLine: string; switch (lineNumber) { case 1: displayLine = "C1"; break; case 3: displayLine = "C3"; break; case 30: displayLine = "N1"; break; case 33: displayLine = "N4"; break; case 8: displayLine = "A"; break; case 101: displayLine = "H"; break; case 150: displayLine = "REF"; break; case 500: displayLine = "TUR"; break; default: displayLine = `L${lineNumber}`; } return `${displayLine}-${turnNumber}`; }; export default function Timetable() { const { t } = useTranslation(); const params = useParams(); const stopIdNum = parseInt(params.id ?? ""); const [timetableData, setTimetableData] = useState([]); const [customName, setCustomName] = useState(undefined); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showPastEntries, setShowPastEntries] = useState(false); const nextEntryRef = useRef(null); const currentTime = new Date().toTimeString().slice(0, 8); // HH:MM:SS const filteredData = filterTimetableData(timetableData, currentTime, showPastEntries); useEffect(() => { loadTimetableData(params.id!).then((timetableBody: TimetableEntry[]) => { setTimetableData(timetableBody); setLoading(false); if (timetableBody.length === 0) { setError(t("timetable.noDataAvailable", "No hay datos de horarios disponibles para hoy")); } else { // Scroll to next entry after a short delay to allow rendering setTimeout(() => { const currentMinutes = timeToMinutes(currentTime); const sortedData = [...timetableBody].sort((a, b) => timeToMinutes(a.departure_time) - timeToMinutes(b.departure_time) ); const nextIndex = sortedData.findIndex(entry => timeToMinutes(entry.departure_time) >= currentMinutes ); if (nextIndex !== -1 && nextEntryRef.current) { nextEntryRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 500); } }).catch((err) => { setError(t("timetable.loadError", "Error al cargar los horarios")); setLoading(false); }); setCustomName(StopDataProvider.getCustomName(stopIdNum)); }, [params.id, stopIdNum, t, currentTime]); if (loading) { return

{t("common.loading")}

; } // Get stop name from timetable data or use stop ID const stopName = customName || (timetableData.length > 0 ? `Parada ${params.id}` : `Parada ${params.id}`); return (

{t("timetable.fullTitle", "Horarios teóricos")} ({params.id})

{t("timetable.backToEstimates", "Volver a estimaciones")}
{error ? (

{error}

{t("timetable.errorDetail", "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.")}

) : (
)}
); } // Custom component for the full timetable with scroll reference const TimetableTableWithScroll: React.FC<{ data: TimetableEntry[]; showAll: boolean; currentTime: string; nextEntryRef: React.RefObject; }> = ({ data, showAll, currentTime, nextEntryRef }) => { const { t } = useTranslation(); const nowMinutes = timeToMinutes(currentTime); return (
{t("timetable.fullCaption", "Horarios teóricos de la parada")}
{data.map((entry, index) => { const entryMinutes = timeToMinutes(entry.departure_time); const isPast = entryMinutes < nowMinutes; const isNext = !isPast && (index === 0 || timeToMinutes(data[index - 1]?.departure_time || '00:00:00') < nowMinutes); return (
{entry.trip.headsign && entry.trip.headsign.trim() ? ( {entry.trip.headsign} ) : ( {t("timetable.noDestination", "Línea")} {entry.line.name} )}
{entry.departure_time.slice(0, 5)}
{parseServiceId(entry.trip.service_id)}
{!isPast && entry.next_streets.length > 0 && (
{entry.next_streets.join(' — ')}
)}
); })}
{data.length === 0 && (

{t("timetable.noData", "No hay datos de horarios disponibles")}

)}
); };