97 lines
4.5 KiB
TypeScript
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) })));
|
|
}
|