134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import type { Journey } from "@/types";
|
|
import { parseHafasTime, hafasDateTime } from "@/lib/hafas-time";
|
|
|
|
interface HafasJourney {
|
|
ctxRecon?: string;
|
|
secL?: Array<{
|
|
dep?: { dTimeS?: string; dTimeR?: string; dPlatfS?: string };
|
|
arr?: { aTimeS?: string; aTimeR?: string };
|
|
jny?: {
|
|
prodX?: number;
|
|
stopL?: Array<{ name: string }>;
|
|
dlySum?: number;
|
|
isCncl?: boolean;
|
|
};
|
|
chgDurR?: number;
|
|
}>;
|
|
}
|
|
|
|
function parseHafasJourneys(json: unknown, hafasDate: string, queryDate: Date): Journey[] {
|
|
// HAFAS response shape is too complex for a clean TypeScript type; use any for parsing.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- HAFAS response shape is undocumented and deeply nested
|
|
const data = json as any;
|
|
const outConL: HafasJourney[] = data?.svcResL?.[0]?.res?.outConL ?? [];
|
|
|
|
return outConL.map((con, i): Journey => {
|
|
const first = con.secL?.[0];
|
|
const last = con.secL?.[con.secL.length - 1];
|
|
const dep = first?.dep;
|
|
const arr = last?.arr;
|
|
|
|
const sD = dep?.dTimeS ? parseHafasTime(hafasDate, dep.dTimeS) : queryDate;
|
|
const rD = dep?.dTimeR ? parseHafasTime(hafasDate, dep.dTimeR) : sD;
|
|
const sA = arr?.aTimeS ? parseHafasTime(hafasDate, arr.aTimeS) : sD;
|
|
const rA = arr?.aTimeR ? parseHafasTime(hafasDate, arr.aTimeR) : sA;
|
|
|
|
const delayMs = rD.getTime() - sD.getTime();
|
|
const delay = Math.max(0, Math.round(delayMs / 60000));
|
|
|
|
const trains = (con.secL ?? [])
|
|
.filter((s) => s.jny)
|
|
.map((s) => s.jny?.stopL?.[0]?.name ?? "")
|
|
.filter(Boolean);
|
|
|
|
const cancelled = (con.secL ?? []).some((s) => s.jny?.isCncl === true);
|
|
const changes = Math.max(0, (con.secL ?? []).filter((s) => s.jny).length - 1);
|
|
const platform = first?.dep?.dPlatfS ?? "";
|
|
|
|
return {
|
|
id: con.ctxRecon ?? `journey-${i}`,
|
|
sD,
|
|
rD,
|
|
sA,
|
|
rA,
|
|
delay,
|
|
platform,
|
|
changes,
|
|
trains,
|
|
cancelled,
|
|
};
|
|
});
|
|
}
|
|
|
|
export function useJourneys(fromStationExtId: string | null, toStationExtId: string | null, date: Date) {
|
|
const [journeys, setJourneys] = useState<Journey[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const fetchJourneys = async () => {
|
|
if (!fromStationExtId || !toStationExtId || !date) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const { date: hafasDate, time: hafasTime } = hafasDateTime(date);
|
|
|
|
const body = {
|
|
svcReqL: [
|
|
{
|
|
meth: "TripSearch",
|
|
req: {
|
|
depLocL: [{ type: "S", extId: fromStationExtId }],
|
|
arrLocL: [{ type: "S", extId: toStationExtId }],
|
|
outDate: hafasDate,
|
|
outTime: hafasTime,
|
|
numF: 5,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const response = await fetch("/api/hafas", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errBody = await response.json().catch(() => ({}));
|
|
throw new Error(errBody.error ?? `HAFAS request failed with status ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const result = parseHafasJourneys(data, hafasDate, date);
|
|
|
|
if (isMounted) {
|
|
setJourneys(result);
|
|
setLoading(false);
|
|
}
|
|
} catch (err: unknown) {
|
|
if (isMounted) {
|
|
const message = err instanceof Error ? err.message : "Failed to fetch journeys";
|
|
setError(message);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
fetchJourneys();
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [fromStationExtId, toStationExtId, date]);
|
|
|
|
return { journeys, loading, error };
|
|
}
|