add grading, attendnace management

This commit is contained in:
root
2026-04-29 17:39:37 -04:00
parent ea495f8382
commit dc7acf2536
27 changed files with 4150 additions and 960 deletions
+142 -39
View File
@@ -41,12 +41,25 @@ function slipPrintSearchParams(row: SlipPreviewRow): string {
}
export function SlipPreviewListPage() {
type SlipSortKey =
| 'student_name'
| 'time_in'
| 'grade'
| 'reason'
| 'school_year'
| 'semester'
| 'admin_name'
const [searchParams, setSearchParams] = useSearchParams()
const schoolYear = searchParams.get('school_year') ?? ''
const semester = searchParams.get('semester') ?? ''
const [rows, setRows] = useState<SlipPreviewRow[]>([])
const [error, setError] = useState<string | null>(null)
const [expandedDates, setExpandedDates] = useState<Set<string>>(new Set())
const [sortConfig, setSortConfig] = useState<{ key: SlipSortKey; direction: 'asc' | 'desc' }>({
key: 'student_name',
direction: 'asc',
})
useEffect(() => {
setError(null)
@@ -54,7 +67,17 @@ export function SlipPreviewListPage() {
school_year: schoolYear || undefined,
semester: semester || undefined,
})
.then((d) => setRows(d.rows ?? []))
.then((d) => {
const nextRows = d.rows ?? []
setRows(nextRows)
const dateKeys = new Set(
nextRows.map((row) => {
const raw = String(row.date ?? '').trim()
return raw || 'Unknown Date'
}),
)
setExpandedDates(dateKeys)
})
.catch((e: unknown) =>
setError(e instanceof ApiHttpError ? e.message : 'Failed to load slips.'),
)
@@ -83,6 +106,47 @@ export function SlipPreviewListPage() {
window.open(`${SLIPS_PRINT_PATH}?${q}`, '_blank', 'noopener')
}
function toggleDate(dateKey: string) {
setExpandedDates((prev) => {
const next = new Set(prev)
next.has(dateKey) ? next.delete(dateKey) : next.add(dateKey)
return next
})
}
function requestSort(key: SlipSortKey) {
setSortConfig((prev) =>
prev.key === key
? { key, direction: prev.direction === 'asc' ? 'desc' : 'asc' }
: { key, direction: 'asc' },
)
}
function sortIndicator(key: SlipSortKey) {
if (sortConfig.key !== key) return '↕'
return sortConfig.direction === 'asc' ? '↑' : '↓'
}
function sortRows(groupRows: SlipPreviewRow[]) {
return [...groupRows].sort((left, right) => {
const direction = sortConfig.direction === 'asc' ? 1 : -1
const leftValue =
sortConfig.key === 'semester'
? displaySemester(left.semester)
: String(left[sortConfig.key] ?? '')
const rightValue =
sortConfig.key === 'semester'
? displaySemester(right.semester)
: String(right[sortConfig.key] ?? '')
return (
leftValue.localeCompare(rightValue, undefined, {
numeric: true,
sensitivity: 'base',
}) * direction
)
})
}
return (
<div className="container-fluid px-0 py-4">
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
@@ -179,50 +243,89 @@ export function SlipPreviewListPage() {
{dateOrder.map((dateKey) => {
const groupRows = groupedRows[dateKey] ?? []
const expanded = expandedDates.has(dateKey)
const displayedRows = sortRows(groupRows)
return (
<div key={dateKey} className="card mb-4">
<div className="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<div className="fw-semibold">Date: {formatPreviewDateKey(dateKey)}</div>
<div
className="card-header d-flex align-items-center justify-content-between flex-wrap gap-2"
role="button"
style={{ cursor: 'pointer', userSelect: 'none' }}
onClick={() => toggleDate(dateKey)}
>
<div className="fw-semibold">
{expanded ? '▼' : '▶'} Date: {formatPreviewDateKey(dateKey)}
</div>
<span className="badge bg-secondary">{groupRows.length} slips</span>
</div>
<div className="table-responsive">
<table className="table table-striped table-bordered align-middle w-100">
<thead className="table-light">
<tr>
<th>Student Name</th>
<th>Time In</th>
<th>Grade</th>
<th>Reason</th>
<th>School Year</th>
<th>Semester</th>
<th>Admin Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{groupRows.map((r, idx) => (
<tr key={idx}>
<td>{String(r.student_name ?? '')}</td>
<td>{String(r.time_in ?? '')}</td>
<td>{String(r.grade ?? '')}</td>
<td>{String(r.reason ?? '')}</td>
<td>{String(r.school_year ?? '')}</td>
<td>{displaySemester(r.semester)}</td>
<td>{String(r.admin_name ?? '')}</td>
<td>
<button
type="button"
className="btn btn-sm btn-primary"
onClick={() => openPrint(r)}
>
Print
{expanded ? (
<div className="table-responsive">
<table className="table table-striped table-bordered align-middle w-100">
<thead className="table-light">
<tr>
<th>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => requestSort('student_name')}>
Student Name {sortIndicator('student_name')}
</button>
</td>
</th>
<th>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => requestSort('time_in')}>
Time In {sortIndicator('time_in')}
</button>
</th>
<th>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => requestSort('grade')}>
Grade {sortIndicator('grade')}
</button>
</th>
<th>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => requestSort('reason')}>
Reason {sortIndicator('reason')}
</button>
</th>
<th>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => requestSort('school_year')}>
School Year {sortIndicator('school_year')}
</button>
</th>
<th>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => requestSort('semester')}>
Semester {sortIndicator('semester')}
</button>
</th>
<th>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => requestSort('admin_name')}>
Admin Name {sortIndicator('admin_name')}
</button>
</th>
<th>Action</th>
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{displayedRows.map((r, idx) => (
<tr key={idx}>
<td>{String(r.student_name ?? '')}</td>
<td>{String(r.time_in ?? '')}</td>
<td>{String(r.grade ?? '')}</td>
<td>{String(r.reason ?? '')}</td>
<td>{String(r.school_year ?? '')}</td>
<td>{displaySemester(r.semester)}</td>
<td>{String(r.admin_name ?? '')}</td>
<td>
<button
type="button"
className="btn btn-sm btn-primary"
onClick={() => openPrint(r)}
>
Print
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</div>
)
})}