feat(quality): add resolution UI for all five rule types and manual scan

DataQualityIssueDetail showed raw JSON as the primary interface for four of
five rule types, with no resolution surface beyond generic defer/reject.
Add a bounded panel per rule type (provide missing fields, retain/correct
an odometer reading, block one of two overlapping bookings, apply the
recommended vehicle status) wired to the new backend endpoints, and move
raw evidence behind a <details> disclosure. Add a "Run quality scan" action
to the workbench (confirmation, progress, per-rule result counts, auto
refresh) -- the endpoint already existed but had no UI trigger.
This commit is contained in:
NuklearRabbit
2026-08-02 06:16:14 +02:00
parent 6e227a214a
commit 477b5e7ce9
4 changed files with 490 additions and 17 deletions
+62 -5
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api } from "../api/client";
import type { DataQualityIssue } from "../api/types";
import { api, ApiError } from "../api/client";
import type { DataQualityIssue, ScanResult } from "../api/types";
import { useAuth } from "../context/AuthContext";
import { SeverityBadge, StatusBadge } from "../components/Badge";
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
@@ -20,8 +20,12 @@ export function DataQuality() {
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState("open");
const [ruleType, setRuleType] = useState("");
const [scanning, setScanning] = useState(false);
const [scanError, setScanError] = useState<string | null>(null);
const [scanResult, setScanResult] = useState<ScanResult | null>(null);
const [confirmingScan, setConfirmingScan] = useState(false);
useEffect(() => {
const load = useCallback(() => {
if (user?.role !== "operations_manager") return;
setIssues(null);
setError(null);
@@ -34,6 +38,25 @@ export function DataQuality() {
.catch(() => setError("Data-quality issues are unavailable right now."));
}, [status, ruleType, user]);
useEffect(() => {
load();
}, [load]);
async function handleScan() {
setScanError(null);
setScanning(true);
try {
const result = await api.post<ScanResult>("/api/v1/data-quality/scan");
setScanResult(result);
setConfirmingScan(false);
load();
} catch (err) {
setScanError(err instanceof ApiError ? err.message : "Could not run the quality scan.");
} finally {
setScanning(false);
}
}
if (user?.role !== "operations_manager") {
return (
<div className="page">
@@ -43,9 +66,43 @@ export function DataQuality() {
);
}
const scanTotal = scanResult ? Object.values(scanResult.created).reduce((a, b) => a + b, 0) : 0;
return (
<div className="page">
<PageHeader eyebrow="Assurance / Workbench" title="Data quality" description="Resolve evidence-backed exceptions before they disrupt operations." />
<PageHeader
eyebrow="Assurance / Workbench"
title="Data quality"
description="Resolve evidence-backed exceptions before they disrupt operations."
actions={
!confirmingScan ? (
<button className="button button-secondary" type="button" onClick={() => setConfirmingScan(true)} disabled={scanning}>
Run quality scan
</button>
) : (
<div className="confirm-bar" role="alertdialog" aria-label="Confirm quality scan">
<p>Run the deterministic scan across all five rule types now?</p>
<button type="button" onClick={handleScan} disabled={scanning}>
{scanning ? "Scanning…" : "Yes, run scan"}
</button>
<button type="button" onClick={() => setConfirmingScan(false)} disabled={scanning}>
Cancel
</button>
</div>
)
}
/>
{scanError && <p className="error" role="alert">{scanError}</p>}
{scanResult && (
<p className="quiet-empty" role="status">
Scan complete: {scanTotal === 0
? "no new issues found (existing open issues are not recreated)."
: Object.entries(scanResult.created)
.map(([rule, count]) => `${count} new ${rule.replace(/_/g, " ")}`)
.join(", ")}
</p>
)}
<form className="filters" aria-label="Filter data-quality issues">
<label>