page fixes
This commit is contained in:
@@ -1,85 +1,101 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { fetchPublishedCompetition } from '../../api/session'
|
||||
import type { PublicCompetitionRow, PublicCompetitionWinnerRow } from '../../api/types'
|
||||
import { fetchAdminCompetitionWinnerPreview, publishAdminCompetitionWinner } from '../../api/session'
|
||||
import type { CompetitionWinnerAdminPreviewResponse, CompetitionWinnerPreviewStudentRow } from '../../api/types'
|
||||
import { ApiScopeNote, CompetitionPageShell, competitionLabel } from './competitionWinnersShared'
|
||||
|
||||
function usePublishedCompetition(id: number) {
|
||||
const [competition, setCompetition] = useState<PublicCompetitionRow | null>(null)
|
||||
const [winnersByClass, setWinnersByClass] = useState<Record<string, PublicCompetitionWinnerRow[]>>({})
|
||||
const [sectionMap, setSectionMap] = useState<Record<string, string>>({})
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!Number.isFinite(id) || id <= 0) return
|
||||
;(async () => {
|
||||
try {
|
||||
const response = await fetchPublishedCompetition(id)
|
||||
if (!response.status || !response.data) {
|
||||
setError(response.message ?? 'Competition not found.')
|
||||
return
|
||||
}
|
||||
setCompetition(response.data.competition)
|
||||
setWinnersByClass(response.data.winners_by_class ?? {})
|
||||
setSectionMap(response.data.section_map ?? {})
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unable to load winners.')
|
||||
}
|
||||
})()
|
||||
}, [id])
|
||||
|
||||
return { competition, winnersByClass, sectionMap, error }
|
||||
}
|
||||
|
||||
/** CI `admin/competition_winners/preview.php` */
|
||||
export function CompetitionWinnerPreviewPage() {
|
||||
const { id } = useParams()
|
||||
const competitionId = Number(id)
|
||||
const { competition, winnersByClass, sectionMap, error } = usePublishedCompetition(competitionId)
|
||||
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 reflects published winners from the public API. Unpublished drafts are not shown until the backend exposes an
|
||||
admin preview.
|
||||
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}
|
||||
{competition ? (
|
||||
{message ? <div className="alert alert-success">{message}</div> : null}
|
||||
{data?.competition ? (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 d-flex flex-wrap justify-content-between align-items-start gap-2">
|
||||
<div>
|
||||
<strong>Competition:</strong> {competitionLabel(competition)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>School Year:</strong> {competition.school_year ?? '—'}
|
||||
<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(winnersByClass).length === 0 ? (
|
||||
<div className="alert alert-warning">No published winners found for this competition.</div>
|
||||
{Object.keys(data.topByClass ?? {}).length === 0 ? (
|
||||
<div className="alert alert-warning">No ranked winners found for this competition yet.</div>
|
||||
) : (
|
||||
Object.entries(winnersByClass).map(([classId, rows]) => (
|
||||
Object.entries(data.topByClass ?? {}).map(([classId, rows]) => (
|
||||
<div key={classId} className="mb-4">
|
||||
<h5 className="mt-4">Top Winners - {sectionMap[classId] ?? `Class ${classId}`}</h5>
|
||||
<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.map((row, index) => (
|
||||
{(rows as CompetitionWinnerPreviewStudentRow[]).map((row, index) => (
|
||||
<tr key={`${classId}-${index}`}>
|
||||
<td>{row.rank ?? index + 1}</td>
|
||||
<td>
|
||||
{row.name ??
|
||||
(`${row.firstname ?? ''} ${row.lastname ?? ''}`.trim() ||
|
||||
`Student #${row.student_id ?? ''}`)}
|
||||
</td>
|
||||
<td>{row.name ?? `Student #${row.student_id ?? ''}`}</td>
|
||||
<td>{row.score ?? '—'}</td>
|
||||
<td>{data.questionCounts?.[classId] ?? '—'}</td>
|
||||
<td>{row.prize_amount ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user