aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-07 14:58:32 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-07 14:58:32 +0100
commit52256fd634300b39b915bf1db6020d9d2871a0b4 (patch)
treed32b219427e5974026c544132e3b0a65ffa0a82e /src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
parent4420def7411a053e930b44117e2bf63625d824dc (diff)
Implement experimental consolidated circulation view
Diffstat (limited to 'src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx')
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx217
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>
+ );
+};