720 lines
27 KiB
TypeScript
720 lines
27 KiB
TypeScript
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<string, unknown>, 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<string, unknown> {
|
|
const payload: Record<string, unknown> = {
|
|
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<string, unknown> {
|
|
const currentPayload = formToPayload(current)
|
|
if (!original) return currentPayload
|
|
|
|
const originalPayload = formToPayload(original)
|
|
const changed: Record<string, unknown> = {}
|
|
|
|
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<UserListRow[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [saveError, setSaveError] = useState<string | null>(null)
|
|
const [form, setForm] = useState<EditFormState | null>(null)
|
|
const [originalForm, setOriginalForm] = useState<EditFormState | null>(null)
|
|
const [saving, setSaving] = useState(false)
|
|
const modalRef = useRef<HTMLDivElement | null>(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<string, unknown>, 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 (
|
|
<div className="container-fluid py-3">
|
|
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
|
<h2 className="text-center mt-2 mb-0 flex-grow-1">User List</h2>
|
|
<Link className="btn btn-success btn-sm" to="/app/user/create">
|
|
Create user
|
|
</Link>
|
|
</div>
|
|
|
|
{error ? (
|
|
<div id="userListAlert" className="alert alert-danger" role="alert">
|
|
{error}
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="table-responsive">
|
|
<table className="display table table-striped table-bordered align-middle" style={{ width: '100%' }}>
|
|
<thead className="table-dark">
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>First Name</th>
|
|
<th>Last Name</th>
|
|
<th>Email</th>
|
|
<th>Phone</th>
|
|
<th>User Type</th>
|
|
<th>Roles</th>
|
|
<th style={{ width: 140 }}>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading ? (
|
|
<tr>
|
|
<td colSpan={8} className="text-center text-muted">
|
|
Loading users…
|
|
</td>
|
|
</tr>
|
|
) : rows.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={8} className="text-center text-muted">
|
|
No users found.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
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 (
|
|
<tr key={uid || idDisplay}>
|
|
<td>{dash(idDisplay)}</td>
|
|
<td>
|
|
{parent ? (
|
|
<Link className="text-decoration-none" to={familyHref}>
|
|
{dash(user.firstname)}
|
|
</Link>
|
|
) : (
|
|
dash(user.firstname)
|
|
)}
|
|
</td>
|
|
<td>
|
|
{parent ? (
|
|
<Link className="text-decoration-none" to={familyHref}>
|
|
{dash(user.lastname)}
|
|
</Link>
|
|
) : (
|
|
dash(user.lastname)
|
|
)}
|
|
</td>
|
|
<td>
|
|
{user.email ? (
|
|
<a className="text-decoration-none" href={`mailto:${String(user.email)}`}>
|
|
{String(user.email)}
|
|
</a>
|
|
) : (
|
|
'—'
|
|
)}
|
|
</td>
|
|
<td>
|
|
{user.cellphone ? (
|
|
<a className="text-decoration-none" href={`tel:${String(user.cellphone)}`}>
|
|
{String(user.cellphone)}
|
|
</a>
|
|
) : (
|
|
'—'
|
|
)}
|
|
</td>
|
|
<td>{dash(user.user_type)}</td>
|
|
<td>{roleLabels(entry)}</td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm btn-primary"
|
|
disabled={!uid}
|
|
onClick={() => openEdit(entry)}
|
|
>
|
|
<i className="fas fa-edit" aria-hidden /> Edit
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
)
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{form ? (
|
|
<div
|
|
className="modal fade"
|
|
id="editUserModal"
|
|
tabIndex={-1}
|
|
ref={modalRef}
|
|
aria-labelledby="editUserModalLabel"
|
|
aria-hidden="true"
|
|
>
|
|
<div className="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
|
<form className="modal-content" onSubmit={(e) => void onSubmit(e)}>
|
|
<div className="modal-header bg-primary text-white">
|
|
<h5 className="modal-title" id="editUserModalLabel">
|
|
{modalTitle}
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
className="btn-close btn-close-white"
|
|
aria-label="Close"
|
|
onClick={() => closeModal()}
|
|
/>
|
|
</div>
|
|
|
|
<div className="modal-body">
|
|
{saveError ? <div className="alert alert-danger">{saveError}</div> : null}
|
|
<div className="row g-3">
|
|
<div className="col-md-6">
|
|
<div className="mb-3">
|
|
<label className="form-label">Account ID</label>
|
|
<input
|
|
type="text"
|
|
name="account_id"
|
|
className="form-control"
|
|
value={form.account_id}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, account_id: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">First Name</label>
|
|
<input
|
|
type="text"
|
|
name="firstname"
|
|
className="form-control"
|
|
required
|
|
value={form.firstname}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, firstname: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Last Name</label>
|
|
<input
|
|
type="text"
|
|
name="lastname"
|
|
className="form-control"
|
|
required
|
|
value={form.lastname}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, lastname: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Gender</label>
|
|
<select
|
|
name="gender"
|
|
className="form-select"
|
|
value={form.gender}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, gender: e.target.value } : c))}
|
|
>
|
|
<option value="">—</option>
|
|
<option value="male">Male</option>
|
|
<option value="female">Female</option>
|
|
</select>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Phone</label>
|
|
<input
|
|
type="text"
|
|
name="cellphone"
|
|
className="form-control"
|
|
value={form.cellphone}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, cellphone: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Email</label>
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
className="form-control"
|
|
required
|
|
value={form.email}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, email: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Address Street</label>
|
|
<input
|
|
type="text"
|
|
name="address_street"
|
|
className="form-control"
|
|
value={form.address_street}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, address_street: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Apt</label>
|
|
<input
|
|
type="text"
|
|
name="apt"
|
|
className="form-control"
|
|
value={form.apt}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, apt: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">City</label>
|
|
<input
|
|
type="text"
|
|
name="city"
|
|
className="form-control"
|
|
value={form.city}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, city: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">State</label>
|
|
<input
|
|
type="text"
|
|
name="state"
|
|
maxLength={2}
|
|
className="form-control"
|
|
value={form.state}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, state: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">ZIP</label>
|
|
<input
|
|
type="text"
|
|
name="zip"
|
|
className="form-control"
|
|
value={form.zip}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, zip: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-md-6">
|
|
<div className="mb-3">
|
|
<label className="form-label d-block">Accept School Policy</label>
|
|
<div className="form-check form-switch">
|
|
<input
|
|
className="form-check-input"
|
|
type="checkbox"
|
|
checked={form.accept_school_policy}
|
|
onChange={(e) =>
|
|
setForm((c) => (c ? { ...c, accept_school_policy: e.target.checked } : c))
|
|
}
|
|
/>
|
|
<label className="form-check-label">Accepted</label>
|
|
</div>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">User Type</label>
|
|
<input
|
|
type="text"
|
|
name="user_type"
|
|
className="form-control"
|
|
value={form.user_type}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, user_type: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">RFID Tag</label>
|
|
<input
|
|
type="text"
|
|
name="rfid_tag"
|
|
className="form-control"
|
|
value={form.rfid_tag}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, rfid_tag: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">School ID</label>
|
|
<input
|
|
type="text"
|
|
name="school_id"
|
|
className="form-control"
|
|
value={form.school_id}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, school_id: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Failed Attempts</label>
|
|
<input
|
|
type="number"
|
|
name="failed_attempts"
|
|
className="form-control"
|
|
value={form.failed_attempts}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, failed_attempts: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Last Failed At</label>
|
|
<input
|
|
type="datetime-local"
|
|
name="last_failed_at"
|
|
className="form-control"
|
|
value={form.last_failed_at}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, last_failed_at: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Semester</label>
|
|
<input
|
|
type="text"
|
|
name="semester"
|
|
className="form-control"
|
|
value={form.semester}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, semester: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">School Year</label>
|
|
<input
|
|
type="text"
|
|
name="school_year"
|
|
className="form-control"
|
|
value={form.school_year}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, school_year: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Status</label>
|
|
<input
|
|
type="text"
|
|
name="status"
|
|
className="form-control"
|
|
value={form.status}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, status: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label d-block">Is Suspended</label>
|
|
<div className="form-check form-switch">
|
|
<input
|
|
className="form-check-input"
|
|
type="checkbox"
|
|
checked={form.is_suspended}
|
|
onChange={(e) =>
|
|
setForm((c) => (c ? { ...c, is_suspended: e.target.checked } : c))
|
|
}
|
|
/>
|
|
<label className="form-check-label">Suspended</label>
|
|
</div>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label d-block">Is Verified</label>
|
|
<div className="form-check form-switch">
|
|
<input
|
|
className="form-check-input"
|
|
type="checkbox"
|
|
checked={form.is_verified}
|
|
onChange={(e) =>
|
|
setForm((c) => (c ? { ...c, is_verified: e.target.checked } : c))
|
|
}
|
|
/>
|
|
<label className="form-check-label">Verified</label>
|
|
</div>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Token</label>
|
|
<input
|
|
type="text"
|
|
name="token"
|
|
className="form-control"
|
|
value={form.token}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, token: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Updated At</label>
|
|
<input
|
|
type="datetime-local"
|
|
name="updated_at"
|
|
className="form-control"
|
|
value={form.updated_at}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, updated_at: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<label className="form-label">Created At</label>
|
|
<input
|
|
type="datetime-local"
|
|
name="created_at"
|
|
className="form-control"
|
|
value={form.created_at}
|
|
onChange={(e) => setForm((c) => (c ? { ...c, created_at: e.target.value } : c))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="modal-footer">
|
|
<button type="button" className="btn btn-secondary" onClick={() => closeModal()}>
|
|
Cancel
|
|
</button>
|
|
<button type="submit" className="btn btn-primary" disabled={saving}>
|
|
{saving ? 'Saving…' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|