M39: harden application and acceptance gates
MobilityOps acceptance / backend (push) Failing after 45s
MobilityOps acceptance / frontend (push) Successful in 32s
MobilityOps acceptance / e2e (push) Skipped

This commit is contained in:
NuklearRabbit
2026-08-17 03:17:44 +02:00
parent a9f48d6880
commit ae39a8947f
62 changed files with 5277 additions and 202 deletions
+16 -16
View File
@@ -3,6 +3,7 @@ import { Link, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { api } from "../api/client";
import type { Booking, Page } from "../api/types";
import { brusselsDayStartIso, brusselsNextDayStartIso, toBrusselsDateTimeLocal } from "../i18n/brusselsDateTime";
import { useLocaleFormat } from "../i18n/format";
import { StatusBadge } from "../components/Badge";
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
@@ -10,15 +11,9 @@ import { Pagination } from "../components/Pagination";
const STATUS_OPTIONS = ["reserved", "active", "returned", "cancelled", "blocked"];
function localDateString(date = new Date()): string {
const offset = date.getTimezoneOffset() * 60_000;
return new Date(date.getTime() - offset).toISOString().slice(0, 10);
}
function nextLocalDay(value: string): string {
const date = new Date(`${value}T12:00:00`);
date.setDate(date.getDate() + 1);
return localDateString(date);
/** Today's Brussels calendar day (YYYY-MM-DD) -- the operational "today", not the browser's. */
function brusselsToday(): string {
return toBrusselsDateTimeLocal(new Date()).slice(0, 10);
}
export function Bookings() {
@@ -41,7 +36,7 @@ export function Bookings() {
if (value === null || value === "") next.delete(key);
else next.set(key, String(value));
});
setSearchParams(next);
setSearchParams(next, { replace: true });
}
useEffect(() => {
@@ -50,14 +45,19 @@ export function Bookings() {
const params = new URLSearchParams({ page: String(page), page_size: "25" });
if (status) params.set("status", status);
if (query) params.set("query", query);
if (startsFrom) params.set("starts_from", new Date(`${startsFrom}T00:00:00`).toISOString());
if (startsTo) params.set("starts_to", new Date(`${nextLocalDay(startsTo)}T00:00:00`).toISOString());
// Date filters are Brussels calendar days regardless of the browser's own time zone.
if (startsFrom) params.set("starts_from", brusselsDayStartIso(startsFrom));
if (startsTo) params.set("starts_to", brusselsNextDayStartIso(startsTo));
if (location) params.set("location", location);
params.set("sort", sort);
const controller = new AbortController();
api
.get<Page<Booking>>(`/api/v1/bookings?${params.toString()}`)
.get<Page<Booking>>(`/api/v1/bookings?${params.toString()}`, { signal: controller.signal })
.then(setBookings)
.catch(() => setError(t("list.unavailable")));
.catch(() => {
if (!controller.signal.aborted) setError(t("list.unavailable"));
});
return () => controller.abort();
}, [page, query, status, startsFrom, startsTo, location, sort, t]);
return (
@@ -82,8 +82,8 @@ export function Bookings() {
<label>{t("list.sortLabel")}<select value={sort} onChange={(event) => updateFilters({ sort: event.target.value, page: 1 })}><option value="operational">{t("list.sortOperational")}</option><option value="starts_asc">{t("list.sortAscending")}</option><option value="starts_desc">{t("list.sortDescending")}</option></select></label>
</form>
<div className="filter-presets" aria-label={t("list.presetsLabel")}>
<button type="button" onClick={() => updateFilters({ from: localDateString(), to: localDateString(), status: null, page: 1 })}>{t("list.todayPreset")}</button>
<button type="button" onClick={() => updateFilters({ from: localDateString(), to: null, status: "reserved", sort: "starts_asc", page: 1 })}>{t("list.upcomingPreset")}</button>
<button type="button" onClick={() => updateFilters({ from: brusselsToday(), to: brusselsToday(), status: null, page: 1 })}>{t("list.todayPreset")}</button>
<button type="button" onClick={() => updateFilters({ from: brusselsToday(), to: null, status: "reserved", sort: "starts_asc", page: 1 })}>{t("list.upcomingPreset")}</button>
<button type="button" onClick={() => setSearchParams(new URLSearchParams())}>{t("list.clearFilters")}</button>
</div>