From 9409e52a7fe14575966962c4fc3fbf248699cd96 Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
Date: Sun, 1 Mar 2026 11:26:27 +0100
Subject: Route detail visualisation improvement

Squashed commit of the following:

commit 2f2261f764e0a0a52652bceda306f39f6f568b87
Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date:   Sun Mar 1 10:21:52 2026 +0000

    Implement route-specific realtime filtering and route detail UI updates

    Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>

commit e5ab68b158558e0f6577bf0fdd95e652fb269e6a
Author: Ariel Costas Guerrero <ariel@costas.dev>
Date:   Sun Mar 1 11:17:19 2026 +0100

    Fix delay formatting to ensure absolute values are displayed for arrivals and rejections

commit df7d61c089a4e55a3b2efad8556b17e1f7f25e1c
Author: Ariel Costas Guerrero <ariel@costas.dev>
Date:   Sun Mar 1 11:14:11 2026 +0100

    Improve the formatting a bit for the arrival schedules

commit 4b65cdb43824ba936234be6b5bdd6cf8ac9c56bb
Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date:   Sun Mar 1 10:05:34 2026 +0000

    Fix hook-order violation in route details stop departures

    Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>

commit 2da4fb594f1433ddd1a26e267bbc7e917145b3b5
Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date:   Sun Mar 1 09:50:14 2026 +0000

    Polish selected-stop realtime display in route details

    Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>

commit dc7fc11085773a030bc9109e8c435a62a3567051
Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date:   Sun Mar 1 09:48:33 2026 +0000

    Load route-details realtime only for selected stop

    Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>

commit b9408664fd0c0d115f6aa0341deb9fa5b74f2b26
Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date:   Fri Feb 27 18:20:16 2026 +0000

    Initial plan
---
 src/frontend/app/routes/routes-$id.tsx | 175 +++++++++++++++++++++++++++++++--
 1 file changed, 166 insertions(+), 9 deletions(-)

(limited to 'src/frontend/app/routes/routes-$id.tsx')

diff --git a/src/frontend/app/routes/routes-$id.tsx b/src/frontend/app/routes/routes-$id.tsx
index 32f1fb7..6cc872d 100644
--- a/src/frontend/app/routes/routes-$id.tsx
+++ b/src/frontend/app/routes/routes-$id.tsx
@@ -24,6 +24,7 @@ import {
   usePageTitle,
   usePageTitleNode,
 } from "~/contexts/PageTitleContext";
+import { useStopArrivals } from "~/hooks/useArrivals";
 import { formatHex } from "~/utils/colours";
 import "../tailwind-full.css";
 
@@ -55,12 +56,51 @@ export default function RouteDetailsPage() {
     () => formatDateKey(selectedWeekDate),
     [selectedWeekDate]
   );
+  const ONE_HOUR_SECONDS = 3600;
+  const isTodaySelectedDate = selectedDateKey === formatDateKey(new Date());
+  const now = new Date();
+  const nowSeconds =
+    now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
+  const formatDelayMinutes = (delayMinutes: number) => {
+    if (delayMinutes === 0) return "OK";
+    return delayMinutes > 0
+      ? ` (R${Math.abs(delayMinutes)})`
+      : ` (A${Math.abs(delayMinutes)})`;
+  };
 
   const { data: route, isLoading } = useQuery({
     queryKey: ["route", id, selectedDateKey],
     queryFn: () => fetchRouteDetails(id!, selectedDateKey),
     enabled: !!id,
   });
