import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { fetchUsers, saveUser } from '../../api/session' import type { UserListRow } from '../../api/types' function dash(v: unknown): string { const s = (v ?? '').toString().trim() return s === '' ? '—' : s } function formatDateTimeLocal(value: unknown): string { if (value == null || value === '') return '' const normalized = String(value).trim().replace(' ', 'T') const date = new Date(normalized) if (Number.isNaN(date.valueOf())) { const match = normalized.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})/) return match ? `${match[1]}T${match[2]}` : '' } const pad = (n: number) => String(n).padStart(2, '0') return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}` } function roleLabels(row: UserListRow): string { const roles = row.roles if (!roles?.length) return 'No Role Assigned' const parts = roles .map((r) => { if (typeof r === 'string') return r.trim() return String(r.name ?? r.slug ?? '').trim() }) .filter(Boolean) return parts.length ? parts.join(', ') : 'No Role Assigned' } function isParentRole(row: UserListRow): boolean { return (row.roles ?? []).some((r) => { const s = typeof r === 'string' ? r.toLowerCase() : `${r.name ?? ''} ${r.slug ?? ''}`.toLowerCase() return s.includes('parent') }) } type EditFormState = { id: number account_id: string firstname: string lastname: string gender: string cellphone: string email: string address_street: string apt: string city: string state: string zip: string accept_school_policy: boolean user_type: string rfid_tag: string school_id: string failed_attempts: string last_failed_at: string semester: string school_year: string status: string is_suspended: boolean is_verified: boolean token: string updated_at: string created_at: string } function userToForm(user: Record, id: number): EditFormState { const g = (user.gender ?? '').toString().toLowerCase() return { id, account_id: String(user.account_id ?? ''), firstname: String(user.firstname ?? ''), lastname: String(user.lastname ?? ''), gender: g === 'male' || g === 'female' ? g : '', cellphone: String(user.cellphone ?? ''), email: String(user.email ?? ''), address_street: String(user.address_street ?? ''), apt: String(user.apt ?? ''), city: String(user.city ?? ''), state: String(user.state ?? ''), zip: String(user.zip ?? ''), accept_school_policy: Boolean( user.accept_school_policy === true || user.accept_school_policy === 1 || user.accept_school_policy === '1', ), user_type: String(user.user_type ?? ''), rfid_tag: String(user.rfid_tag ?? ''), school_id: String(user.school_id ?? ''), failed_attempts: String(user.failed_attempts ?? ''), last_failed_at: formatDateTimeLocal(user.last_failed_at), semester: String(user.semester ?? ''), school_year: String(user.school_year ?? ''), status: String(user.status ?? ''), is_suspended: Boolean( user.is_suspended === true || user.is_suspended === 1 || user.is_suspended === '1', ), is_verified: Boolean( user.is_verified === true || user.is_verified === 1 || user.is_verified === '1', ), token: String(user.token ?? ''), updated_at: formatDateTimeLocal(user.updated_at), created_at: formatDateTimeLocal(user.created_at), } } function formToPayload(f: EditFormState): Record { const payload: Record = { firstname: f.firstname.trim(), lastname: f.lastname.trim(), accept_school_policy: f.accept_school_policy ? 1 : 0, is_suspended: f.is_suspended ? 1 : 0, is_verified: f.is_verified ? 1 : 0, } const normalizedGender = f.gender.trim().toLowerCase() const normalizedUserType = f.user_type.trim().toLowerCase() const normalizedStatus = f.status.trim().toLowerCase() const assignIfPresent = (key: string, value: string, opts?: { nullable?: boolean }) => { const trimmed = value.trim() if (trimmed !== '') { payload[key] = trimmed return } if (opts?.nullable) { payload[key] = null } } assignIfPresent('account_id', f.account_id, { nullable: true }) assignIfPresent('cellphone', f.cellphone) assignIfPresent('email', f.email) assignIfPresent('address_street', f.address_street) assignIfPresent('apt', f.apt, { nullable: true }) assignIfPresent('city', f.city) assignIfPresent('state', f.state) assignIfPresent('zip', f.zip) assignIfPresent('rfid_tag', f.rfid_tag, { nullable: true }) assignIfPresent('last_failed_at', f.last_failed_at, { nullable: true }) assignIfPresent('semester', f.semester) assignIfPresent('school_year', f.school_year, { nullable: true }) assignIfPresent('token', f.token, { nullable: true }) if (normalizedGender === 'male' || normalizedGender === 'female') { payload.gender = normalizedGender } else if (normalizedGender === '') { payload.gender = null } if (normalizedUserType === 'primary' || normalizedUserType === 'secondary' || normalizedUserType === 'tertiary') { payload.user_type = normalizedUserType } if (normalizedStatus === 'active' || normalizedStatus === 'inactive') { payload.status = normalizedStatus === 'active' ? 'Active' : 'Inactive' } const schoolId = f.school_id.trim() if (schoolId !== '') { if (/^\d+$/.test(schoolId)) { payload.school_id = Number(schoolId) } } else { payload.school_id = null } const failedAttempts = f.failed_attempts.trim() payload.failed_attempts = failedAttempts === '' ? null : Number(failedAttempts) return payload } function buildChangedPayload( current: EditFormState, original: EditFormState | null, ): Record { const currentPayload = formToPayload(current) if (!original) return currentPayload const originalPayload = formToPayload(original) const changed: Record = {} for (const [key, value] of Object.entries(currentPayload)) { if (!Object.is(value, originalPayload[key])) { changed[key] = value } } return changed } /** CI `user/user_list.php` — DataTable-style list + full edit modal (`saveUser`). */ export function UserListPage() { const [rows, setRows] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [saveError, setSaveError] = useState(null) const [form, setForm] = useState(null) const [originalForm, setOriginalForm] = useState(null) const [saving, setSaving] = useState(false) const modalRef = useRef(null) async function load() { setLoading(true) try { const data = await fetchUsers({ sort: 'lastname', order: 'asc' }) setRows(data.users ?? []) setError(null) } catch (e) { setError(e instanceof Error ? e.message : 'Unable to load users.') } finally { setLoading(false) } } useEffect(() => { void load() }, []) const modalTitle = useMemo(() => { if (!form) return 'Edit User' const name = [form.firstname, form.lastname].filter(Boolean).join(' ').trim() return name ? `Edit User — ${name}` : 'Edit User' }, [form]) useEffect(() => { if (!form) return const el = modalRef.current const B = ( window as unknown as { bootstrap?: { Modal: { getOrCreateInstance: (n: HTMLElement) => { show: () => void } } } } ).bootstrap if (el && B?.Modal?.getOrCreateInstance) { B.Modal.getOrCreateInstance(el).show() } }, [form]) function openEdit(entry: UserListRow) { const user = entry.user ?? {} const id = Number(user.id ?? 0) if (!id) return const nextForm = userToForm(user as Record, id) setSaveError(null) setOriginalForm(nextForm) setForm(nextForm) } function closeModal() { const el = modalRef.current const Inst = ( window as unknown as { bootstrap?: { Modal: { getInstance: (n: HTMLElement) => { hide: () => void } | null } } } ).bootstrap?.Modal if (el && Inst?.getInstance) { Inst.getInstance(el)?.hide() } setForm(null) setOriginalForm(null) } async function onSubmit(e: FormEvent) { e.preventDefault() if (!form) return const payload = buildChangedPayload(form, originalForm) if (Object.keys(payload).length === 0) { setSaveError(null) closeModal() return } setSaving(true) setSaveError(null) try { await saveUser(form.id, payload) await load() closeModal() } catch (err) { setSaveError(err instanceof Error ? err.message : 'Save failed.') } finally { setSaving(false) } } return (

User List

Create user
{error ? ( ) : null}
{loading ? ( ) : rows.length === 0 ? ( ) : ( rows.map((entry) => { const user = entry.user ?? {} const uid = Number(user.id ?? 0) const idDisplay = user.school_id != null && String(user.school_id).trim() !== '' ? String(user.school_id) : String(user.id ?? '') const parent = isParentRole(entry) && uid > 0 const familyHref = `/app/administrator/family?guardian_id=${encodeURIComponent(String(uid))}` return ( ) }) )}
ID First Name Last Name Email Phone User Type Roles Actions
Loading users…
No users found.
{dash(idDisplay)} {parent ? ( {dash(user.firstname)} ) : ( dash(user.firstname) )} {parent ? ( {dash(user.lastname)} ) : ( dash(user.lastname) )} {user.email ? ( {String(user.email)} ) : ( '—' )} {user.cellphone ? ( {String(user.cellphone)} ) : ( '—' )} {dash(user.user_type)} {roleLabels(entry)}
{form ? ( ) : null}
) }