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

210 lines
7.9 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import {
exportAdminCompetitionWinnerQuiz,
fetchAdminCompetitionWinnersIndex,
lockAdminCompetitionWinner,
unlockAdminCompetitionWinner,
} from '../../api/session'
import type { CompetitionWinnerAdminCompetitionRow } from '../../api/types'
import { ApiScopeNote, CompetitionPageShell, COMPETITION_WINNERS_BASE } from './competitionWinnersShared'
function toFiniteNumber(value: unknown): number | null {
const n = typeof value === 'number' ? value : Number(value)
return Number.isFinite(n) ? n : null
}
function competitionId(row: CompetitionWinnerAdminCompetitionRow): number | null {
return toFiniteNumber(row.id ?? row.competition_id)
}
export function CompetitionWinnersIndexPage() {
const [rows, setRows] = useState<CompetitionWinnerAdminCompetitionRow[]>([])
const [sectionMap, setSectionMap] = useState<Record<string, string>>({})
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [busyAction, setBusyAction] = useState<string | null>(null)
async function load() {
try {
const response = await fetchAdminCompetitionWinnersIndex()
if (!response.ok) {
setError(response.message ?? 'Unable to load competition winners data.')
return
}
setRows(response.competitions ?? [])
setSectionMap(response.sectionMap ?? {})
setError(null)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to load competition winners data.')
}
}
useEffect(() => {
void load()
}, [])
async function handleExport(row: CompetitionWinnerAdminCompetitionRow) {
const id = competitionId(row)
if (id === null) {
setError('This competition does not have a valid id yet.')
return
}
try {
setBusyAction(`export:${id}`)
setMessage(null)
const result = await exportAdminCompetitionWinnerQuiz(id, {
class_section_id: toFiniteNumber(row.class_section_id),
})
if (!result.ok) {
setError(result.message ?? 'Unable to export competition scores to quiz.')
return
}
setMessage(result.message ?? 'Competition scores exported to quiz.')
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to export competition scores to quiz.')
} finally {
setBusyAction(null)
}
}
async function handleLockToggle(row: CompetitionWinnerAdminCompetitionRow) {
const id = competitionId(row)
if (id === null) {
setError('This competition does not have a valid id yet.')
return
}
try {
setBusyAction(`lock:${id}`)
setMessage(null)
const result = row.is_locked
? await unlockAdminCompetitionWinner(id)
: await lockAdminCompetitionWinner(id)
if (!result.ok) {
setError(result.message ?? 'Unable to update competition lock state.')
return
}
setMessage(result.message ?? (row.is_locked ? 'Competition unlocked.' : 'Competition locked.'))
await load()
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to update competition lock state.')
} finally {
setBusyAction(null)
}
}
return (
<CompetitionPageShell title="Competition Winners">
<ApiScopeNote>
Admin competition management now uses dedicated Laravel endpoints. Draft competitions keep working here, and preview
is no longer limited to published rows.
</ApiScopeNote>
{error ? <div className="alert alert-danger">{error}</div> : null}
{message ? <div className="alert alert-success">{message}</div> : null}
<div className="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div />
<Link className="btn btn-primary" to={`${COMPETITION_WINNERS_BASE}/create`}>
+ New Competition
</Link>
</div>
<div className="table-responsive">
<table className="table table-bordered table-sm align-middle mb-0" data-no-mgmt-sticky>
<thead className="table-light">
<tr>
<th>ID</th>
<th>Title</th>
<th>Class Section</th>
<th>Winners Rule</th>
<th>Published</th>
<th>Locked</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan={7} className="text-muted">
No competitions found.
</td>
</tr>
) : (
rows.map((row) => {
const id = competitionId(row)
const sectionId = toFiniteNumber(row.class_section_id) ?? 0
const sectionName =
sectionId > 0 ? (sectionMap[String(sectionId)] ?? String(sectionId)) : 'All'
const title = String(row.title ?? (id !== null ? `Competition #${id}` : 'Competition'))
const exportBusy = busyAction === `export:${id}`
const lockBusy = busyAction === `lock:${id}`
return (
<tr key={id !== null ? String(id) : `${title}-${sectionId}`}>
<td>{id ?? '—'}</td>
<td>{title}</td>
<td>{sectionName}</td>
<td>Tiered per class</td>
<td>{row.is_published ? 'Yes' : 'No'}</td>
<td>{row.is_locked ? 'Yes' : 'No'}</td>
<td>
<div className="d-flex flex-wrap gap-1">
{id !== null ? (
<>
<Link
className="btn btn-sm btn-outline-secondary"
to={`${COMPETITION_WINNERS_BASE}/${id}/settings`}
>
Settings
</Link>
<Link className="btn btn-sm btn-outline-primary" to={`${COMPETITION_WINNERS_BASE}/${id}`}>
Enter Scores
</Link>
<Link
className="btn btn-sm btn-outline-secondary"
to={`${COMPETITION_WINNERS_BASE}/${id}/preview`}
>
Preview
</Link>
<Link
className="btn btn-sm btn-outline-secondary"
to={`${COMPETITION_WINNERS_BASE}/${id}/winners`}
>
Winners
</Link>
</>
) : (
<span className="text-muted small me-2">Invalid competition id</span>
)}
<button
type="button"
className="btn btn-sm btn-outline-success"
disabled={id === null || exportBusy || lockBusy}
onClick={() => void handleExport(row)}
>
{exportBusy ? 'Exporting...' : 'Export Scores to Quiz'}
</button>
<button
type="button"
className={`btn btn-sm ${row.is_locked ? 'btn-outline-warning' : 'btn-outline-danger'}`}
disabled={id === null || exportBusy || lockBusy}
onClick={() => void handleLockToggle(row)}
>
{lockBusy ? 'Saving...' : row.is_locked ? 'Unlock' : 'Lock'}
</button>
</div>
</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
</CompetitionPageShell>
)
}