fix attendance

This commit is contained in:
root
2026-06-04 20:49:24 -04:00
parent dec023f03e
commit 647b96cafc
9 changed files with 650 additions and 94 deletions
+146
View File
@@ -0,0 +1,146 @@
import { apiFetch } from './http'
const BASE = '/api/v1/attendance/management'
type ApiEnvelope<T> = { status?: boolean; message?: string; data?: T }
function unwrap<T>(body: ApiEnvelope<T> | T): T {
if (body && typeof body === 'object' && 'data' in body) return (body as ApiEnvelope<T>).data as T
return body as T
}
export type AttendanceManagementSummary = {
present: number
absent: number
late: number
early_dismissal: number
not_reported: number
follow_up_required: number
badge_exceptions: number
late_slip_reprints: number
}
export type AttendanceManagementEvent = {
id: number
person_type: string
person_id: number | null
person_name: string | null
role_grade: string | null
badge_id: string | null
event_date: string
attendance_status: string
report_status: string
entry_time: string | null
exit_time: string | null
official_entry_time: string | null
official_exit_time: string | null
entry_method: string | null
exit_method: string | null
manual_reason: string | null
reason: string | null
scan_location: string | null
exit_location: string | null
absence_count: number
late_count: number
early_dismissal_count: number
badge_exception_count: number
combination_code: string | null
risk_level: string
follow_up_required: boolean | number
follow_up_completed: boolean | number
action_needed: string | null
final_decision: string | null
notes: string | null
}
export type AttendanceManagementDashboard = {
date: string
summary: AttendanceManagementSummary
filters: {
attendance_status: string[]
report_status: string[]
risk_level: string[]
person_type: string[]
}
rows: AttendanceManagementEvent[]
}
export type ManualEntryPayload = {
person_type?: string
person_id?: number
person_name?: string
role_grade?: string
badge_id?: string
entry_time?: string
manual_reason?: string
report_status?: string
reason?: string
notes?: string
}
export type ScanPayload = {
badge_scan?: string
badge_id?: string
scan_type?: 'entry' | 'exit'
scan_time?: string
location?: string
report_status?: string
authorized?: boolean
reason?: string
}
export async function fetchAttendanceManagementDashboard(params: Record<string, string> = {}) {
const qs = new URLSearchParams()
Object.entries(params).forEach(([k, v]) => {
if (v.trim()) qs.set(k, v.trim())
})
const suffix = qs.toString() ? `?${qs.toString()}` : ''
return unwrap<AttendanceManagementDashboard>(await apiFetch(`${BASE}/dashboard${suffix}`))
}
export async function fetchBadgeScanningList(params: Record<string, string> = {}) {
return fetchAttendanceManagementDashboard({
...params,
entry_method: 'badge_scan',
})
}
export async function recordManualAttendanceEntry(payload: ManualEntryPayload) {
return unwrap<{ event: AttendanceManagementEvent }>(
await apiFetch(`${BASE}/manual-entry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}),
)
}
export async function recordAttendanceBadgeScan(payload: ScanPayload) {
return unwrap<{ event: AttendanceManagementEvent }>(
await apiFetch(`${BASE}/scan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}),
)
}
export async function completeAttendanceFollowUp(id: number, payload: { report_status?: string; final_decision?: string; notes?: string }) {
return unwrap<{ event: AttendanceManagementEvent }>(
await apiFetch(`${BASE}/${id}/follow-up`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}),
)
}
export async function reprintAttendanceLateSlip(id: number, payload: { reason?: string; slip_number?: string } = {}) {
return unwrap<{ reprint: Record<string, unknown> }>(
await apiFetch(`${BASE}/${id}/late-slip-reprint`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}),
)
}
+3 -2
View File
@@ -381,7 +381,8 @@ export async function fetchStaffMonthlyAttendanceOverview(params?: {
if (params?.semester) query.set('semester', params.semester)
if (params?.schoolYear) query.set('school_year', params.schoolYear)
const qs = query.size > 0 ? `?${query.toString()}` : ''
return apiFetch<StaffMonthlyOverviewPayload>(`/api/v1/attendance/staff/month${qs}`)
const raw = await apiFetch<unknown>(`/api/v1/attendance/staff/month${qs}`)
return unwrapApiDataLayer(raw) as StaffMonthlyOverviewPayload
}
/** Matches CI `teacher_attendance_month` save cell (form body). */
@@ -392,7 +393,7 @@ export async function saveStaffMonthlyAttendanceCell(payload: Record<string, str
}> {
const body = new URLSearchParams()
for (const [k, v] of Object.entries(payload)) body.set(k, v)
return apiFetch('/api/v1/attendance/staff/month-save', {
return apiFetch('/api/v1/attendance/staff/cell', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),