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

364 lines
14 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
fetchAdminCompetitionWinnerCreate,
fetchAdminCompetitionWinnerSettings,
saveAdminCompetitionWinner,
} from '../../api/session'
import type { CompetitionWinnerAdminClassRow, CompetitionWinnerAdminFormResponse } from '../../api/types'
import { ApiScopeNote, CompetitionPageShell, COMPETITION_WINNERS_BASE } from './competitionWinnersShared'
type CompetitionFormState = {
title: string
class_section_id: string
semester: string
school_year: string
start_date: string
end_date: string
}
function numberOrNull(value: string): number | null {
const trimmed = value.trim()
if (trimmed === '') return null
const parsed = Number(trimmed)
return Number.isFinite(parsed) ? parsed : null
}
function initialFormState(payload: CompetitionWinnerAdminFormResponse): CompetitionFormState {
const competition = payload.competition
return {
title: String(competition?.title ?? ''),
class_section_id:
competition?.class_section_id === null || competition?.class_section_id === undefined
? ''
: String(competition.class_section_id),
semester: String(competition?.semester ?? payload.defaultSemester ?? ''),
school_year: String(competition?.school_year ?? payload.defaultSchoolYear ?? ''),
start_date: String(competition?.start_date ?? ''),
end_date: String(competition?.end_date ?? ''),
}
}
function normalizeRowValue(value: number | string | null | undefined): string {
return value === null || value === undefined ? '' : String(value)
}
/** CI `admin/competition_winners/form.php` */
export function CompetitionWinnerFormPage() {
const { id } = useParams()
const navigate = useNavigate()
const isEdit = Boolean(id)
const competitionId = Number(id)
const [form, setForm] = useState<CompetitionFormState>({
title: '',
class_section_id: '',
semester: '',
school_year: '',
start_date: '',
end_date: '',
})
const [classRows, setClassRows] = useState<CompetitionWinnerAdminClassRow[]>([])
const [classSections, setClassSections] = useState<CompetitionWinnerAdminFormResponse['classSections']>([])
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
;(async () => {
try {
const response =
isEdit && Number.isFinite(competitionId) && competitionId > 0
? await fetchAdminCompetitionWinnerSettings(competitionId)
: await fetchAdminCompetitionWinnerCreate()
if (!response.ok) {
setError(response.message ?? 'Unable to load competition settings.')
return
}
setForm(initialFormState(response))
setClassRows(response.classRows ?? [])
setClassSections(response.classSections ?? [])
setError(null)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to load competition settings.')
}
})()
}, [competitionId, isEdit])
const visibleRows = useMemo(() => {
const selectedClassId = numberOrNull(form.class_section_id)
if (selectedClassId === null) return classRows
return classRows.filter((row) => row.class_section_id === selectedClassId)
}, [classRows, form.class_section_id])
function updateClassRow(
classSectionId: number,
key:
| 'question_count'
| 'override_winners'
| 'prize_1'
| 'prize_2'
| 'prize_3'
| 'prize_4'
| 'prize_5'
| 'prize_6',
value: string,
) {
setClassRows((current) =>
current.map((row) => {
if (row.class_section_id !== classSectionId) return row
return {
...row,
[key]: value.trim() === '' ? null : Number(value),
}
}),
)
}
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
try {
setSaving(true)
setMessage(null)
setError(null)
const winnerOverrides = Object.fromEntries(
classRows.map((row) => [String(row.class_section_id), normalizeRowValue(row.override_winners)]),
)
const questionCounts = Object.fromEntries(
classRows.map((row) => [String(row.class_section_id), normalizeRowValue(row.question_count)]),
)
const prizes = Object.fromEntries(
classRows.map((row) => [
String(row.class_section_id),
{
1: normalizeRowValue(row.prize_1),
2: normalizeRowValue(row.prize_2),
3: normalizeRowValue(row.prize_3),
4: normalizeRowValue(row.prize_4),
5: normalizeRowValue(row.prize_5),
6: normalizeRowValue(row.prize_6),
},
]),
)
const result = await saveAdminCompetitionWinner(
{
title: form.title,
class_section_id: numberOrNull(form.class_section_id),
semester: form.semester || null,
school_year: form.school_year || null,
start_date: form.start_date || null,
end_date: form.end_date || null,
winner_overrides: winnerOverrides,
question_counts: questionCounts,
prizes,
},
isEdit && Number.isFinite(competitionId) && competitionId > 0 ? competitionId : undefined,
)
if (!result.ok) {
setError(result.message ?? 'Unable to save competition.')
return
}
const nextId = result.id ?? competitionId
setMessage(result.message ?? 'Competition saved.')
if (Number.isFinite(nextId) && nextId > 0) {
navigate(`${COMPETITION_WINNERS_BASE}/${nextId}/settings`, { replace: true })
return
}
navigate(COMPETITION_WINNERS_BASE)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to save competition.')
} finally {
setSaving(false)
}
}
return (
<CompetitionPageShell title={isEdit ? 'Edit Competition' : 'Create Competition'}>
<ApiScopeNote>
This screen now saves directly through the Laravel admin competition endpoints, including per-class winner overrides
and prize grids.
</ApiScopeNote>
{error ? <div className="alert alert-danger">{error}</div> : null}
{message ? <div className="alert alert-success">{message}</div> : null}
<form className="card shadow-sm" onSubmit={(e) => void onSubmit(e)}>
<div className="card-body row g-3">
<div className="col-md-8">
<label className="form-label">Title *</label>
<input
className="form-control"
value={form.title}
onChange={(e) => setForm((current) => ({ ...current, title: e.target.value }))}
required
/>
</div>
<div className="col-md-6">
<label className="form-label">Class Section (optional)</label>
<select
className="form-select"
value={form.class_section_id}
onChange={(e) => setForm((current) => ({ ...current, class_section_id: e.target.value }))}
>
<option value="">All classes</option>
{(classSections ?? []).map((section) => (
<option key={String(section?.class_section_id ?? '')} value={String(section?.class_section_id ?? '')}>
{section?.class_section_name ?? section?.class_section_id}
</option>
))}
</select>
<div className="form-text">Leave empty to manage winners for all classes.</div>
</div>
<div className="col-md-3">
<label className="form-label">Semester</label>
<input
className="form-control"
value={form.semester}
onChange={(e) => setForm((current) => ({ ...current, semester: e.target.value }))}
/>
</div>
<div className="col-md-3">
<label className="form-label">School Year</label>
<input
className="form-control"
value={form.school_year}
onChange={(e) => setForm((current) => ({ ...current, school_year: e.target.value }))}
placeholder="2025-2026"
/>
<div className="form-text">Counts use the school year above.</div>
</div>
<div className="col-md-4">
<label className="form-label">Start Date</label>
<input
className="form-control"
type="date"
value={form.start_date}
onChange={(e) => setForm((current) => ({ ...current, start_date: e.target.value }))}
/>
</div>
<div className="col-md-4">
<label className="form-label">End Date</label>
<input
className="form-control"
type="date"
value={form.end_date}
onChange={(e) => setForm((current) => ({ ...current, end_date: e.target.value }))}
/>
</div>
<div className="col-12">
<h5 className="mt-2">Winners per Class</h5>
<div className="table-responsive">
<table className="table table-bordered table-sm" data-no-mgmt-sticky>
<thead className="table-light">
<tr>
<th>Class</th>
<th style={{ width: 140 }}>Students</th>
<th style={{ width: 140 }}>Auto Winners</th>
<th style={{ width: 150 }}>Questions</th>
<th style={{ width: 120 }}>1st</th>
<th style={{ width: 120 }}>2nd</th>
<th style={{ width: 120 }}>3rd</th>
<th style={{ width: 120 }}>4th</th>
<th style={{ width: 120 }}>5th</th>
<th style={{ width: 120 }}>6th</th>
<th style={{ width: 160 }}>Override</th>
<th style={{ width: 140 }}>Final Winners</th>
</tr>
</thead>
<tbody>
{visibleRows.length === 0 ? (
<tr>
<td colSpan={12} className="text-muted small">
No class rows found for the selected scope.
</td>
</tr>
) : (
visibleRows.map((row) => {
const finalWinners =
row.override_winners === null || row.override_winners === undefined
? row.auto_winners
: row.override_winners
return (
<tr key={row.class_section_id}>
<td>{row.class_section_name}</td>
<td>{row.student_count}</td>
<td>{row.auto_winners}</td>
<td>
<input
className="form-control form-control-sm"
type="number"
min="0"
value={normalizeRowValue(row.question_count)}
onChange={(e) => updateClassRow(row.class_section_id, 'question_count', e.target.value)}
/>
</td>
{[1, 2, 3, 4, 5, 6].map((rank) => (
<td key={rank}>
<input
className="form-control form-control-sm"
type="number"
min="0"
step="0.01"
value={normalizeRowValue(row[`prize_${rank}` as keyof CompetitionWinnerAdminClassRow] as number | null | undefined)}
onChange={(e) =>
updateClassRow(
row.class_section_id,
`prize_${rank}` as
| 'prize_1'
| 'prize_2'
| 'prize_3'
| 'prize_4'
| 'prize_5'
| 'prize_6',
e.target.value,
)
}
/>
</td>
))}
<td>
<input
className="form-control form-control-sm"
type="number"
min="0"
value={normalizeRowValue(row.override_winners)}
onChange={(e) => updateClassRow(row.class_section_id, 'override_winners', e.target.value)}
/>
</td>
<td>{finalWinners}</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
</div>
<div className="col-12 d-flex gap-2">
<button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? 'Saving...' : 'Save'}
</button>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => navigate(COMPETITION_WINNERS_BASE)}
disabled={saving}
>
Back
</button>
</div>
</div>
</form>
</CompetitionPageShell>
)
}