fix apply the plan docs/alrahma_api_fix_plan_school_year_only_v7
This commit is contained in:
@@ -141,7 +141,7 @@ export function SchoolYearsManagementPage() {
|
||||
const [busyEdit, setBusyEdit] = useState(false)
|
||||
const [busyOpenId, setBusyOpenId] = useState<number | null>(null)
|
||||
const [busyCloseAction, setBusyCloseAction] = useState<'validate' | 'preview' | 'close' | null>(null)
|
||||
const selectedYearId = Number(searchParams.get('year') ?? 0) || null
|
||||
const selectedYearName = searchParams.get('school_year')?.trim() || null
|
||||
|
||||
const activeYear = useMemo(
|
||||
() => years.find((year) => Boolean(year.is_current)) ?? null,
|
||||
@@ -150,13 +150,13 @@ export function SchoolYearsManagementPage() {
|
||||
|
||||
const selectedYear = useMemo(() => {
|
||||
if (years.length === 0) return null
|
||||
if (selectedYearId != null) {
|
||||
return years.find((year) => year.id === selectedYearId) ?? null
|
||||
if (selectedYearName) {
|
||||
return years.find((year) => year.name === selectedYearName) ?? null
|
||||
}
|
||||
return activeYear ?? years[0] ?? null
|
||||
}, [activeYear, selectedYearId, years])
|
||||
}, [activeYear, selectedYearName, years])
|
||||
|
||||
async function refreshYears(preferredYearId?: number | null) {
|
||||
async function refreshYears(preferredYearName?: string | null) {
|
||||
setLoading(true)
|
||||
setLoadingMessage('Loading school years…')
|
||||
setError(null)
|
||||
@@ -166,15 +166,17 @@ export function SchoolYearsManagementPage() {
|
||||
setYears(list)
|
||||
|
||||
const nextSelectedId =
|
||||
preferredYearId ??
|
||||
(selectedYearId != null && list.some((year) => year.id === selectedYearId)
|
||||
? selectedYearId
|
||||
: list.find((year) => year.is_current)?.id ?? list[0]?.id ?? null)
|
||||
preferredYearName ??
|
||||
(selectedYearName && list.some((year) => year.name === selectedYearName)
|
||||
? selectedYearName
|
||||
: list.find((year) => year.is_current)?.name ?? list[0]?.name ?? null)
|
||||
|
||||
if (nextSelectedId != null) {
|
||||
setSearchParams((current) => {
|
||||
const next = new URLSearchParams(current)
|
||||
next.set('year', String(nextSelectedId))
|
||||
next.delete('year')
|
||||
next.delete('school_year_id')
|
||||
next.set('school_year', nextSelectedId)
|
||||
return next
|
||||
}, { replace: true })
|
||||
}
|
||||
@@ -238,7 +240,7 @@ export function SchoolYearsManagementPage() {
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
const nextSummary = await fetchSchoolYearSummary(selectedYear.id)
|
||||
const nextSummary = await fetchSchoolYearSummary(selectedYear.name)
|
||||
if (!cancelled) {
|
||||
setSummary(nextSummary)
|
||||
}
|
||||
@@ -273,10 +275,12 @@ export function SchoolYearsManagementPage() {
|
||||
setMessages([{ type, message }])
|
||||
}
|
||||
|
||||
function selectYear(yearId: number) {
|
||||
function selectYear(yearName: string) {
|
||||
setSearchParams((current) => {
|
||||
const next = new URLSearchParams(current)
|
||||
next.set('year', String(yearId))
|
||||
next.delete('year')
|
||||
next.delete('school_year_id')
|
||||
next.set('school_year', yearName)
|
||||
return next
|
||||
})
|
||||
setPromotionPreview(null)
|
||||
@@ -292,7 +296,7 @@ export function SchoolYearsManagementPage() {
|
||||
const created = await createSchoolYear(createForm)
|
||||
pushMessage('success', `Draft school year ${created.name} created.`)
|
||||
setCreateForm(emptyYearForm())
|
||||
await refreshYears(created.id)
|
||||
await refreshYears(created.name)
|
||||
} catch (saveError) {
|
||||
setError(normalizeApiError(saveError, 'Unable to create school year.'))
|
||||
} finally {
|
||||
@@ -306,9 +310,9 @@ export function SchoolYearsManagementPage() {
|
||||
setBusyEdit(true)
|
||||
setError(null)
|
||||
try {
|
||||
const updated = await updateSchoolYear(selectedYear.id, editForm)
|
||||
const updated = await updateSchoolYear(selectedYear.name, editForm)
|
||||
pushMessage('success', `School year ${updated.name} updated.`)
|
||||
await refreshYears(updated.id)
|
||||
await refreshYears(updated.name)
|
||||
} catch (saveError) {
|
||||
setError(normalizeApiError(saveError, 'Unable to update school year.'))
|
||||
} finally {
|
||||
@@ -320,11 +324,11 @@ export function SchoolYearsManagementPage() {
|
||||
setBusyOpenId(year.id)
|
||||
setError(null)
|
||||
try {
|
||||
const reopened = await reopenSchoolYear(year.id)
|
||||
const reopened = await reopenSchoolYear(year.name)
|
||||
pushMessage('success', `School year ${reopened.name} is now active.`)
|
||||
setCloseValidation(null)
|
||||
setClosePreview(null)
|
||||
await refreshYears(reopened.id)
|
||||
await refreshYears(reopened.name)
|
||||
} catch (actionError) {
|
||||
setError(normalizeApiError(actionError, 'Unable to open school year.'))
|
||||
} finally {
|
||||
@@ -338,7 +342,7 @@ export function SchoolYearsManagementPage() {
|
||||
setError(null)
|
||||
try {
|
||||
const preview = await fetchSchoolYearPromotionPreview(
|
||||
selectedYear.id,
|
||||
selectedYear.name,
|
||||
suggestNextName(selectedYear.name),
|
||||
)
|
||||
setPromotionPreview(preview)
|
||||
@@ -353,7 +357,7 @@ export function SchoolYearsManagementPage() {
|
||||
setError(null)
|
||||
try {
|
||||
const balances = await fetchSchoolYearParentBalances(
|
||||
selectedYear.id,
|
||||
selectedYear.name,
|
||||
suggestNextName(selectedYear.name),
|
||||
)
|
||||
setParentBalances(balances)
|
||||
@@ -363,11 +367,11 @@ export function SchoolYearsManagementPage() {
|
||||
}
|
||||
|
||||
async function handleValidateClose() {
|
||||
if (!activeYear) return
|
||||
if (!selectedYear?.is_current) return
|
||||
setBusyCloseAction('validate')
|
||||
setError(null)
|
||||
try {
|
||||
const validation = await validateSchoolYearClose(activeYear.id, closePayloadFrom(closeForm))
|
||||
const validation = await validateSchoolYearClose(selectedYear.name, closePayloadFrom(closeForm))
|
||||
setCloseValidation(validation)
|
||||
setClosePreview(null)
|
||||
setCloseResult(null)
|
||||
@@ -379,11 +383,11 @@ export function SchoolYearsManagementPage() {
|
||||
}
|
||||
|
||||
async function handlePreviewClose() {
|
||||
if (!activeYear) return
|
||||
if (!selectedYear?.is_current) return
|
||||
setBusyCloseAction('preview')
|
||||
setError(null)
|
||||
try {
|
||||
const preview = await previewSchoolYearClose(activeYear.id, closePayloadFrom(closeForm))
|
||||
const preview = await previewSchoolYearClose(selectedYear.name, closePayloadFrom(closeForm))
|
||||
setCloseValidation(preview.validation)
|
||||
setClosePreview(preview)
|
||||
setCloseResult(null)
|
||||
@@ -396,11 +400,11 @@ export function SchoolYearsManagementPage() {
|
||||
|
||||
async function handleClose(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
if (!activeYear) return
|
||||
if (!selectedYear?.is_current) return
|
||||
setBusyCloseAction('close')
|
||||
setError(null)
|
||||
try {
|
||||
const result = await closeSchoolYear(activeYear.id, closePayloadFrom(closeForm))
|
||||
const result = await closeSchoolYear(selectedYear.name, closePayloadFrom(closeForm))
|
||||
pushMessage(
|
||||
'success',
|
||||
`Closed ${result.closed_school_year.name} and opened ${result.new_school_year.name}.`,
|
||||
@@ -411,7 +415,7 @@ export function SchoolYearsManagementPage() {
|
||||
setPromotionPreview(null)
|
||||
setParentBalances(null)
|
||||
setCloseForm(emptyCloseForm())
|
||||
await refreshYears(result.new_school_year.id)
|
||||
await refreshYears(result.new_school_year.name)
|
||||
} catch (actionError) {
|
||||
setError(normalizeApiError(actionError, 'Unable to close school year.'))
|
||||
} finally {
|
||||
@@ -425,8 +429,9 @@ export function SchoolYearsManagementPage() {
|
||||
const closePromotionRows = closePreview?.promotion_preview.rows ?? []
|
||||
const closeBalanceRows = closePreview?.parent_balances.rows ?? []
|
||||
const selectedYearReadOnly = isReadOnlySchoolYear(selectedYear)
|
||||
const requiredConfirmation = activeYear ? `CLOSE ${activeYear.name}` : ''
|
||||
const confirmationMatches = !activeYear || closeForm.confirmation.trim() === requiredConfirmation
|
||||
const selectedYearIsActive = Boolean(selectedYear?.is_current)
|
||||
const requiredConfirmation = selectedYearIsActive && selectedYear ? `CLOSE ${selectedYear.name}` : ''
|
||||
const confirmationMatches = !selectedYearIsActive || closeForm.confirmation.trim() === requiredConfirmation
|
||||
|
||||
return (
|
||||
<div className="container-fluid py-4">
|
||||
@@ -484,7 +489,7 @@ export function SchoolYearsManagementPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link p-0 fw-semibold text-decoration-none"
|
||||
onClick={() => selectYear(year.id)}
|
||||
onClick={() => selectYear(year.name)}
|
||||
>
|
||||
{year.name}
|
||||
</button>
|
||||
@@ -498,13 +503,13 @@ export function SchoolYearsManagementPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => selectYear(year.id)}
|
||||
onClick={() => selectYear(year.name)}
|
||||
>
|
||||
Inspect
|
||||
</button>
|
||||
<Link
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
to={`/app/admin/school-years/${year.id}/summary`}
|
||||
to={`/app/admin/school-years/summary?school_year=${encodeURIComponent(year.name)}`}
|
||||
>
|
||||
Summary
|
||||
</Link>
|
||||
@@ -634,14 +639,14 @@ export function SchoolYearsManagementPage() {
|
||||
</div>
|
||||
<div className="col-md-6 col-xl-3">
|
||||
<div className="border rounded p-3 h-100">
|
||||
<div className="small text-muted text-uppercase">Parents with balances</div>
|
||||
<div className="display-6">{summaryCards.parents_with_balances}</div>
|
||||
<div className="small text-muted text-uppercase">Unpaid carryover parents</div>
|
||||
<div className="display-6">{summaryCards.parents_with_positive_balance ?? summaryCards.parents_with_balances}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 col-xl-3">
|
||||
<div className="border rounded p-3 h-100">
|
||||
<div className="small text-muted text-uppercase">Net balance impact</div>
|
||||
<div className="display-6">${formatMoney(summaryCards.net_balance_to_transfer)}</div>
|
||||
<div className="display-6">${formatMoney(summaryCards.net_balance_impact ?? summaryCards.net_balance_to_transfer)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -651,13 +656,13 @@ export function SchoolYearsManagementPage() {
|
||||
|
||||
{selectedYear ? (
|
||||
<div className="d-flex flex-wrap gap-2 mb-4">
|
||||
<Link className="btn btn-outline-secondary btn-sm" to={`/app/admin/school-years/${selectedYear.id}/summary`}>
|
||||
<Link className="btn btn-outline-secondary btn-sm" to={`/app/admin/school-years/summary?school_year=${encodeURIComponent(selectedYear.name)}`}>
|
||||
Open summary page
|
||||
</Link>
|
||||
<Link className="btn btn-outline-secondary btn-sm" to={`/app/admin/school-years/${selectedYear.id}/reports/closing`}>
|
||||
<Link className="btn btn-outline-secondary btn-sm" to={`/app/admin/school-years/reports/closing?school_year=${encodeURIComponent(selectedYear.name)}`}>
|
||||
Closing report
|
||||
</Link>
|
||||
<Link className="btn btn-outline-secondary btn-sm" to={`/app/admin/school-years/${selectedYear.id}/reports/balance-transfers`}>
|
||||
<Link className="btn btn-outline-secondary btn-sm" to={`/app/admin/school-years/reports/balance-transfers?school_year=${encodeURIComponent(selectedYear.name)}`}>
|
||||
Balance transfer report
|
||||
</Link>
|
||||
</div>
|
||||
@@ -828,8 +833,9 @@ export function SchoolYearsManagementPage() {
|
||||
) : (
|
||||
<>
|
||||
<div className="small text-muted mb-3">
|
||||
{parentBalances.summary.parents_with_non_zero_balance} parents with
|
||||
balances, net ${formatMoney(parentBalances.summary.net_balance_to_transfer)}.
|
||||
Source year: {parentBalances.from_school_year}; target year: {parentBalances.to_school_year}.{' '}
|
||||
Unpaid ${formatMoney(parentBalances.summary.total_positive_unpaid_balance ?? parentBalances.summary.total_old_unpaid_balance)},{' '}
|
||||
credits ${formatMoney(parentBalances.summary.total_credit_balance)}, net ${formatMoney(parentBalances.summary.net_balance_impact ?? parentBalances.summary.net_balance_to_transfer)}.
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm align-middle mb-0">
|
||||
@@ -837,13 +843,17 @@ export function SchoolYearsManagementPage() {
|
||||
<tr>
|
||||
<th>Parent</th>
|
||||
<th>Students</th>
|
||||
<th>Transfer</th>
|
||||
<th className="text-end">Unpaid</th>
|
||||
<th className="text-end">Credits</th>
|
||||
<th className="text-end">Net</th>
|
||||
<th>Sources</th>
|
||||
<th>Conflict</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{balanceRows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-muted">
|
||||
<td colSpan={7} className="text-muted">
|
||||
No rows found.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -852,7 +862,11 @@ export function SchoolYearsManagementPage() {
|
||||
<tr key={row.parent_id}>
|
||||
<td>{row.parent_name || `Parent #${row.parent_id}`}</td>
|
||||
<td>{row.student_names?.join(', ') || '—'}</td>
|
||||
<td>${formatMoney(row.amount_to_transfer)}</td>
|
||||
<td className="text-end">${formatMoney(row.positive_unpaid_balance ?? row.old_unpaid_balance)}</td>
|
||||
<td className="text-end">${formatMoney(row.credit_balance)}</td>
|
||||
<td className="text-end">${formatMoney(row.net_balance ?? row.amount_to_transfer)}</td>
|
||||
<td>{(row.positive_invoice_ids ?? row.source_invoice_ids ?? []).join(', ') || '—'}</td>
|
||||
<td>{row.existing_transfer_conflict ? <span className="badge text-bg-warning">Conflict</span> : '—'}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -871,19 +885,23 @@ export function SchoolYearsManagementPage() {
|
||||
<div className="card-body">
|
||||
<div className="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
||||
<div>
|
||||
<h2 className="h5 mb-1">Close Active Year</h2>
|
||||
<h2 className="h5 mb-1">Close Selected Active Year</h2>
|
||||
<div className="text-muted small">
|
||||
Validate, preview, and close the current year through the transactional
|
||||
school-year closure endpoints.
|
||||
Validate, preview, and close the inspected school year through the transactional
|
||||
school-year closure endpoints. Only the current active year can be closed.
|
||||
</div>
|
||||
</div>
|
||||
{activeYear ? (
|
||||
<span className="badge text-bg-success">{activeYear.name}</span>
|
||||
{selectedYearIsActive && selectedYear ? (
|
||||
<span className="badge text-bg-success">{selectedYear.name}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!activeYear ? (
|
||||
<p className="text-muted mb-0">No active school year is available to close.</p>
|
||||
{!selectedYear ? (
|
||||
<p className="text-muted mb-0">Select a school year to close it.</p>
|
||||
) : !selectedYearIsActive ? (
|
||||
<div className="alert alert-info mb-0">
|
||||
Only the current active school year can be closed. You are inspecting {selectedYear.name}, which is {selectedYear.status}.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="row g-2 mb-3 small" aria-label="Close-year wizard steps">
|
||||
@@ -995,7 +1013,7 @@ export function SchoolYearsManagementPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={busyCloseAction === 'validate'}
|
||||
disabled={busyCloseAction === 'validate' || !selectedYearIsActive}
|
||||
onClick={() => void handleValidateClose()}
|
||||
>
|
||||
{busyCloseAction === 'validate' ? 'Validating…' : 'Validate'}
|
||||
@@ -1003,7 +1021,7 @@ export function SchoolYearsManagementPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
disabled={busyCloseAction === 'preview'}
|
||||
disabled={busyCloseAction === 'preview' || !selectedYearIsActive}
|
||||
onClick={() => void handlePreviewClose()}
|
||||
>
|
||||
{busyCloseAction === 'preview' ? 'Previewing…' : 'Preview'}
|
||||
@@ -1011,9 +1029,9 @@ export function SchoolYearsManagementPage() {
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-danger"
|
||||
disabled={busyCloseAction === 'close' || !confirmationMatches}
|
||||
disabled={busyCloseAction === 'close' || !confirmationMatches || !selectedYearIsActive}
|
||||
>
|
||||
{busyCloseAction === 'close' ? 'Closing…' : `Close ${activeYear.name}`}
|
||||
{busyCloseAction === 'close' ? 'Closing…' : `Close ${selectedYear.name}`}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1033,10 +1051,10 @@ export function SchoolYearsManagementPage() {
|
||||
Missing decisions: <strong>{closeValidation.promotion_summary.hold}</strong>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
Parents with balances: <strong>{closeValidation.financial_summary.parents_with_non_zero_balance}</strong>
|
||||
Unpaid carryover parents: <strong>{closeValidation.financial_summary.parents_with_positive_balance ?? closeValidation.financial_summary.parents_with_non_zero_balance}</strong>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
Net transfer: <strong>${formatMoney(closeValidation.financial_summary.net_balance_to_transfer)}</strong>
|
||||
Net impact: <strong>${formatMoney(closeValidation.financial_summary.net_balance_impact ?? closeValidation.financial_summary.net_balance_to_transfer)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
{closeValidation.errors.length > 0 ? (
|
||||
@@ -1099,13 +1117,19 @@ export function SchoolYearsManagementPage() {
|
||||
<div className="col-lg-6">
|
||||
<div className="border rounded p-3 h-100">
|
||||
<h3 className="h6">Closure balance preview</h3>
|
||||
<div className="small text-muted mb-2">
|
||||
Source year: {closePreview.parent_balances.from_school_year}; target year: {closePreview.parent_balances.to_school_year}. Transfer mode: unpaid balances only.
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parent</th>
|
||||
<th>Students</th>
|
||||
<th>Transfer</th>
|
||||
<th className="text-end">Unpaid</th>
|
||||
<th className="text-end">Credits</th>
|
||||
<th className="text-end">Net</th>
|
||||
<th>Conflict</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -1113,7 +1137,10 @@ export function SchoolYearsManagementPage() {
|
||||
<tr key={`balance-${row.parent_id}`}>
|
||||
<td>{row.parent_name || `Parent #${row.parent_id}`}</td>
|
||||
<td>{row.student_names?.join(', ') || '—'}</td>
|
||||
<td>${formatMoney(row.amount_to_transfer)}</td>
|
||||
<td className="text-end">${formatMoney(row.positive_unpaid_balance ?? row.old_unpaid_balance)}</td>
|
||||
<td className="text-end">${formatMoney(row.credit_balance)}</td>
|
||||
<td className="text-end">${formatMoney(row.net_balance ?? row.amount_to_transfer)}</td>
|
||||
<td>{row.existing_transfer_conflict ? <span className="badge text-bg-warning">Conflict</span> : '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user