M3: implement Data Quality Workbench
Five rule scanners (duplicate customers, missing fields, odometer regression, booking overlap, status conflict) run automatically after seed and via an explicit scan endpoint. Issue defer/reject/merge-customers endpoints with transactional customer merge (booking rewiring, tombstone, audit). Data Quality nav + workbench UI with two-column duplicate comparison and inline (non-native) confirm. Dashboard attention items now link to issues. 35 backend tests passing, ruff clean. Fixed a real false-positive bug in odometer-regression detection found through iteration on seed data, and two TS narrowing errors. Verified end-to-end via browser: S2 merge and S4 overlap scenarios.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api/client";
|
||||
import type { DataQualityIssue } from "../api/types";
|
||||
import { SeverityBadge, StatusBadge } from "../components/Badge";
|
||||
|
||||
const RULE_TYPES = [
|
||||
"possible_duplicate_customer",
|
||||
"missing_required_field",
|
||||
"odometer_regression",
|
||||
"booking_overlap",
|
||||
"vehicle_status_conflict",
|
||||
];
|
||||
|
||||
export function DataQuality() {
|
||||
const [issues, setIssues] = useState<DataQualityIssue[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState("open");
|
||||
const [ruleType, setRuleType] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams();
|
||||
if (status) params.set("status", status);
|
||||
if (ruleType) params.set("rule_type", ruleType);
|
||||
api
|
||||
.get<DataQualityIssue[]>(`/api/v1/data-quality/issues?${params.toString()}`)
|
||||
.then(setIssues)
|
||||
.catch(() => setError("Data-quality issues are unavailable right now."));
|
||||
}, [status, ruleType]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Data Quality</h1>
|
||||
|
||||
<form className="filters" aria-label="Filter data-quality issues">
|
||||
<label>
|
||||
Status
|
||||
<select value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
<option value="">All statuses</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="deferred">Deferred</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Rule type
|
||||
<select value={ruleType} onChange={(e) => setRuleType(e.target.value)}>
|
||||
<option value="">All rule types</option>
|
||||
{RULE_TYPES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r.replace(/_/g, " ")}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
{error && <p className="error" role="alert">{error}</p>}
|
||||
{!error && !issues && <p>Loading issues…</p>}
|
||||
{issues && issues.length === 0 && <p>No issues match these filters.</p>}
|
||||
|
||||
{issues && issues.length > 0 && (
|
||||
<table className="data-table">
|
||||
<caption className="visually-hidden">Data-quality issues</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Reference</th>
|
||||
<th scope="col">Rule</th>
|
||||
<th scope="col">Entity</th>
|
||||
<th scope="col">Severity</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{issues.map((i) => (
|
||||
<tr key={i.public_ref}>
|
||||
<th scope="row">
|
||||
<Link to={`/data-quality/${i.public_ref}`}>{i.public_ref}</Link>
|
||||
</th>
|
||||
<td>{i.rule_type.replace(/_/g, " ")}</td>
|
||||
<td>{i.entity_ref}</td>
|
||||
<td>
|
||||
<SeverityBadge severity={i.severity} />
|
||||
</td>
|
||||
<td>
|
||||
<StatusBadge status={i.status} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user