diff options
Diffstat (limited to 'src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx')
| -rw-r--r-- | src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx new file mode 100644 index 0000000..a1b50f2 --- /dev/null +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx @@ -0,0 +1,217 @@ +import { useTranslation } from "react-i18next"; +import { Clock } from "lucide-react"; +import { type ConsolidatedCirculation } from "~routes/stops-$id"; +import LineIcon from "~components/LineIcon"; +import { type RegionConfig } from "~data/RegionConfig"; + +import './ConsolidatedCirculationList.css'; + +interface RegularTableProps { + data: ConsolidatedCirculation[]; + dataDate: Date | null; + regionConfig: RegionConfig; +} + +// 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; + case 201: + displayLine = "U1"; + break; + case 202: + displayLine = "U2"; + break; + default: + displayLine = `L${lineNumber}`; + } + + return `${displayLine}-${turnNumber}`; +}; + +export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({ + data, + dataDate, + regionConfig, +}) => { + const { t } = useTranslation(); + + const absoluteArrivalTime = (minutes: number) => { + const now = new Date(); + const arrival = new Date(now.getTime() + minutes * 60000); + return Intl.DateTimeFormat( + typeof navigator !== "undefined" ? navigator.language : "en", + { + hour: "2-digit", + minute: "2-digit", + }, + ).format(arrival); + }; + + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} ${t("estimates.meters", "m")}`; + } + }; + + const getDelayText = (estimate: ConsolidatedCirculation): string | null => { + if (!estimate.schedule || !estimate.realTime) { + return null; + } + + const delay = estimate.realTime.minutes - estimate.schedule.minutes; + + if (delay >= -1 && delay <= 2) { + return t("estimates.on_time", "on time"); + } else if (delay > 2) { + return t("estimates.minutes_late", "{{minutes}} minutes late", { minutes: delay }); + } else { + return t("estimates.minutes_early", "{{minutes}} minutes early", { minutes: Math.abs(delay) }); + } + }; + + const getTripIdDisplay = (tripId: string): string => { + const parts = tripId.split("_"); + return parts.length > 1 ? parts[1] : tripId; + }; + + const getTimeClass = (estimate: ConsolidatedCirculation): string => { + if (estimate.realTime && estimate.schedule?.running) { + return "time-running"; + } + + if (estimate.realTime && !estimate.schedule) { + return "time-running"; + } + + else if (estimate.realTime && !estimate.schedule?.running) { + return "time-delayed"; + } + + return "time-scheduled"; + }; + + const sortedData = [...data].sort( + (a, b) => (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - + (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) + ); + + return ( + <div className="consolidated-circulation-container"> + <div className="consolidated-circulation-caption"> + {t("estimates.caption", "Estimaciones de llegadas a las {{time}}", { + time: dataDate?.toLocaleTimeString(), + })} + </div> + + {sortedData.length === 0 ? ( + <div className="consolidated-circulation-no-data"> + {t("estimates.none", "No hay estimaciones disponibles")} + </div> + ) : ( + <div className="consolidated-circulation-list"> + {sortedData.map((estimate, idx) => { + const displayMinutes = estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; + const timeClass = getTimeClass(estimate); + const delayText = getDelayText(estimate); + + return ( + <div key={idx} className="consolidated-circulation-card"> + <div className="card-header"> + <div className="line-info"> + <LineIcon line={estimate.line} region={regionConfig.id} /> + </div> + + <div className="route-info"> + <strong>{estimate.route}</strong> + </div> + + <div className="time-info"> + <div className={`arrival-time ${timeClass}`}> + <Clock /> + {estimate.realTime + ? `${displayMinutes} ${t("estimates.minutes", "min")}` + : absoluteArrivalTime(displayMinutes)} + </div> + {estimate.realTime && estimate.realTime.distance >= 0 && ( + <div className="distance-info"> + {formatDistance(estimate.realTime.distance)} + </div> + )} + </div> + </div> + + <div className="card-footer"> + <span className="status-text"> + {delayText && ( + <> + {t("estimates.bus_is", "Bus is")} {delayText}.{" "} + </> + )} + </span> + <span className="status-text"> + {estimate.schedule ? ( + <> + {t("estimates.service", "Service")}{" "} + {parseServiceId(estimate.schedule.serviceId)} + {", "} + {t("estimates.trip", "trip")}{" "} + {getTripIdDisplay(estimate.schedule.tripId)} + </> + ) : ( + <> + {t("estimates.unknown_service", "Unknown service. It may be a reinforcement or the service has a different name than planned.")} + </> + )} + </span> + </div> + </div> + ); + })} + </div> + )} + </div> + ); +}; |
