Files
alrahma_web_client/src/pages/attendance/BadgeScanningListPage.tsx
T
2026-06-04 20:49:24 -04:00

187 lines
5.7 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import {
fetchBadgeScanningList,
type AttendanceManagementDashboard,
} from '../../api/attendanceManagement'
import { ApiHttpError } from '../../api/http'
const today = new Date().toISOString().slice(0, 10)
function nice(value: string | null | undefined): string {
if (!value) return '—'
return value.replace(/_/g, ' ')
}
export function BadgeScanningListPage() {
const [date, setDate] = useState(today)
const [status, setStatus] = useState('')
const [q, setQ] = useState('')
const [dashboard, setDashboard] = useState<AttendanceManagementDashboard | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const params = useMemo(
() => ({
date,
attendance_status: status,
q,
}),
[date, status, q],
)
async function load() {
setLoading(true)
setError(null)
try {
setDashboard(await fetchBadgeScanningList(params))
} catch (e) {
setError(e instanceof ApiHttpError ? e.message : 'Unable to load badge scanning list.')
} finally {
setLoading(false)
}
}
useEffect(() => {
void load()
}, [params])
const rows = dashboard?.rows ?? []
return (
<div className="container-fluid py-4">
<div className="d-flex flex-wrap justify-content-between gap-3 align-items-start mb-4">
<div>
<h1 className="h3 mb-1">Badge Scanning List</h1>
<p className="text-muted mb-0">
Students, staff, contractors, and visitors recorded through badge scan.
</p>
</div>
<button
className="btn btn-outline-secondary"
type="button"
onClick={() => void load()}
disabled={loading}
>
Refresh
</button>
</div>
{error ? <div className="alert alert-danger">{error}</div> : null}
<div className="card shadow-sm mb-4">
<div className="card-body">
<div className="row g-3 align-items-end">
<div className="col-md-3">
<label className="form-label">Date</label>
<input
className="form-control"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</div>
<div className="col-md-3">
<label className="form-label">Status</label>
<select
className="form-select"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="">All</option>
<option value="present">Present</option>
<option value="late">Late</option>
<option value="early_dismissal">Early dismissal</option>
</select>
</div>
<div className="col-md-4">
<label className="form-label">Search</label>
<input
className="form-control"
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Name, badge, grade, department"
/>
</div>
<div className="col-md-2">
<button
className="btn btn-primary w-100"
type="button"
onClick={() => void load()}
disabled={loading}
>
Search
</button>
</div>
</div>
</div>
</div>
<div className="card shadow-sm">
<div className="card-body">
<div className="d-flex justify-content-between align-items-center mb-3">
<h2 className="h5 mb-0">Scanned Badge Records</h2>
<span className="badge bg-secondary">{rows.length} records</span>
</div>
<div className="table-responsive">
<table className="table table-sm align-middle">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Grade/Dept</th>
<th>Badge ID</th>
<th>Date</th>
<th>Scan Time</th>
<th>Location</th>
<th>Status</th>
<th>Reported</th>
<th>Decision</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>{row.person_name || '—'}</td>
<td>{nice(row.person_type)}</td>
<td>{row.role_grade || '—'}</td>
<td>{row.badge_id || '—'}</td>
<td>{row.event_date}</td>
<td>{row.official_entry_time || row.official_exit_time || '—'}</td>
<td>{row.scan_location || row.exit_location || '—'}</td>
<td>{nice(row.attendance_status)}</td>
<td>{nice(row.report_status)}</td>
<td>{row.final_decision || '—'}</td>
</tr>
))}
{!loading && rows.length === 0 ? (
<tr>
<td colSpan={10} className="text-center text-muted py-4">
No badge scans found for this filter.
</td>
</tr>
) : null}
{loading ? (
<tr>
<td colSpan={10} className="text-center text-muted py-4">
Loading badge scans...
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}