"use client"; import { useMemo, useOptimistic, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import type { User } from "@/lib/types"; import { deleteUser, updateUser } from "@/app/actions"; import EditableCell from "./editable-cell"; import ConfirmDialog from "./confirm-dialog"; type Props = { initial: User[]; prefixPattern: string }; type SortDir = "asc" | "desc"; type SortKey = "f_username" | "last_update_time"; type OptimisticPatch = { f_username: string; field: keyof Pick; value: string; }; function timeOf(t: string | null) { if (!t) return 0; const ms = Date.parse(t); return Number.isNaN(ms) ? 0 : ms; } function sortUsers(rows: User[], key: SortKey, dir: SortDir, prefix: string): User[] { return [...rows].sort((a, b) => { const ap = a.f_username.startsWith(prefix); const bp = b.f_username.startsWith(prefix); if (ap && !bp) return -1; if (!ap && bp) return 1; if (key === "f_username") { return dir === "asc" ? a.f_username.localeCompare(b.f_username) : b.f_username.localeCompare(a.f_username); } return dir === "asc" ? timeOf(a.last_update_time) - timeOf(b.last_update_time) : timeOf(b.last_update_time) - timeOf(a.last_update_time); }); } function formatTime(t: string | null) { if (!t) return ; const d = new Date(t); if (Number.isNaN(d.getTime())) return t; return d.toLocaleString(undefined, { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } function DeleteButton({ label, onClick, }: { label: string; onClick: () => void; }) { return ( ); } export default function UsersTable({ initial, prefixPattern }: Props) { const router = useRouter(); const [sortKey, setSortKey] = useState("last_update_time"); const [sortDir, setSortDir] = useState("desc"); const [editingKey, setEditingKey] = useState(null); const [, startTransition] = useTransition(); const [refreshing, setRefreshing] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); const [deleteError, setDeleteError] = useState(null); const [optimistic, applyOptimistic] = useOptimistic( initial, (state, patch) => state.map((row) => row.f_username === patch.f_username ? { ...row, [patch.field]: patch.value } : row, ), ); const sorted = useMemo( () => sortUsers(optimistic, sortKey, sortDir, prefixPattern), [optimistic, sortKey, sortDir, prefixPattern], ); function saveCell( f_username: string, field: OptimisticPatch["field"], value: string, ) { return new Promise<{ ok: boolean; error?: string }>((resolve) => { startTransition(async () => { applyOptimistic({ f_username, field, value }); const row = initial.find((r) => r.f_username === f_username); if (!row) return resolve({ ok: false, error: "row not found" }); const next = { f_username: row.f_username, f_password: row.f_password, t_username: row.t_username, t_password: row.t_password, [field]: value, }; const result = await updateUser(next); resolve(result.ok ? { ok: true } : { ok: false, error: result.error }); }); }); } function refresh() { setRefreshing(true); startTransition(() => { router.refresh(); setTimeout(() => setRefreshing(false), 400); }); } function toggleSort(k: SortKey) { if (sortKey === k) setSortDir((d) => (d === "asc" ? "desc" : "asc")); else { setSortKey(k); setSortDir("desc"); } } async function confirmDelete() { if (!deleteTarget) return; setDeleting(true); setDeleteError(null); const result = await deleteUser(deleteTarget); setDeleting(false); if (result.ok) { setDeleteTarget(null); } else { setDeleteError(result.error); } } function HeaderTh({ k, label }: { k: SortKey; label: string }) { const active = sortKey === k; return ( ); } if (initial.length === 0) { return (

No users yet.

); } return (
{sorted.map((row) => { const k = (f: string) => `${row.f_username}::${f}`; return ( ); })}
From password To username To password
{row.f_username} setEditingKey(k("f_password"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.f_username, "f_password", v)} /> setEditingKey(k("t_username"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.f_username, "t_username", v)} /> setEditingKey(k("t_password"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.f_username, "t_password", v)} /> {formatTime(row.last_update_time)} { setDeleteError(null); setDeleteTarget(row.f_username); }} />
{sorted.map((row) => { const k = (f: string) => `${row.f_username}::${f}`; return (
{row.f_username}
{formatTime(row.last_update_time)} { setDeleteError(null); setDeleteTarget(row.f_username); }} />
setEditingKey(k("f_password"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.f_username, "f_password", v)} /> setEditingKey(k("t_username"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.f_username, "t_username", v)} /> setEditingKey(k("t_password"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.f_username, "t_password", v)} />
); })}
{ if (!deleting) setDeleteTarget(null); }} onConfirm={confirmDelete} title={`Delete ${deleteTarget ?? ""}?`} message={ <>

Permanently remove the user pairing for{" "} {deleteTarget} {" "} from the users table. This action cannot be undone and only removes the pairing — the underlying account row stays.

{deleteError && (

{deleteError}

)} } confirmLabel="Delete" destructive pending={deleting} />
); } function MobileRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function PageHead({ count, onRefresh, refreshing, }: { count: number; onRefresh: () => void; refreshing: boolean; }) { return (

Table

Users {count}

); }