+  const { data: selectedStopRealtime, isLoading: isRealtimeLoading } =
+    useStopArrivals(
+      selectedStopId ?? "",
+      true,
+      Boolean(selectedStopId) && isTodaySelectedDate
+    );
+  const filteredRealtimeArrivals = useMemo(() => {
+    const arrivals = selectedStopRealtime?.arrivals ?? [];
+    if (arrivals.length === 0) {
+      return [];
+    }
+
+    const routeId = id?.trim();
+    const routeShortName = route?.shortName?.trim().toLowerCase();
+
+    return arrivals.filter((arrival) => {
+      const arrivalGtfsId = arrival.route.gtfsId?.trim();
+      if (routeId && arrivalGtfsId) {
+        return arrivalGtfsId === routeId;
+      }
+
+      if (routeShortName) {
+        return arrival.route.shortName.trim().toLowerCase() === routeShortName;
+      }
+
+      return true;
+    });
+  }, [selectedStopRealtime?.arrivals, id, route?.shortName]);
 
   usePageTitle(
     route?.shortName
@@ -161,6 +201,35 @@ export default function RouteDetailsPage() {
   const selectedPatternLabel = selectedPattern
     ? selectedPattern.headsign || selectedPattern.name
     : t("routes.details", "Detalles de ruta");
+  const sameDirectionPatterns = selectedPattern
+    ? (patternsByDirection[selectedPattern.directionId] ?? [])
+    : [];
+  const departuresByStop = (() => {
+    const byStop = new Map<
+      string,
+      { departure: number; patternId: string; tripId?: string | null }[]
+    >();
+
+    for (const pattern of sameDirectionPatterns) {
+      for (const stop of pattern.stops) {
+        const current = byStop.get(stop.id) ?? [];
+        current.push(
+          ...stop.scheduledDepartures.map((departure) => ({
+            departure,
+            patternId: pattern.id,
+            tripId: null,
+          }))
+        );
+        byStop.set(stop.id, current);
+      }
+    }
+
+    for (const stopDepartures of byStop.values()) {
+      stopDepartures.sort((a, b) => a.departure - b.departure);
+    }
+
+    return byStop;
+  })();
 
   const mapHeightClass =
     layoutMode === "map"
@@ -274,14 +343,33 @@ export default function RouteDetailsPage() {
               {selectedPattern?.geometry && (
                 <Source type="geojson" data={geojson}>
                   <Layer
-                    id="route-line"
+                    id="route-line-border"
+                    type="line"
+                    paint={{
+                      "line-color":
+                        route.textColor && route.textColor.trim()
+                          ? formatHex(route.textColor)
+                          : "#111827",
+                      "line-width": 7,
+                      "line-opacity": 0.75,
+                    }}
+                    layout={{
+                      "line-cap": "round",
+                      "line-join": "round",
+                    }}
+                  />
+                  <Layer
+                    id="route-line-inner"
                     type="line"
                     paint={{
                       "line-color": route.color
                         ? formatHex(route.color)
                         : "#3b82f6",
-                      "line-width": 4,
-                      "line-opacity": 0.8,
+                      "line-width": 5,
+                    }}
+                    layout={{
+                      "line-cap": "round",
+                      "line-join": "round",
                     }}
                   />
                 </Source>
@@ -551,24 +639,93 @@ export default function RouteDetailsPage() {
                     )}
 
                     {selectedStopId === stop.id &&
-                      stop.scheduledDepartures.length > 0 && (
+                      (departuresByStop.get(stop.id)?.length ?? 0) > 0 && (
                         <div className="mt-2 flex flex-wrap gap-1">
-                          {stop.scheduledDepartures.map((dep, i) => (
+                          {(
+                            departuresByStop
+                              .get(stop.id)
+                              ?.filter((item) =>
+                                isTodaySelectedDate
+                                  ? item.departure >=
+                                    nowSeconds - ONE_HOUR_SECONDS
+                                  : true
+                              ) ?? []
+                          ).map((item, i) => (
                             <span
-                              key={i}
-                              className="text-[10px] px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded"
+                              key={`${item.patternId}-${item.departure}-${i}`}
+                              className={`text-[11px] px-2 py-0.5 rounded ${
+                                item.patternId === selectedPattern?.id
+                                  ? "bg-gray-100 dark:bg-gray-900"
+                                  : "bg-gray-50 dark:bg-gray-900 text-gray-400 font-light"
+                              }`}
                             >
-                              {Math.floor(dep / 3600)
+                              {Math.floor(item.departure / 3600)
                                 .toString()
                                 .padStart(2, "0")}
                               :
-                              {Math.floor((dep % 3600) / 60)
+                              {Math.floor((item.departure % 3600) / 60)
                                 .toString()
                                 .padStart(2, "0")}
                             </span>
                           ))}
                         </div>
                       )}
+
+                    {selectedStopId === stop.id && isTodaySelectedDate && (
+                      <div className="mt-2">
+                        <div className="text-[10px] uppercase tracking-wide text-muted mb-1">
+                          {t("routes.realtime", "Tiempo real")}
+                        </div>
+                        {isRealtimeLoading ? (
+                          <div className="text-[11px] text-muted">
+                            {t("routes.loading_realtime", "Cargando...")}
+                          </div>
+                        ) : filteredRealtimeArrivals.length === 0 ? (
+                          <div className="text-[11px] text-muted">
+                            {t(
+                              "routes.realtime_no_route_estimates",
+                              "Sin estimaciones para esta línea"
+                            )}
+                          </div>
+                        ) : (
+                          <>
+                            <div className="flex items-center justify-between gap-2 rounded-lg border border-green-500/30 bg-green-500/10 px-2.5 py-2">
+                              <span className="text-[11px] font-semibold uppercase tracking-wide text-green-700 dark:text-green-300">
+                                {t("routes.next_arrival", "Próximo")}
+                              </span>
+                              <span className="inline-flex min-w-16 items-center justify-center rounded-xl bg-green-600 px-3 py-1.5 text-base font-bold leading-none text-white">
+                                {filteredRealtimeArrivals[0].estimate.minutes}′
+                                {filteredRealtimeArrivals[0].delay?.minutes
+                                  ? formatDelayMinutes(
+                                      filteredRealtimeArrivals[0].delay.minutes
+                                    )
+                                  : ""}
+                              </span>
+                            </div>
+
+                            {filteredRealtimeArrivals.length > 1 && (
+                              <div className="mt-2 flex flex-wrap justify-end gap-1">
+                                {filteredRealtimeArrivals
+                                  .slice(1)
+                                  .map((arrival, i) => (
+                                    <span
+                                      key={`${arrival.tripId}-${i}`}
+                                      className="text-[11px] px-2 py-0.5 bg-primary/10 text-primary rounded"
+                                    >
+                                      {arrival.estimate.minutes}′
+                                      {arrival.delay?.minutes
+                                        ? formatDelayMinutes(
+                                            arrival.delay.minutes
+                                          )
+                                        : ""}
+                                    </span>
+                                  ))}
+                              </div>
+                            )}
+                          </>
+                        )}
+                      </div>
+                    )}
                   </div>
                 </div>
               ))}
-- 
cgit v1.3

