feat(FN-3786): add reports dashboard view and comparison tooling
Adds a Reports dashboard view (`ReportsView`) to the reports plugin with a full supporting stack of components (`ReportComparisonDrawer`, `ReportDetailPanel`, `ReportFiltersBar`, `ReportListItem`, `ReportEmptyState`), hooks (`useReports`, `useReportPreview`, `useReportSectionDiff`, `useViewportMode` Fusion-Task-Id: FN-3786
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { useMemo } from "react";
|
||||
import type { ReportRecord } from "../types.js";
|
||||
import { useReportPreview } from "../useReportPreview.js";
|
||||
import { useReportSectionDiff } from "../useReportSectionDiff.js";
|
||||
|
||||
export function ReportComparisonDrawer({ reports, leftId, rightId, onPick, onClose, projectId }: { reports: ReportRecord[]; leftId?: string; rightId?: string; onPick: (slot: "a" | "b", id: string) => void; onClose: () => void; projectId?: string }) {
|
||||
const left = useMemo(() => reports.find((r) => r.id === leftId), [reports, leftId]);
|
||||
const right = useMemo(() => reports.find((r) => r.id === rightId), [reports, rightId]);
|
||||
const leftPreview = useReportPreview(leftId, projectId);
|
||||
const rightPreview = useReportPreview(rightId, projectId);
|
||||
const diff = useReportSectionDiff(left, right);
|
||||
return <div className="modal-overlay open reports-compare-overlay" role="dialog" aria-modal="true" aria-label="Compare reports">
|
||||
<div className="modal modal-lg reports-compare">
|
||||
<div className="modal-header reports-compare-header"><h3>Compare reports</h3><button className="btn btn-sm" onClick={onClose}>Close</button></div>
|
||||
<div className="reports-compare-pickers"><select className="select" value={leftId ?? ""} onChange={(e) => onPick("a", e.target.value)}>{reports.map((r) => <option key={r.id} value={r.id}>{r.title}</option>)}</select>
|
||||
<select className="select" value={rightId ?? ""} onChange={(e) => onPick("b", e.target.value)}>{reports.map((r) => <option key={r.id} value={r.id}>{r.title}</option>)}</select></div>
|
||||
<div className="reports-compare-frames"><iframe sandbox="allow-same-origin" srcDoc={leftPreview.html} title="Report A" /><iframe sandbox="allow-same-origin" srcDoc={rightPreview.html} title="Report B" /></div>
|
||||
<div>Changed: {diff.changed.map((s) => s.id).join(", ")}</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
import { getReportExportUrl } from "../api.js";
|
||||
import { useReportPreview } from "../useReportPreview.js";
|
||||
import type { ReportRecord } from "../types.js";
|
||||
|
||||
const SECTION_IDS = ["summary", "system-wins", "system-highlights", "system-lowlights", "system-proposals", "system-deep-dives", "agent-card", "data-coverage", "review-panel"];
|
||||
|
||||
export function ReportDetailPanel({ report, projectId }: { report?: ReportRecord; projectId?: string }) {
|
||||
const frameRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const { html, loading, error } = useReportPreview(report?.id, projectId);
|
||||
const sections = useMemo(() => SECTION_IDS, []);
|
||||
if (!report) return <div className="reports-detail card">Select a report.</div>;
|
||||
return <div className="reports-detail card">
|
||||
<div className="reports-detail-header"><h3>{report.title}</h3><a className="btn btn-sm" href={getReportExportUrl(report.id, projectId)} download>Download standalone HTML</a></div>
|
||||
<div className="reports-detail-meta">{report.cadence} • {report.status} • {report.periodStart} → {report.periodEnd}</div>
|
||||
<div className="reports-detail-body">
|
||||
<nav className="reports-detail-sections">{sections.map((section) => <button key={section} className="btn btn-sm" onClick={() => frameRef.current?.contentWindow?.document.querySelector(`[data-section="${section}"]`)?.scrollIntoView()}>{section}</button>)}</nav>
|
||||
{loading ? <div>Loading preview...</div> : null}
|
||||
{error ? <div>{error}</div> : null}
|
||||
<iframe ref={frameRef} sandbox="allow-same-origin" srcDoc={html} title="Report preview" />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function ReportEmptyState() {
|
||||
return <div className="reports-empty card"><h3>No reports found</h3><p>Adjust filters or enable schedules in settings.</p></div>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ReportFilters } from "../types.js";
|
||||
|
||||
export function ReportFiltersBar({ filters, onChange, agents }: { filters: ReportFilters; onChange: (next: ReportFilters) => void; agents: string[] }) {
|
||||
const [query, setQuery] = useState(filters.q);
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onChange({ ...filters, q: query }), 250);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [query]);
|
||||
|
||||
return <div className="reports-filters">
|
||||
<select className="select" value={filters.cadence} onChange={(e) => onChange({ ...filters, cadence: e.target.value as ReportFilters["cadence"] })}><option value="all">All cadence</option><option value="daily">Daily</option><option value="weekly">Weekly</option></select>
|
||||
<select className="select" value={filters.status} onChange={(e) => onChange({ ...filters, status: e.target.value as ReportFilters["status"] })}><option value="all">All status</option><option value="generating">Generating</option><option value="review_pending">Review pending</option><option value="review_in_progress">Review in progress</option><option value="review_complete">Review complete</option><option value="approved">Approved</option><option value="published">Published</option><option value="failed">Failed</option></select>
|
||||
<input className="input" type="date" value={filters.from} onChange={(e) => onChange({ ...filters, from: e.target.value })} />
|
||||
<input className="input" type="date" value={filters.to} onChange={(e) => onChange({ ...filters, to: e.target.value })} />
|
||||
<input className="input" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search title" />
|
||||
<select className="select" value={filters.agentId} onChange={(e) => onChange({ ...filters, agentId: e.target.value })}><option value="">All agents</option>{agents.map((agent) => <option key={agent} value={agent}>{agent}</option>)}</select>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ReportListItemVm } from "../types.js";
|
||||
|
||||
export function ReportListItem({ report, selected, onSelect }: { report: ReportListItemVm; selected: boolean; onSelect: (id: string) => void }) {
|
||||
return <button className="card reports-list-item" data-selected={selected ? "true" : "false"} onClick={() => onSelect(report.id)}>
|
||||
<div className="card-header"><span className="card-title">{report.title}</span></div>
|
||||
<div className="card-meta"><span>{report.cadence}</span><span>{report.status}</span><span>{report.periodStart} → {report.periodEnd}</span></div>
|
||||
</button>;
|
||||
}
|
||||
Reference in New Issue
Block a user