From 7e2a196f992794d9dba41b3d031259113fb52df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Sun, 30 Nov 2025 13:20:33 +0100 Subject: [PATCH] feat(frontend): add Run now button and matching runs list on event details page - New adminAPI for run-now and runs listing - MatchingRunsSection with refresh and run controls - Integrate into EventDetailsPage under matching configuration --- .../migration.sql | 21 +++ .../components/events/MatchingRunsSection.jsx | 125 ++++++++++++++++++ frontend/src/pages/EventDetailsPage.jsx | 6 + frontend/src/services/api.js | 16 +++ 4 files changed, 168 insertions(+) create mode 100644 backend/prisma/migrations/20251130120000_add_matching_runs_audit/migration.sql create mode 100644 frontend/src/components/events/MatchingRunsSection.jsx diff --git a/backend/prisma/migrations/20251130120000_add_matching_runs_audit/migration.sql b/backend/prisma/migrations/20251130120000_add_matching_runs_audit/migration.sql new file mode 100644 index 0000000..929cd1a --- /dev/null +++ b/backend/prisma/migrations/20251130120000_add_matching_runs_audit/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "matching_runs" ( + "id" SERIAL NOT NULL, + "event_id" INTEGER NOT NULL, + "trigger" VARCHAR(20) NOT NULL, + "status" VARCHAR(20) NOT NULL DEFAULT 'running', + "started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ended_at" TIMESTAMP(3), + "matched_count" INTEGER NOT NULL DEFAULT 0, + "not_found_count" INTEGER NOT NULL DEFAULT 0, + "error" TEXT, + + CONSTRAINT "matching_runs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "matching_runs_event_id_started_at_idx" ON "matching_runs"("event_id", "started_at"); + +-- AddForeignKey +ALTER TABLE "matching_runs" ADD CONSTRAINT "matching_runs_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/frontend/src/components/events/MatchingRunsSection.jsx b/frontend/src/components/events/MatchingRunsSection.jsx new file mode 100644 index 0000000..cbfcd43 --- /dev/null +++ b/frontend/src/components/events/MatchingRunsSection.jsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from 'react'; +import { adminAPI } from '../../services/api'; +import { RefreshCcw, Play } 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 loadRuns = async () => { + try { + setLoading(true); + setError(''); + const res = await adminAPI.getMatchingRuns(slug, 20); + setRuns(res.data || []); + } catch (err) { + console.error('Failed to load matching runs', err); + setError(err.message || 'Failed to load matching runs'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadRuns(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [slug]); + + const handleRunNow = async () => { + try { + setRunning(true); + setError(''); + await adminAPI.runMatchingNow(slug); + await loadRuns(); + } catch (err) { + console.error('Failed to run matching now', err); + setError(err.message || 'Failed to run matching'); + } finally { + setRunning(false); + } + }; + + const formatDateTime = (dt) => dt ? new Date(dt).toLocaleString() : '-'; + + return ( +
+
+

Matching Runs

+
+ + +
+
+ + {error && ( +
{error}
+ )} + +
+ + + + + + + + + + + + + {loading ? ( + + + + ) : runs.length === 0 ? ( + + + + ) : ( + runs.map((run) => ( + + + + + + + + + )) + )} + +
StartedEndedTriggerStatusMatchedNot found
Loading...
No runs yet
{formatDateTime(run.startedAt)}{formatDateTime(run.endedAt)}{run.trigger} + + {run.status} + + {run.matchedCount}{run.notFoundCount}
+
+ + {runs.some(r => r.error) && ( +
+ Last error: {runs.find(r => r.error)?.error} +
+ )} +
+ ); +} + diff --git a/frontend/src/pages/EventDetailsPage.jsx b/frontend/src/pages/EventDetailsPage.jsx index 8e6e165..18257cd 100644 --- a/frontend/src/pages/EventDetailsPage.jsx +++ b/frontend/src/pages/EventDetailsPage.jsx @@ -7,6 +7,7 @@ import QRCodeSection from '../components/events/QRCodeSection'; import ParticipantsSection from '../components/events/ParticipantsSection'; import MatchingConfigSection from '../components/events/MatchingConfigSection'; import ScheduleConfigSection from '../components/events/ScheduleConfigSection'; +import MatchingRunsSection from '../components/events/MatchingRunsSection'; export default function EventDetailsPage() { const { slug } = useParams(); @@ -105,6 +106,11 @@ export default function EventDetailsPage() { + {/* Matching Runs (admin tools) */} +
+ +
+ {/* Schedule Configuration */}
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 0095cfc..806ab27 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -441,4 +441,20 @@ export const matchingAPI = { }, }; +// Admin API (matching control) +export const adminAPI = { + async runMatchingNow(slug) { + const data = await fetchAPI(`/admin/events/${slug}/run-now`, { + method: 'POST', + }); + return data.data; + }, + + async getMatchingRuns(slug, limit = 20) { + const params = new URLSearchParams({ limit: String(limit) }); + const data = await fetchAPI(`/admin/events/${slug}/matching-runs?${params.toString()}`); + return data; + }, +}; + export { ApiError };