48 lines
4.8 KiB
TypeScript
48 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { FormEvent, useEffect, useState } from 'react';
|
|
|
|
type Resource = 'users' | 'vehicles' | 'rides';
|
|
type Item = Record<string, string | number | boolean | null>;
|
|
|
|
async function adminRequest(path: string, init?: RequestInit) {
|
|
const response = await fetch(`/api/admin/${path}`, { ...init, headers: { 'content-type': 'application/json', ...(init?.headers ?? {}) } });
|
|
const body = await response.json().catch(() => null);
|
|
if (!response.ok) throw new Error(body?.detail ?? 'Admin request failed.');
|
|
return body;
|
|
}
|
|
|
|
export function AdminConsole() {
|
|
const [resource, setResource] = useState<Resource>('users');
|
|
const [items, setItems] = useState<Item[]>([]);
|
|
const [message, setMessage] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function load(selected = resource) {
|
|
setBusy(true); setMessage('');
|
|
try { const body = await adminRequest(`${selected}?page=1&page_size=50`); setItems(body.items ?? []); }
|
|
catch (error) { setItems([]); setMessage(error instanceof Error ? error.message : 'Unable to load admin data.'); }
|
|
finally { setBusy(false); }
|
|
}
|
|
|
|
useEffect(() => { void load(); }, [resource]);
|
|
|
|
async function create(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault(); setBusy(true); setMessage('');
|
|
const form = new FormData(event.currentTarget);
|
|
const payload = Object.fromEntries(form.entries());
|
|
try { await adminRequest(resource, { method: 'POST', body: JSON.stringify(payload) }); event.currentTarget.reset(); setMessage('Created.'); await load(); }
|
|
catch (error) { setMessage(error instanceof Error ? error.message : 'Create failed.'); }
|
|
finally { setBusy(false); }
|
|
}
|
|
|
|
async function archive(id: string) {
|
|
setBusy(true); setMessage('');
|
|
try { await adminRequest(`${resource}/${id}`, { method: 'DELETE' }); setMessage(resource === 'rides' ? 'Ride archived.' : 'Deleted.'); await load(); }
|
|
catch (error) { setMessage(error instanceof Error ? error.message : 'Action failed.'); }
|
|
finally { setBusy(false); }
|
|
}
|
|
|
|
return <div className="admin-console"><div className="admin-tabs">{(['users', 'vehicles', 'rides'] as Resource[]).map((value) => <button className={resource === value ? 'admin-tab active' : 'admin-tab'} key={value} onClick={() => setResource(value)} type="button">{value}</button>)}</div><form className="admin-create-form" onSubmit={create}>{resource === 'users' && <><input name="email" required placeholder="Email" type="email" /><input name="password" required minLength={12} placeholder="Temporary password" type="password" /><select defaultValue="user" name="role"><option value="user">User</option><option value="support">Support</option><option value="admin">Admin</option></select></>}{resource === 'vehicles' && <><input name="user_id" required placeholder="Owner user ID" /><input name="nickname" required placeholder="Nickname" /><input name="make" required placeholder="Make" /><input name="model" required placeholder="Model" /><input name="year" placeholder="Year" type="number" /></>}{resource === 'rides' && <><input name="user_id" required placeholder="Rider user ID" /><input name="vehicle_id" placeholder="Vehicle ID" /><input name="started_at" required type="datetime-local" /><input name="distance_miles" placeholder="Distance miles" type="number" step="0.1" /><input name="top_speed_mph" placeholder="Top speed MPH" type="number" step="0.1" /></>}<button className="button" disabled={busy} type="submit">Create {resource.slice(0, -1)}</button></form>{message && <p className="form-message" role="status">{message}</p>}<div className="admin-table-wrap"><table className="admin-table"><thead><tr>{resource === 'users' ? <><th>Email</th><th>Role</th><th>Status</th><th /></> : resource === 'vehicles' ? <><th>Nickname</th><th>Make / model</th><th>Owner</th><th /></> : <><th>Ride</th><th>Rider</th><th>Distance</th><th>Status</th><th /></>}</tr></thead><tbody>{items.map((item) => <tr key={String(item.id)}>{resource === 'users' ? <><td>{String(item.email)}</td><td>{String(item.role)}</td><td>{item.is_active ? 'Active' : 'Inactive'}</td><td><button className="text-button" onClick={() => void archive(String(item.id))} type="button">Deactivate</button></td></> : resource === 'vehicles' ? <><td>{String(item.nickname)}</td><td>{String(item.make)} {String(item.model)}</td><td>{String(item.user_id)}</td><td><button className="text-button" onClick={() => void archive(String(item.id))} type="button">Delete</button></td></> : <><td>{String(item.id).slice(0, 8)}</td><td>{String(item.user_id)}</td><td>{item.distance_miles == null ? '-' : `${item.distance_miles} mi`}</td><td>{String(item.status)}</td><td>{item.status !== 'archived' && <button className="text-button" onClick={() => void archive(String(item.id))} type="button">Archive</button>}</td></>}</tr>)}</tbody></table>{!busy && items.length === 0 && <p className="empty-state">No {resource} found.</p>}</div></div>;
|
|
}
|