feat(matching-runs): add per-run aggregate stats and UI display
- Admin list endpoint returns totalSuggestions, assignedCount, aggregatedNotFoundCount per run - UI: show Total/Matched/Not found columns using fresh aggregates - Add anchor link Run #ID and wording 'Pairs created in this run'
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
-- Add origin_run_id column to recording_suggestions
|
||||||
|
ALTER TABLE "recording_suggestions" ADD COLUMN "origin_run_id" INTEGER;
|
||||||
|
|
||||||
|
-- Add foreign key to matching_runs
|
||||||
|
ALTER TABLE "recording_suggestions"
|
||||||
|
ADD CONSTRAINT "recording_suggestions_origin_run_id_fkey"
|
||||||
|
FOREIGN KEY ("origin_run_id") REFERENCES "matching_runs"("id")
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Simple index to support joins by origin_run_id
|
||||||
|
CREATE INDEX IF NOT EXISTS "recording_suggestions_origin_run_id_idx"
|
||||||
|
ON "recording_suggestions"("origin_run_id");
|
||||||
|
|
||||||
@@ -104,6 +104,30 @@ router.get('/events/:slug/matching-runs', authenticate, async (req, res, next) =
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Aggregate fresh stats per run from recording_suggestions (origin_run_id)
|
||||||
|
// Cheap and valuable: shows actual created pairs in this run.
|
||||||
|
if (runs.length > 0) {
|
||||||
|
const runIds = runs.map(r => r.id);
|
||||||
|
// Single SQL query for all listed runs
|
||||||
|
const placeholders = runIds.join(',');
|
||||||
|
const aggRows = await prisma.$queryRawUnsafe(
|
||||||
|
`SELECT origin_run_id AS "originRunId",
|
||||||
|
COUNT(*)::int AS "totalSuggestions",
|
||||||
|
COUNT(*) FILTER (WHERE recorder_id IS NOT NULL)::int AS "assignedCount",
|
||||||
|
COUNT(*) FILTER (WHERE status = 'not_found')::int AS "notFoundCount"
|
||||||
|
FROM recording_suggestions
|
||||||
|
WHERE event_id = ${event.id} AND origin_run_id IN (${placeholders})
|
||||||
|
GROUP BY origin_run_id`
|
||||||
|
);
|
||||||
|
const aggByRun = new Map(aggRows.map(r => [r.originRunId, r]));
|
||||||
|
for (const r of runs) {
|
||||||
|
const agg = aggByRun.get(r.id) || { totalSuggestions: 0, assignedCount: 0, notFoundCount: 0 };
|
||||||
|
r.totalSuggestions = agg.totalSuggestions;
|
||||||
|
r.assignedCount = agg.assignedCount;
|
||||||
|
r.aggregatedNotFoundCount = agg.notFoundCount; // keep original notFoundCount for backward compat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ success: true, count: runs.length, data: runs });
|
res.json({ success: true, count: runs.length, data: runs });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export default function MatchingRunsSection({ slug }) {
|
|||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ended</th>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ended</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Trigger</th>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Trigger</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||||
|
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th>
|
||||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Matched</th>
|
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Matched</th>
|
||||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Not found</th>
|
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Not found</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -117,7 +118,7 @@ export default function MatchingRunsSection({ slug }) {
|
|||||||
) : (
|
) : (
|
||||||
runs.map((run) => (
|
runs.map((run) => (
|
||||||
<>
|
<>
|
||||||
<tr key={run.id}>
|
<tr key={run.id} id={`run-${run.id}`}>
|
||||||
<td className="px-3 py-2 text-sm text-gray-700">
|
<td className="px-3 py-2 text-sm text-gray-700">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleViewPairs(run.id)}
|
onClick={() => toggleViewPairs(run.id)}
|
||||||
@@ -139,13 +140,17 @@ export default function MatchingRunsSection({ slug }) {
|
|||||||
{run.status}
|
{run.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-sm text-right text-gray-700">{run.matchedCount}</td>
|
<td className="px-3 py-2 text-sm text-right text-gray-700">{run.totalSuggestions ?? '-'}</td>
|
||||||
<td className="px-3 py-2 text-sm text-right text-gray-700">{run.notFoundCount}</td>
|
<td className="px-3 py-2 text-sm text-right text-gray-700">{run.assignedCount ?? run.matchedCount}</td>
|
||||||
|
<td className="px-3 py-2 text-sm text-right text-gray-700">{run.aggregatedNotFoundCount ?? run.notFoundCount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expandedRunId === run.id && (
|
{expandedRunId === run.id && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="7" className="px-3 py-2 bg-gray-50">
|
<td colSpan="7" className="px-3 py-2 bg-gray-50">
|
||||||
<div className="text-sm text-gray-800 font-medium mb-2">Pairs created in this run</div>
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="text-sm text-gray-800 font-medium">Pairs created in this run</div>
|
||||||
|
<a href={`#run-${run.id}`} className="text-xs text-primary-600 hover:underline">Run #{run.id}</a>
|
||||||
|
</div>
|
||||||
{pairsLoading && (!pairsByRun[run.id] || pairsByRun[run.id].length === 0) ? (
|
{pairsLoading && (!pairsByRun[run.id] || pairsByRun[run.id].length === 0) ? (
|
||||||
<div className="text-gray-500">Loading pairs...</div>
|
<div className="text-gray-500">Loading pairs...</div>
|
||||||
) : (pairsByRun[run.id] && pairsByRun[run.id].length > 0 ? (
|
) : (pairsByRun[run.id] && pairsByRun[run.id].length > 0 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user