From 276e73412abef28c222c52a84334d49f5e414f3c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 23:39:08 +0100 Subject: Use consolidated data API in map sheet with shared card component (#100) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> Co-authored-by: Ariel Costas Guerrero --- .../Stops/ConsolidatedCirculationCard.tsx | 180 +++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx (limited to 'src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx') diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx new file mode 100644 index 0000000..8c3e922 --- /dev/null +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx @@ -0,0 +1,180 @@ +import { Clock } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import LineIcon from "~components/LineIcon"; +import { type RegionConfig } from "~data/RegionConfig"; +import { type ConsolidatedCirculation } from "~routes/stops-$id"; + +import "./ConsolidatedCirculationList.css"; + +interface ConsolidatedCirculationCardProps { + estimate: ConsolidatedCirculation; + 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 ConsolidatedCirculationCard: React.FC< + ConsolidatedCirculationCardProps +> = ({ estimate, 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 "OK"; + } else if (delay > 2) { + return "R" + delay; + } else { + return "A" + 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 displayMinutes = + estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; + const timeClass = getTimeClass(estimate); + const delayText = getDelayText(estimate); + + return ( +
+
+
+ +
+ +
+ {estimate.route} +
+ +
+
+ + {estimate.realTime + ? `${displayMinutes} ${t("estimates.minutes", "min")}` + : absoluteArrivalTime(displayMinutes)} +
+
+ {estimate.schedule && ( + <> + {parseServiceId(estimate.schedule.serviceId)} ( + {getTripIdDisplay(estimate.schedule.tripId)}) + + )} + + {estimate.schedule && + estimate.realTime && + estimate.realTime.distance >= 0 && <> · } + + {estimate.realTime && estimate.realTime.distance >= 0 && ( + <>{formatDistance(estimate.realTime.distance)} + )} + + {estimate.schedule && + estimate.realTime && + estimate.realTime.distance >= 0 && <> · } + + {delayText} +
+
+
+
+ ); +}; -- cgit v1.3