page fixes
This commit is contained in:
@@ -1,47 +1,255 @@
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { ApiScopeNote, CompetitionPageShell } from './competitionWinnersShared'
|
||||
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'
|
||||
|
||||
/** CI `admin/competition_winners/form.php` — layout parity; persist requires admin competition APIs. */
|
||||
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>
|
||||
Saving competitions and per-class prize grids requires POST endpoints that mirror CI (
|
||||
<code>store</code>, <code>update</code>). Connect the API to enable the fields below.
|
||||
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}
|
||||
|
||||
<div className="card shadow-sm">
|
||||
<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" disabled placeholder="Competition title" />
|
||||
<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" disabled>
|
||||
<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" disabled />
|
||||
<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" disabled placeholder="2025-2026" />
|
||||
<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" disabled />
|
||||
<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" disabled />
|
||||
<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">
|
||||
@@ -65,23 +273,91 @@ export function CompetitionWinnerFormPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={12} className="text-muted small">
|
||||
Class rows load from the backend when competition settings APIs are available.
|
||||
</td>
|
||||
</tr>
|
||||
{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">
|
||||
<button type="button" className="btn btn-primary" disabled>
|
||||
Save
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</CompetitionPageShell>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user