Files
MobilityOps/frontend/src/pages/VehicleDetail.tsx
T
NuklearRabbitandClaude Sonnet 5 c2b8268927 fix: form field alignment, raw maintenance text, and static movements list
- .form-grid labels (missing-field form, odometer-regression correction fields)
  and the odometer/overlap note textareas had no stacked label-above-input
  styling at all -- the shared rule only covered .filters/.return-form, so these
  fell back to default inline browser layout with mismatched input widths.
  Extended the existing rule to cover .form-grid and label:has(> textarea).

- Vehicle maintenance list showed the raw, untranslated seed text
  ("Synthetic scheduled service record") regardless of locale -- purely
  decorative and 1:1 redundant with the (already-translated) category. Replaced
  it with the record's real odometer reading, mirroring the sibling
  Inspections tab's pattern.

- "Today's movements" was always the same fixed 4 bookings (2 returns, 2
  departures) on every reset, reading as a static mockup rather than live
  fleet activity. Added 8 more bookings anchored to land on "today" across 8
  additional vehicles, spread through the day.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 18:29:56 +02:00

126 lines
5.9 KiB
TypeScript

import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { api } from "../api/client";
import type { VehicleDetail as VehicleDetailData } from "../api/types";
import { useLocaleFormat } from "../i18n/format";
import { SeverityBadge, StatusBadge } from "../components/Badge";
import { Icon } from "../components/Icons";
import { ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
const TABS = ["overview", "bookings", "inspections", "maintenance", "quality"] as const;
type Tab = (typeof TABS)[number];
export function VehicleDetail() {
const { t } = useTranslation(["fleet", "bookings", "quality"]);
const { formatNumber, formatShortDate } = useLocaleFormat();
const { publicRef } = useParams<{ publicRef: string }>();
const [vehicle, setVehicle] = useState<VehicleDetailData | null>(null);
const [error, setError] = useState<string | null>(null);
const [tab, setTab] = useState<Tab>("overview");
useEffect(() => {
if (!publicRef) return;
setVehicle(null);
setError(null);
api
.get<VehicleDetailData>(`/api/v1/vehicles/${publicRef}`)
.then(setVehicle)
.catch(() => setError(t("detail.notFound")));
}, [publicRef]);
if (error) return <ErrorState message={error} />;
if (!vehicle) return <LoadingState label={t("detail.loading")} />;
return (
<div className="page">
<Link className="back-link" to="/vehicles"><Icon name="arrow-left" /> {t("detail.backLink")}</Link>
<PageHeader eyebrow={t("detail.eyebrow")} title={`${vehicle.public_ref} · ${vehicle.make} ${vehicle.model}`} description={`${vehicle.registration_number} · ${vehicle.location}`} actions={<div className="status-stack"><StatusBadge status={vehicle.operational_status} label={t(`statuses.${vehicle.operational_status}`, { defaultValue: vehicle.operational_status })} />{vehicle.attention && <span className="badge severity-high">{t("detail.needsAttention")}</span>}</div>} />
<div role="tablist" aria-label={t("detail.tabsAriaLabel")} className="tabs" aria-orientation="horizontal">
{TABS.map((tb) => (
<button
key={tb}
role="tab"
type="button"
aria-selected={tab === tb}
className={tab === tb ? "active" : ""}
onClick={() => setTab(tb)}
>
{t(`detail.tabs.${tb}`)}
</button>
))}
</div>
{tab === "overview" && (
<section className="record-surface" aria-label={t("detail.tabs.overview")}><dl className="detail-grid">
<div><dt>{t("detail.overview.registration")}</dt><dd>{vehicle.registration_number}</dd></div>
<div><dt>{t("detail.overview.modelYear")}</dt><dd>{vehicle.model_year}</dd></div>
<div><dt>{t("detail.overview.location")}</dt><dd>{vehicle.location}</dd></div>
<div><dt>{t("detail.overview.odometer")}</dt><dd>{formatNumber(vehicle.odometer_km)} km</dd></div>
<div><dt>{t("detail.overview.nextService")}</dt><dd>{formatNumber(vehicle.next_service_km)} km</dd></div>
<div><dt>{t("detail.overview.active")}</dt><dd>{vehicle.active ? t("detail.yes") : t("detail.no")}</dd></div>
</dl></section>
)}
{tab === "bookings" && (
<ul className="record-list">
{vehicle.bookings.length === 0 && <li>{t("detail.noBookings")}</li>}
{vehicle.bookings.map((b) => (
<li key={b.public_ref}>
<Link to={`/bookings/${b.public_ref}`}>{b.public_ref}</Link>
<StatusBadge status={b.status} label={t(`bookings:statuses.${b.status}`, { defaultValue: b.status })} />
<span>
{formatShortDate(b.starts_at)} {formatShortDate(b.ends_at)}
</span>
</li>
))}
</ul>
)}
{tab === "inspections" && (
<ul className="record-list">
{vehicle.inspections.length === 0 && <li>{t("detail.noInspections")}</li>}
{vehicle.inspections.map((i) => (
<li key={i.public_ref}>
<span>{t(`fleet:detail.inspectionTypes.${i.type}`, { defaultValue: i.type })}</span>
<span>{formatNumber(i.odometer_km)} km</span>
<span>{t("detail.fuel", { percent: i.fuel_level_percent })}</span>
{i.damage_reported && <span className="badge severity-high">{t("detail.damage")}</span>}
{i.technical_warning && <span className="badge severity-high">{t("detail.technicalWarning")}</span>}
<time dateTime={i.completed_at}>{formatShortDate(i.completed_at)}</time>
</li>
))}
</ul>
)}
{tab === "maintenance" && (
<ul className="record-list">
{vehicle.maintenance.length === 0 && <li>{t("detail.noMaintenance")}</li>}
{vehicle.maintenance.map((m) => (
<li key={m.public_ref}>
<span>{t(`fleet:detail.maintenanceCategories.${m.category}`, { defaultValue: m.category })}</span>
<span>{formatNumber(m.odometer_km)} km</span>
<time dateTime={m.occurred_at}>{formatShortDate(m.occurred_at)}</time>
</li>
))}
</ul>
)}
{tab === "quality" && (
<ul className="record-list">
{vehicle.quality_issues.length === 0 && <li>{t("detail.noQualityIssues")}</li>}
{vehicle.quality_issues.map((q) => (
<li key={q.public_ref}>
<Link to={`/data-quality/${q.public_ref}`}>{q.public_ref}</Link>
<SeverityBadge severity={q.severity} />
<span>{t(`quality:ruleTypes.${q.rule_type}`, { defaultValue: q.rule_type.replace(/_/g, " ") })}</span>
<StatusBadge status={q.status} label={t(`quality:list.status${q.status.charAt(0).toUpperCase()}${q.status.slice(1)}`, { defaultValue: q.status })} />
</li>
))}
</ul>
)}
</div>
);
}