M36: deepen operational and mobile UX
This commit is contained in:
@@ -9,6 +9,7 @@ import { SeverityBadge, StatusBadge } from "../components/Badge";
|
||||
import { ApiErrorNotice, EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
import { Pagination } from "../components/Pagination";
|
||||
import { useLocaleFormat } from "../i18n/format";
|
||||
import { brusselsLocalToIso } from "../i18n/brusselsDateTime";
|
||||
|
||||
const RULE_TYPES = [
|
||||
"possible_duplicate_customer",
|
||||
@@ -42,6 +43,7 @@ export function DataQuality() {
|
||||
const [bulkDueAt, setBulkDueAt] = useState("");
|
||||
const [bulkSaving, setBulkSaving] = useState(false);
|
||||
const [bulkError, setBulkError] = useState<ApiErrorInfo | null>(null);
|
||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
|
||||
function updateFilters(updates: Record<string, string | boolean | number | null>) {
|
||||
const next = new URLSearchParams(searchParams);
|
||||
@@ -87,7 +89,7 @@ export function DataQuality() {
|
||||
await api.post("/api/v1/data-quality/issues/bulk-work", {
|
||||
issue_refs: [...selected],
|
||||
assigned_to_ref: bulkAssignee || undefined,
|
||||
due_at: bulkDueAt ? new Date(bulkDueAt).toISOString() : undefined,
|
||||
due_at: bulkDueAt ? brusselsLocalToIso(bulkDueAt) : undefined,
|
||||
});
|
||||
setSelected(new Set());
|
||||
setBulkAssignee("");
|
||||
@@ -108,6 +110,16 @@ export function DataQuality() {
|
||||
});
|
||||
}
|
||||
|
||||
function toggleVisibleSelection() {
|
||||
setSelected((current) => {
|
||||
const visibleRefs = visibleIssues.map((issue) => issue.public_ref);
|
||||
const allSelected = visibleRefs.every((ref) => current.has(ref));
|
||||
const next = new Set(current);
|
||||
visibleRefs.forEach((ref) => allSelected ? next.delete(ref) : next.add(ref));
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async function handleScan() {
|
||||
setScanError(null);
|
||||
setScanning(true);
|
||||
@@ -138,6 +150,8 @@ export function DataQuality() {
|
||||
? issues.items.filter((i) => i.public_ref.startsWith("DQ-DEMO-"))
|
||||
: issues.items
|
||||
: [];
|
||||
const activeFilterCount = [status !== "open", Boolean(ruleType), Boolean(severity), Boolean(assignee), overdueOnly, demoScenariosOnly].filter(Boolean).length;
|
||||
const allVisibleSelected = visibleIssues.length > 0 && visibleIssues.every((issue) => selected.has(issue.public_ref));
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
@@ -178,7 +192,10 @@ export function DataQuality() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<form className="filters" aria-label={t("list.title")}>
|
||||
<button className="button button-secondary filter-toggle" type="button" aria-expanded={filtersOpen} aria-controls="quality-filters" onClick={() => setFiltersOpen((open) => !open)}>
|
||||
{t("list.filtersToggle", { count: activeFilterCount })}
|
||||
</button>
|
||||
<form id="quality-filters" className={`filters quality-filters${filtersOpen ? " is-open" : ""}`} aria-label={t("list.title")}>
|
||||
<label>
|
||||
{t("list.statusLabel")}
|
||||
<select value={status} onChange={(e) => updateFilters({ status: e.target.value, page: 1 })}>
|
||||
@@ -230,6 +247,16 @@ export function DataQuality() {
|
||||
{t("list.demoScenariosOnly")}
|
||||
</label>
|
||||
</form>
|
||||
{activeFilterCount > 0 && (
|
||||
<div className="active-filter-chips" aria-label={t("list.activeFilters")}>
|
||||
{status !== "open" && <button type="button" onClick={() => updateFilters({ status: "open", page: 1 })}>{t(`list.status${status.charAt(0).toUpperCase()}${status.slice(1)}`)} ×</button>}
|
||||
{ruleType && <button type="button" onClick={() => updateFilters({ rule_type: null, page: 1 })}>{t(`ruleTypes.${ruleType}`)} ×</button>}
|
||||
{severity && <button type="button" onClick={() => updateFilters({ severity: null, page: 1 })}>{t(`severities.${severity}`)} ×</button>}
|
||||
{assignee && <button type="button" onClick={() => updateFilters({ assignee: null, page: 1 })}>{assignee === "unassigned" ? t("list.unassigned") : users.find((record) => record.public_ref === assignee)?.display_name ?? assignee} ×</button>}
|
||||
{overdueOnly && <button type="button" onClick={() => updateFilters({ overdue: null, page: 1 })}>{t("list.overdueOnly")} ×</button>}
|
||||
{demoScenariosOnly && <button type="button" onClick={() => { setDemoScenariosOnly(false); updateFilters({ demo: null, page: 1 }); }}>{t("list.demoScenariosOnly")} ×</button>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <ErrorState message={error} />}
|
||||
{!error && !issues && <LoadingState label={t("list.loading")} />}
|
||||
@@ -249,11 +276,11 @@ export function DataQuality() {
|
||||
<button type="button" className="button button-secondary" onClick={() => setSelected(new Set())}>{t("list.clearSelection")}</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="table-meta"><span>{t("list.count", { count: issues?.total ?? visibleIssues.length })}</span><span>{t("list.evidenceBacked")}</span></div><table className="data-table">
|
||||
<div className="table-meta"><span>{t("list.count", { count: issues?.total ?? visibleIssues.length })}</span><button className="select-visible" type="button" onClick={toggleVisibleSelection}>{t(allVisibleSelected ? "list.deselectVisible" : "list.selectVisible")}</button><span>{t("list.evidenceBacked")}</span></div><table className="data-table quality-table">
|
||||
<caption className="visually-hidden">{t("list.title")}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><span className="visually-hidden">{t("list.columns.select")}</span></th>
|
||||
<th scope="col" className="selection-cell"><input type="checkbox" checked={allVisibleSelected} onChange={toggleVisibleSelection} aria-label={t("list.selectVisible")} /></th>
|
||||
<th scope="col">{t("list.columns.reference")}</th>
|
||||
<th scope="col">{t("list.columns.rule")}</th>
|
||||
<th scope="col">{t("list.columns.entity")}</th>
|
||||
@@ -268,7 +295,7 @@ export function DataQuality() {
|
||||
<tr key={i.public_ref} className="row-clickable">
|
||||
<td className="selection-cell"><input type="checkbox" checked={selected.has(i.public_ref)} onChange={() => toggleSelected(i.public_ref)} aria-label={t("list.selectIssue", { ref: i.public_ref })} /></td>
|
||||
<th scope="row" data-label={t("list.columns.reference")}>
|
||||
{i.public_ref}
|
||||
<span aria-hidden="true">{i.public_ref}</span>
|
||||
<Link className="row-link" to={`/data-quality/${i.public_ref}`}><span className="visually-hidden">{i.public_ref}</span></Link>
|
||||
</th>
|
||||
<td data-label={t("list.columns.rule")}>{t(`ruleTypes.${i.rule_type}`, { defaultValue: i.rule_type.replace(/_/g, " ") })}</td>
|
||||
|
||||
Reference in New Issue
Block a user