feat(matching-runs): attach origin_run_id to new suggestions and expose pairs-per-run API

- Extend saveMatchingResults(eventId, suggestions, runId) and set originRunId
- Scheduler/Admin run-now: always pass runId
- Admin API: GET /api/admin/events/:slug/matching-runs/:runId/suggestions
- Prisma: add compound index on (origin_run_id, status)
- Frontend: add getRunSuggestions, expand row in MatchingRunsSection with 'Pairs created in this run' wording
This commit is contained in:
Radosław Gierwiało
2025-11-30 13:37:32 +01:00
parent 7e2a196f99
commit a9ad25eb38
7 changed files with 188 additions and 24 deletions

View File

@@ -1,12 +1,15 @@
import { useEffect, useState } from 'react';
import { adminAPI } from '../../services/api';
import { RefreshCcw, Play } from 'lucide-react';
import { RefreshCcw, Play, ChevronDown, ChevronRight } from 'lucide-react';
export default function MatchingRunsSection({ slug }) {
const [runs, setRuns] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [running, setRunning] = useState(false);
const [expandedRunId, setExpandedRunId] = useState(null);
const [pairsByRun, setPairsByRun] = useState({});
const [pairsLoading, setPairsLoading] = useState(false);
const loadRuns = async () => {
try {
@@ -43,6 +46,26 @@ export default function MatchingRunsSection({ slug }) {
const formatDateTime = (dt) => dt ? new Date(dt).toLocaleString() : '-';
const toggleViewPairs = async (runId) => {
if (expandedRunId === runId) {
setExpandedRunId(null);
return;
}
setExpandedRunId(runId);
if (!pairsByRun[runId]) {
try {
setPairsLoading(true);
const res = await adminAPI.getRunSuggestions(slug, runId, { onlyAssigned: true, includeNotFound: false, limit: 100 });
setPairsByRun((prev) => ({ ...prev, [runId]: res.suggestions || [] }));
} catch (e) {
console.error('Failed to load run pairs', e);
setError(e.message || 'Failed to load run pairs');
} finally {
setPairsLoading(false);
}
}
};
return (
<section className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between mb-3">
@@ -73,6 +96,7 @@ export default function MatchingRunsSection({ slug }) {
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2"></th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Started</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>
@@ -92,22 +116,59 @@ export default function MatchingRunsSection({ slug }) {
</tr>
) : (
runs.map((run) => (
<tr key={run.id}>
<td className="px-3 py-2 text-sm text-gray-900">{formatDateTime(run.startedAt)}</td>
<td className="px-3 py-2 text-sm text-gray-700">{formatDateTime(run.endedAt)}</td>
<td className="px-3 py-2 text-sm text-gray-700 capitalize">{run.trigger}</td>
<td className="px-3 py-2 text-sm">
<span className={`px-2 py-1 rounded text-xs font-medium
${run.status === 'success' ? 'bg-green-50 text-green-700' : ''}
${run.status === 'error' ? 'bg-red-50 text-red-700' : ''}
${run.status === 'running' ? 'bg-blue-50 text-blue-700' : ''}
`}>
{run.status}
</span>
</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.notFoundCount}</td>
</tr>
<>
<tr key={run.id}>
<td className="px-3 py-2 text-sm text-gray-700">
<button
onClick={() => toggleViewPairs(run.id)}
className="inline-flex items-center text-primary-600 hover:text-primary-700"
title="View pairs created in this run"
>
{expandedRunId === run.id ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
</td>
<td className="px-3 py-2 text-sm text-gray-900">{formatDateTime(run.startedAt)}</td>
<td className="px-3 py-2 text-sm text-gray-700">{formatDateTime(run.endedAt)}</td>
<td className="px-3 py-2 text-sm text-gray-700 capitalize">{run.trigger}</td>
<td className="px-3 py-2 text-sm">
<span className={`px-2 py-1 rounded text-xs font-medium
${run.status === 'success' ? 'bg-green-50 text-green-700' : ''}
${run.status === 'error' ? 'bg-red-50 text-red-700' : ''}
${run.status === 'running' ? 'bg-blue-50 text-blue-700' : ''}
`}>
{run.status}
</span>
</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.notFoundCount}</td>
</tr>
{expandedRunId === run.id && (
<tr>
<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>
{pairsLoading && (!pairsByRun[run.id] || pairsByRun[run.id].length === 0) ? (
<div className="text-gray-500">Loading pairs...</div>
) : (pairsByRun[run.id] && pairsByRun[run.id].length > 0 ? (
<ul className="space-y-1">
{pairsByRun[run.id].map((p) => (
<li key={p.id} className="flex justify-between items-center p-2 bg-white rounded border border-gray-200">
<div className="flex items-center gap-2 text-gray-900">
<span className="text-xs text-gray-500">{p.heat.label}</span>
<span className="font-semibold">{p.dancer?.username}</span>
<span className="text-gray-500"></span>
<span className="font-semibold">{p.recorder?.username}</span>
</div>
<span className="text-xs px-2 py-0.5 rounded bg-green-50 text-green-700">{p.status}</span>
</li>
))}
</ul>
) : (
<div className="text-gray-500">No pairs created in this run</div>
))}
</td>
</tr>
)}
</>
))
)}
</tbody>
@@ -122,4 +183,3 @@ export default function MatchingRunsSection({ slug }) {
</section>
);
}

View File

@@ -455,6 +455,16 @@ export const adminAPI = {
const data = await fetchAPI(`/admin/events/${slug}/matching-runs?${params.toString()}`);
return data;
},
async getRunSuggestions(slug, runId, { onlyAssigned = true, includeNotFound = false, limit = 100 } = {}) {
const params = new URLSearchParams({
onlyAssigned: String(onlyAssigned),
includeNotFound: String(includeNotFound),
limit: String(limit),
});
const data = await fetchAPI(`/admin/events/${slug}/matching-runs/${runId}/suggestions?${params.toString()}`);
return data;
},
};
export { ApiError };