Files
alrahma_web_client/src/pages/competitionWinners/CompetitionWinnerPreviewPage.tsx
T
2026-06-11 03:22:22 -04:00

112 lines
4.4 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { fetchAdminCompetitionWinnerPreview, publishAdminCompetitionWinner } from '../../api/session'
import type { CompetitionWinnerAdminPreviewResponse, CompetitionWinnerPreviewStudentRow } from '../../api/types'
import { ApiScopeNote, CompetitionPageShell, competitionLabel } from './competitionWinnersShared'
/** CI `admin/competition_winners/preview.php` */
export function CompetitionWinnerPreviewPage() {
const { id } = useParams()
const competitionId = Number(id)
const [data, setData] = useState<CompetitionWinnerAdminPreviewResponse | null>(null)
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [publishing, setPublishing] = useState(false)
async function load() {
try {
const response = await fetchAdminCompetitionWinnerPreview(competitionId)
setData(response)
setError(response.ok ? null : response.message ?? 'Competition not found.')
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to load winners preview.')
}
}
useEffect(() => {
if (!Number.isFinite(competitionId) || competitionId <= 0) return
void load()
}, [competitionId])
async function handlePublish() {
try {
setPublishing(true)
setMessage(null)
const result = await publishAdminCompetitionWinner(competitionId)
if (!result.ok) {
setError(result.message ?? 'Unable to publish competition winners.')
return
}
setMessage(result.message ?? 'Competition winners published.')
await load()
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to publish competition winners.')
} finally {
setPublishing(false)
}
}
return (
<CompetitionPageShell title="Preview Winners">
<ApiScopeNote>
Preview is generated from the admin scoring data, so it works before a competition is publicly published.
</ApiScopeNote>
{error ? <div className="alert alert-danger">{error}</div> : null}
{message ? <div className="alert alert-success">{message}</div> : null}
{data?.competition ? (
<>
<div className="mb-3 d-flex flex-wrap justify-content-between align-items-start gap-2">
<div>
<div>
<strong>Competition:</strong> {competitionLabel(data.competition)}
</div>
<div>
<strong>School Year:</strong> {data.competition.school_year ?? '—'}
</div>
<div>
<strong>Published:</strong> {data.competition.is_published ? 'Yes' : 'No'}
</div>
</div>
<button type="button" className="btn btn-primary" disabled={publishing} onClick={() => void handlePublish()}>
{publishing ? 'Publishing...' : 'Publish Winners'}
</button>
</div>
{Object.keys(data.topByClass ?? {}).length === 0 ? (
<div className="alert alert-warning">No ranked winners found for this competition yet.</div>
) : (
Object.entries(data.topByClass ?? {}).map(([classId, rows]) => (
<div key={classId} className="mb-4">
<h5 className="mt-4">
Top Winners - {data.classMap?.[classId] ?? `Class ${classId}`}
</h5>
<table className="table table-bordered table-sm">
<thead className="table-light">
<tr>
<th>Rank</th>
<th>Student</th>
<th>Score</th>
<th>Question Count</th>
<th>Prize</th>
</tr>
</thead>
<tbody>
{(rows as CompetitionWinnerPreviewStudentRow[]).map((row, index) => (
<tr key={`${classId}-${index}`}>
<td>{row.rank ?? index + 1}</td>
<td>{row.name ?? `Student #${row.student_id ?? ''}`}</td>
<td>{row.score ?? '—'}</td>
<td>{data.questionCounts?.[classId] ?? '—'}</td>
<td>{row.prize_amount ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
))
)}
</>
) : null}
</CompetitionPageShell>
)
}