112 lines
4.4 KiB
TypeScript
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>
|
|
)
|
|
}
|