add grading, attendnace management
This commit is contained in:
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user