fix attendance
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user