Files
multi-seal-mail-webui/src/features/campaigns/ReviewDataPage.tsx

97 lines
4.5 KiB
TypeScript

import { Link } from "react-router-dom";
import type { ApiSettings } from "../../types";
import Button from "../../components/Button";
import PageTitle from "../../components/PageTitle";
import Card from "../../components/Card";
import StatusBadge from "../../components/StatusBadge";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { asArray, asRecord, formatDateTime, stringifyPreview, summaryValue } from "./utils/campaignView";
export default function ReviewDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true });
const version = data.currentVersion;
const issues = collectIssues(data.summary?.issues);
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>Review</PageTitle>
</div>
<div className="button-row compact-actions">
<Button onClick={reload} disabled={loading}>Reload</Button>
<Link to="../wizard/review"><Button variant="primary">Open Review Wizard</Button></Link>
</div>
</div>
{error && <div className="alert danger">{error}</div>}
<div className="dashboard-grid">
<Card title="Validation summary">
<div className="summary-grid">
<SummaryTile label="Errors" value={summaryValue(version?.validation_summary, ["error_count", "errors", "blocked"])} />
<SummaryTile label="Warnings" value={summaryValue(version?.validation_summary, ["warning_count", "warnings"])} />
<SummaryTile label="Info" value={summaryValue(version?.validation_summary, ["info_count", "info"])} />
<SummaryTile label="Validated" value={formatDateTime(version?.updated_at)} />
</div>
{!version?.validation_summary && <p className="muted">No validation summary is stored yet.</p>}
</Card>
<Card title="Build summary">
<div className="summary-grid">
<SummaryTile label="Built" value={summaryValue(version?.build_summary, ["built_count", "built", "messages_built"])} />
<SummaryTile label="Blocked" value={summaryValue(version?.build_summary, ["blocked_count", "blocked"])} />
<SummaryTile label="Needs review" value={summaryValue(version?.build_summary, ["needs_review_count", "needs_review"])} />
<SummaryTile label="Warnings" value={summaryValue(version?.build_summary, ["warning_count", "warnings"])} />
</div>
{!version?.build_summary && <p className="muted">No build summary is stored yet.</p>}
</Card>
</div>
<Card title="Review issues" actions={<span className="muted small-note">Grouped issue display will be expanded in the next review pass.</span>}>
{issues.length === 0 && <p className="muted">No stored issues were returned for this campaign summary.</p>}
{issues.length > 0 && (
<div className="app-table-wrap data-table-wrap">
<table className="app-table data-table">
<thead>
<tr>
<th>Severity</th>
<th>Section</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{issues.map((issue, index) => (
<tr key={index}>
<td><StatusBadge status={String(issue.severity || "info")} /></td>
<td>{String(issue.section || issue.field || "—")}</td>
<td>{String(issue.message || issue.code || stringifyPreview(issue, 180))}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Card>
</div>
);
}
function SummaryTile({ label, value }: { label: string; value: string | number }) {
return (
<div className="summary-tile">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function collectIssues(raw: unknown): Record<string, unknown>[] {
if (Array.isArray(raw)) return raw.map(asRecord);
if (!raw || typeof raw !== "object") return [];
const record = raw as Record<string, unknown>;
const direct = asArray(record.items ?? record.issues ?? record.results);
if (direct.length) return direct.map(asRecord);
return Object.entries(record).flatMap(([section, value]) => asArray(value).map((item) => ({ section, ...asRecord(item) })));
}