fix teacher, parent and admin pages
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
import { type FormEvent, useEffect, useMemo, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { ApiHttpError } from '../../api/http'
|
||||
import { fetchSlipPreviewList, type SlipPreviewRow } from '../../api/slips'
|
||||
import { displaySemester, formatPreviewDateKey } from './slipFormatting'
|
||||
import { SLIPS_PRINT_PATH } from './slipPaths'
|
||||
|
||||
function groupRowsByDate(rows: SlipPreviewRow[]) {
|
||||
const groupedRows: Record<string, SlipPreviewRow[]> = {}
|
||||
const dateOrder: string[] = []
|
||||
for (const r of rows) {
|
||||
let dateKey = String(r.date ?? '').trim()
|
||||
if (!dateKey) dateKey = 'Unknown Date'
|
||||
if (!(dateKey in groupedRows)) {
|
||||
groupedRows[dateKey] = []
|
||||
dateOrder.push(dateKey)
|
||||
}
|
||||
groupedRows[dateKey].push(r)
|
||||
}
|
||||
const chartData = dateOrder.map((d) => groupedRows[d].length)
|
||||
const chartLabels = dateOrder.map((d) => formatPreviewDateKey(d))
|
||||
return { groupedRows, dateOrder, chartData, chartLabels }
|
||||
}
|
||||
|
||||
function slipPrintSearchParams(row: SlipPreviewRow): string {
|
||||
const fields = [
|
||||
'student_name',
|
||||
'date',
|
||||
'time_in',
|
||||
'grade',
|
||||
'reason',
|
||||
'school_year',
|
||||
'semester',
|
||||
'admin_name',
|
||||
] as const
|
||||
const qs = new URLSearchParams()
|
||||
for (const f of fields) {
|
||||
qs.set(f, String(row[f] ?? ''))
|
||||
}
|
||||
return qs.toString()
|
||||
}
|
||||
|
||||
export function SlipPreviewListPage() {
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
setError(null)
|
||||
fetchSlipPreviewList({
|
||||
school_year: schoolYear || undefined,
|
||||
semester: semester || undefined,
|
||||
})
|
||||
.then((d) => setRows(d.rows ?? []))
|
||||
.catch((e: unknown) =>
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Failed to load slips.'),
|
||||
)
|
||||
}, [schoolYear, semester])
|
||||
|
||||
const { groupedRows, dateOrder, chartData, chartLabels } = useMemo(
|
||||
() => groupRowsByDate(rows),
|
||||
[rows],
|
||||
)
|
||||
|
||||
const maxCount = Math.max(1, ...chartData)
|
||||
|
||||
function applyFilter(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
const fd = new FormData(e.currentTarget)
|
||||
const next = new URLSearchParams()
|
||||
const sy = String(fd.get('school_year') ?? '')
|
||||
const sem = String(fd.get('semester') ?? '')
|
||||
if (sy) next.set('school_year', sy)
|
||||
if (sem) next.set('semester', sem)
|
||||
setSearchParams(next)
|
||||
}
|
||||
|
||||
function openPrint(row: SlipPreviewRow) {
|
||||
const q = slipPrintSearchParams(row)
|
||||
window.open(`${SLIPS_PRINT_PATH}?${q}`, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
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">
|
||||
<div>
|
||||
<h2 className="mb-0">Late Slip Preview</h2>
|
||||
<small className="text-muted">View and print student late slips</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="row gy-2 gx-3 align-items-end mb-4" onSubmit={applyFilter}>
|
||||
<div className="col-md-4">
|
||||
<label className="form-label">School Year</label>
|
||||
<input
|
||||
type="text"
|
||||
name="school_year"
|
||||
className="form-control"
|
||||
defaultValue={schoolYear}
|
||||
placeholder="e.g. 2025-2026"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label className="form-label">Semester</label>
|
||||
<select name="semester" className="form-select" defaultValue={semester}>
|
||||
<option value="">All Semesters</option>
|
||||
<option value="fall">Fall</option>
|
||||
<option value="spring">Spring</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-2">
|
||||
<button type="submit" className="btn btn-primary w-100">
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="alert alert-light border">No slips found.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="card shadow-sm border-0">
|
||||
<div className="card-header bg-white py-3">
|
||||
<h5 className="card-title mb-0 fw-bold text-primary">Late Statistics by Date</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div style={{ width: '100%' }}>
|
||||
<div
|
||||
className="d-flex align-items-end gap-1 px-2 border-bottom"
|
||||
style={{ height: 240 }}
|
||||
>
|
||||
{chartLabels.map((label, i) => {
|
||||
const barH = Math.round((chartData[i] / maxCount) * 220)
|
||||
return (
|
||||
<div
|
||||
key={label + i}
|
||||
className="d-flex flex-column align-items-center flex-fill"
|
||||
style={{ maxWidth: 90 }}
|
||||
>
|
||||
<div
|
||||
className="w-100 rounded-top bg-primary bg-opacity-75"
|
||||
style={{
|
||||
height: barH,
|
||||
minHeight: chartData[i] > 0 ? 6 : 0,
|
||||
}}
|
||||
title={`${label}: ${chartData[i]}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="d-flex gap-1 px-2 mt-1 flex-wrap justify-content-between">
|
||||
{chartLabels.map((label, i) => (
|
||||
<div
|
||||
key={`lbl-${label}-${i}`}
|
||||
className="small text-muted text-center text-truncate flex-fill"
|
||||
style={{ fontSize: 10, maxWidth: 90 }}
|
||||
>
|
||||
{label}
|
||||
<span className="d-block fw-semibold text-dark">{chartData[i]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="small text-muted text-center mt-2">
|
||||
Recorded Dates · Total slips per day
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dateOrder.map((dateKey) => {
|
||||
const groupRows = groupedRows[dateKey] ?? []
|
||||
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>
|
||||
<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
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user