{balanceRows.length === 0 ? (
- |
+ |
No rows found.
|
@@ -852,7 +862,11 @@ export function SchoolYearsManagementPage() {
| {row.parent_name || `Parent #${row.parent_id}`} |
{row.student_names?.join(', ') || '—'} |
- ${formatMoney(row.amount_to_transfer)} |
+ ${formatMoney(row.positive_unpaid_balance ?? row.old_unpaid_balance)} |
+ ${formatMoney(row.credit_balance)} |
+ ${formatMoney(row.net_balance ?? row.amount_to_transfer)} |
+ {(row.positive_invoice_ids ?? row.source_invoice_ids ?? []).join(', ') || '—'} |
+ {row.existing_transfer_conflict ? Conflict : '—'} |
))
)}
@@ -871,19 +885,23 @@ export function SchoolYearsManagementPage() {
-
Close Active Year
+
Close Selected Active Year
- 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.
- {activeYear ? (
-
{activeYear.name}
+ {selectedYearIsActive && selectedYear ? (
+
{selectedYear.name}
) : null}
- {!activeYear ? (
-
No active school year is available to close.
+ {!selectedYear ? (
+
Select a school year to close it.
+ ) : !selectedYearIsActive ? (
+
+ Only the current active school year can be closed. You are inspecting {selectedYear.name}, which is {selectedYear.status}.
+
) : (
<>
@@ -995,7 +1013,7 @@ export function SchoolYearsManagementPage() {
@@ -1033,10 +1051,10 @@ export function SchoolYearsManagementPage() {
Missing decisions:
{closeValidation.promotion_summary.hold}
- Parents with balances: {closeValidation.financial_summary.parents_with_non_zero_balance}
+ Unpaid carryover parents: {closeValidation.financial_summary.parents_with_positive_balance ?? closeValidation.financial_summary.parents_with_non_zero_balance}
- Net transfer: ${formatMoney(closeValidation.financial_summary.net_balance_to_transfer)}
+ Net impact: ${formatMoney(closeValidation.financial_summary.net_balance_impact ?? closeValidation.financial_summary.net_balance_to_transfer)}
{closeValidation.errors.length > 0 ? (
@@ -1099,13 +1117,19 @@ export function SchoolYearsManagementPage() {
Closure balance preview
+
+ Source year: {closePreview.parent_balances.from_school_year}; target year: {closePreview.parent_balances.to_school_year}. Transfer mode: unpaid balances only.
+
| Parent |
Students |
- Transfer |
+ Unpaid |
+ Credits |
+ Net |
+ Conflict |
@@ -1113,7 +1137,10 @@ export function SchoolYearsManagementPage() {
| {row.parent_name || `Parent #${row.parent_id}`} |
{row.student_names?.join(', ') || '—'} |
- ${formatMoney(row.amount_to_transfer)} |
+ ${formatMoney(row.positive_unpaid_balance ?? row.old_unpaid_balance)} |
+ ${formatMoney(row.credit_balance)} |
+ ${formatMoney(row.net_balance ?? row.amount_to_transfer)} |
+ {row.existing_transfer_conflict ? Conflict : '—'} |
))}
diff --git a/src/pages/parent/ParentStatementPage.tsx b/src/pages/parent/ParentStatementPage.tsx
index 92dc573..2ce6bad 100644
--- a/src/pages/parent/ParentStatementPage.tsx
+++ b/src/pages/parent/ParentStatementPage.tsx
@@ -18,13 +18,13 @@ function formatMoney(value: number | null | undefined): string {
}
export function ParentStatementPage() {
- const { selectedSchoolYearId, selectedSchoolYearName } = useSchoolYear()
+ const { selectedSchoolYearName } = useSchoolYear()
const [statement, setStatement] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
- if (selectedSchoolYearId == null) return
+ if (!selectedSchoolYearName) return
let cancelled = false
setLoading(true)
@@ -32,7 +32,7 @@ export function ParentStatementPage() {
;(async () => {
try {
- const nextStatement = await fetchParentStatement({ schoolYearId: selectedSchoolYearId })
+ const nextStatement = await fetchParentStatement({ schoolYear: selectedSchoolYearName })
if (!cancelled) setStatement(nextStatement)
} catch (loadError) {
if (!cancelled) setError(normalizeApiError(loadError, 'Unable to load parent statement.'))
@@ -44,7 +44,7 @@ export function ParentStatementPage() {
return () => {
cancelled = true
}
- }, [selectedSchoolYearId])
+ }, [selectedSchoolYearName])
const lines = useMemo(() => statement?.lines ?? [], [statement])
diff --git a/src/pages/staff/StaffCreatePage.tsx b/src/pages/staff/StaffCreatePage.tsx
index 4e43f3e..6dcfc0a 100644
--- a/src/pages/staff/StaffCreatePage.tsx
+++ b/src/pages/staff/StaffCreatePage.tsx
@@ -1,22 +1,27 @@
import { type FormEvent, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { createStaff, fetchStaffFormOptions, type RoleOption } from '../../api/staff'
+import { ReadOnlyBanner, SchoolYearSelector } from '../../components/schoolYear'
+import { useSchoolYear } from '../../context/SchoolYearContext'
export function StaffCreatePage() {
const navigate = useNavigate()
+ const { selectedSchoolYearName, isReadOnlyYear } = useSchoolYear()
const [roles, setRoles] = useState([])
const [firstname, setFirstname] = useState('')
const [lastname, setLastname] = useState('')
const [email, setEmail] = useState('')
+ const [phone, setPhone] = useState('')
const [password, setPassword] = useState('')
const [roleId, setRoleId] = useState(0)
+ const [status, setStatus] = useState('active')
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
;(async () => {
try {
- const data = await fetchStaffFormOptions()
+ const data = await fetchStaffFormOptions({ school_year: selectedSchoolYearName ?? undefined })
if (cancelled) return
const list = data.roles ?? []
setRoles(list)
@@ -28,17 +33,21 @@ export function StaffCreatePage() {
return () => {
cancelled = true
}
- }, [])
+ }, [selectedSchoolYearName])
async function onSubmit(e: FormEvent) {
e.preventDefault()
+ if (!selectedSchoolYearName) {
+ setError('Please select a school year.')
+ return
+ }
if (!roleId) {
setError('Please select a role.')
return
}
try {
- await createStaff({ firstname, lastname, email, password, role_id: roleId })
- navigate('/app/staff')
+ await createStaff({ firstname, lastname, email, phone, password, role_id: roleId, status, school_year: selectedSchoolYearName })
+ navigate(`/app/staff?school_year=${encodeURIComponent(selectedSchoolYearName)}`)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to create staff.')
}
@@ -47,6 +56,11 @@ export function StaffCreatePage() {
return (
Add Staff Member
+
+
School year: {selectedSchoolYearName ?? '—'}
+
+
+
{error ?
{error}
: null}
diff --git a/src/pages/staff/StaffEditPage.tsx b/src/pages/staff/StaffEditPage.tsx
index c21ec7d..1f5fd49 100644
--- a/src/pages/staff/StaffEditPage.tsx
+++ b/src/pages/staff/StaffEditPage.tsx
@@ -1,10 +1,13 @@
import { type FormEvent, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { fetchStaffMember, updateStaff } from '../../api/staff'
+import { ReadOnlyBanner, SchoolYearSelector } from '../../components/schoolYear'
+import { useSchoolYear } from '../../context/SchoolYearContext'
export function StaffEditPage() {
const { staffId } = useParams()
const navigate = useNavigate()
+ const { selectedSchoolYearName, isReadOnlyYear } = useSchoolYear()
const id = Number(staffId)
const [firstname, setFirstname] = useState('')
const [lastname, setLastname] = useState('')
@@ -17,7 +20,7 @@ export function StaffEditPage() {
let cancelled = false
;(async () => {
try {
- const data = await fetchStaffMember(id)
+ const data = await fetchStaffMember(id, { school_year: selectedSchoolYearName ?? undefined })
if (cancelled) return
const u = (data.user ?? data.staff ?? {}) as Record
setFirstname(String(u.firstname ?? ''))
@@ -32,14 +35,18 @@ export function StaffEditPage() {
return () => {
cancelled = true
}
- }, [id])
+ }, [id, selectedSchoolYearName])
async function onSubmit(e: FormEvent) {
e.preventDefault()
if (!id) return
try {
- await updateStaff(id, { firstname, lastname, email, phone })
- navigate('/app/staff')
+ if (!selectedSchoolYearName) {
+ setError('Please select a school year.')
+ return
+ }
+ await updateStaff(id, { firstname, lastname, email, phone, school_year: selectedSchoolYearName })
+ navigate(`/app/staff?school_year=${encodeURIComponent(selectedSchoolYearName)}`)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to update staff.')
}
@@ -48,6 +55,11 @@ export function StaffEditPage() {
return (
Edit Staff Member
+
+
School year: {selectedSchoolYearName ?? '—'}
+
+
+
{error ?
{error}
: null}
diff --git a/src/pages/staff/StaffIndexPage.tsx b/src/pages/staff/StaffIndexPage.tsx
index 7b63ae8..4e163b6 100644
--- a/src/pages/staff/StaffIndexPage.tsx
+++ b/src/pages/staff/StaffIndexPage.tsx
@@ -1,51 +1,112 @@
import { useCallback, useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
-import { CiAcademicFilter } from '../../components/CiPartials'
import { fetchStaffIndex, type StaffListRow } from '../../api/staff'
+import { ReadOnlyBanner, SchoolYearSelector } from '../../components/schoolYear'
+import { useSchoolYear } from '../../context/SchoolYearContext'
export function StaffIndexPage() {
const [searchParams, setSearchParams] = useSearchParams()
+ const { selectedSchoolYearName, isReadOnlyYear } = useSchoolYear()
const [rows, setRows] = useState
([])
const [issuesCount, setIssuesCount] = useState(0)
const [schoolYear, setSchoolYear] = useState(null)
- const [schoolYears, setSchoolYears] = useState([])
const [semester, setSemester] = useState(null)
+ const [search, setSearch] = useState(searchParams.get('search') ?? '')
+ const [status, setStatus] = useState(searchParams.get('status') ?? '')
+ const [role, setRole] = useState(searchParams.get('role') ?? '')
const [error, setError] = useState(null)
const load = useCallback(async () => {
+ const sy = selectedSchoolYearName ?? searchParams.get('school_year') ?? undefined
try {
- const sy = searchParams.get('school_year') ?? undefined
const sem = searchParams.get('semester') ?? undefined
- const data = await fetchStaffIndex({ school_year: sy, semester: sem })
+ const data = await fetchStaffIndex({
+ school_year: sy,
+ semester: sem,
+ search: searchParams.get('search') ?? undefined,
+ status: searchParams.get('status') ?? undefined,
+ role: searchParams.get('role') ?? undefined,
+ })
setRows(data.staff ?? [])
setIssuesCount(Number(data.issues_count ?? 0))
setSchoolYear(data.school_year ?? null)
setSemester(data.semester ?? null)
- setSchoolYears(data.schoolYears ?? [])
setError(null)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to load staff.')
}
- }, [searchParams])
+ }, [searchParams, selectedSchoolYearName])
+
+ useEffect(() => {
+ if (!selectedSchoolYearName) return
+ setSearchParams((current) => {
+ const next = new URLSearchParams(current)
+ next.delete('school_year_id')
+ next.delete('year')
+ next.set('school_year', selectedSchoolYearName)
+ return next.toString() === current.toString() ? current : next
+ }, { replace: true })
+ }, [selectedSchoolYearName, setSearchParams])
useEffect(() => {
void load()
}, [load])
+ function applyFilters() {
+ const next = new URLSearchParams()
+ if (selectedSchoolYearName) next.set('school_year', selectedSchoolYearName)
+ if (semester) next.set('semester', semester)
+ if (search.trim()) next.set('search', search.trim())
+ if (status) next.set('status', status)
+ if (role) next.set('role', role)
+ setSearchParams(next)
+ }
+
+ const selectedYear = selectedSchoolYearName ?? schoolYear ?? ''
+ const selectedQuery = selectedYear ? `?school_year=${encodeURIComponent(selectedYear)}` : ''
+
return (
-
Staff List
-
0 ? schoolYears : schoolYear ? [schoolYear] : []}
- selectedYear={searchParams.get('school_year') ?? schoolYear ?? undefined}
- selectedSemester={searchParams.get('semester') ?? semester ?? undefined}
- onApply={({ schoolYear: y, semester: sem }) => {
- const next = new URLSearchParams()
- if (y) next.set('school_year', y)
- if (sem) next.set('semester', sem)
- setSearchParams(next)
- }}
- />
+
+
+
Staff List
+
School year: {selectedYear || '—'}
+
+
+
+ {!isReadOnlyYear ? (
+ Add Staff
+ ) : null}
+
+
+
+
+
+
+
+
+ setSearch(event.target.value)} />
+
+
+
+ setSemester(event.target.value)} placeholder="Fall" />
+
+
+
+ setRole(event.target.value)} />
+
+
+
+
+
+
+ Apply
+
+
{issuesCount > 0 ? (
@@ -83,7 +144,7 @@ export function StaffIndexPage() {
{s.active_role ?? ''} |
{s.class_section ?? ''} |
-
+
Edit
|