feat(ui): redesign return and data-quality experiences

This commit is contained in:
NuklearRabbit
2026-08-02 03:27:17 +02:00
parent 3d78cac850
commit ce98a5f1c9
3 changed files with 61 additions and 40 deletions
+14 -11
View File
@@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
import { api } from "../api/client";
import type { DataQualityIssue } from "../api/types";
import { SeverityBadge, StatusBadge } from "../components/Badge";
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
const RULE_TYPES = [
"possible_duplicate_customer",
@@ -19,6 +20,8 @@ export function DataQuality() {
const [ruleType, setRuleType] = useState("");
useEffect(() => {
setIssues(null);
setError(null);
const params = new URLSearchParams();
if (status) params.set("status", status);
if (ruleType) params.set("rule_type", ruleType);
@@ -30,7 +33,7 @@ export function DataQuality() {
return (
<div className="page">
<h1>Data Quality</h1>
<PageHeader eyebrow="Assurance / Workbench" title="Data quality" description="Resolve evidence-backed exceptions before they disrupt operations." />
<form className="filters" aria-label="Filter data-quality issues">
<label>
@@ -56,12 +59,12 @@ export function DataQuality() {
</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>}
{error && <ErrorState message={error} />}
{!error && !issues && <LoadingState label="Loading quality workbench…" />}
{issues && issues.length === 0 && <EmptyState icon="check" title="Queue is clear" detail="No issues match the current filters." />}
{issues && issues.length > 0 && (
<table className="data-table">
<div className="table-shell"><div className="table-meta"><span>{issues.length} issues</span><span>Evidence-backed detection</span></div><table className="data-table">
<caption className="visually-hidden">Data-quality issues</caption>
<thead>
<tr>
@@ -75,21 +78,21 @@ export function DataQuality() {
<tbody>
{issues.map((i) => (
<tr key={i.public_ref}>
<th scope="row">
<th scope="row" data-label="Reference">
<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>
<td data-label="Rule">{i.rule_type.replace(/_/g, " ")}</td>
<td data-label="Entity">{i.entity_ref}</td>
<td data-label="Severity">
<SeverityBadge severity={i.severity} />
</td>
<td>
<td data-label="Status">
<StatusBadge status={i.status} />
</td>
</tr>
))}
</tbody>
</table>
</table></div>
)}
</div>
);
+13 -14
View File
@@ -4,6 +4,8 @@ import { api, ApiError } from "../api/client";
import type { DataQualityIssueDetail as IssueDetail, EntitySnapshot } from "../api/types";
import { SeverityBadge, StatusBadge } from "../components/Badge";
import { useAuth } from "../context/AuthContext";
import { Icon } from "../components/Icons";
import { ErrorState, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
const MERGE_FIELDS = ["first_name", "last_name", "email", "phone", "postal_code", "city"];
@@ -60,7 +62,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
return (
<section className="panel duplicate-compare" aria-labelledby="compare-heading">
<h2 id="compare-heading">Compare and merge</h2>
<SectionHeading title="Compare and merge" description="Choose the canonical customer and review each conflicting field." />
{error && <p className="error" role="alert">{error}</p>}
<fieldset>
@@ -100,8 +102,8 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
const differ = valueA !== valueB;
return (
<tr key={field}>
<th scope="row">{field.replace(/_/g, " ")}</th>
<td>
<th scope="row" data-label="Field">{field.replace(/_/g, " ")}{differ ? <span className="difference-mark">Differs</span> : <span className="match-mark">Match</span>}</th>
<td data-label={a.public_ref}>
{differ ? (
<label className="checkbox-label">
<input
@@ -116,7 +118,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
valueA
)}
</td>
<td>
<td data-label={b.public_ref}>
{differ ? (
<label className="checkbox-label">
<input
@@ -137,7 +139,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
</tbody>
</table>
<p>
<p className="merge-preview">
<strong>{loser.public_ref}</strong> will become a tombstone linked to{" "}
<strong>{survivor.public_ref}</strong>; its bookings will be rewired to the survivor.
</p>
@@ -195,21 +197,18 @@ export function DataQualityIssueDetail() {
}
}
if (error) return <p className="error" role="alert">{error}</p>;
if (!issue) return <p>Loading issue…</p>;
if (error) return <ErrorState message={error} />;
if (!issue) return <LoadingState label="Loading issue evidence…" />;
return (
<div className="page">
<p><Link to="/data-quality">← Back to data quality</Link></p>
<h1>{issue.public_ref}</h1>
<p>
<SeverityBadge severity={issue.severity} /> <StatusBadge status={issue.status} />
</p>
<dl className="detail-grid">
<Link className="back-link" to="/data-quality"><Icon name="arrow-left" /> Quality workbench</Link>
<PageHeader eyebrow={`Quality / ${issue.rule_type.replace(/_/g, " ")}`} title={issue.public_ref} description="Review persisted evidence and record an audited resolution." actions={<div className="status-stack"><SeverityBadge severity={issue.severity} /><StatusBadge status={issue.status} /></div>} />
<section className="record-surface" aria-label="Issue summary"><dl className="detail-grid">
<div><dt>Rule</dt><dd>{issue.rule_type.replace(/_/g, " ")}</dd></div>
<div><dt>Entity</dt><dd>{issue.entity_ref}</dd></div>
<div><dt>Evidence</dt><dd>{String(issue.evidence.summary ?? "")}</dd></div>
</dl>
</dl></section>
{actionError && <p className="error" role="alert">{actionError}</p>}