2026-02-09 10:51:47 +01:00
|
|
|
import { useState, useMemo } from 'react'
|
|
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
|
import { AdminTable, type Column } from '../../components/admin/AdminTable'
|
|
|
|
|
import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal'
|
|
|
|
|
import { useGenlockes } from '../../hooks/useGenlockes'
|
|
|
|
|
import { useDeleteGenlocke } from '../../hooks/useAdmin'
|
|
|
|
|
import type { GenlockeListItem } from '../../types/game'
|
|
|
|
|
|
|
|
|
|
export function AdminGenlockes() {
|
|
|
|
|
const { data: genlockes = [], isLoading } = useGenlockes()
|
|
|
|
|
const deleteGenlocke = useDeleteGenlocke()
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
|
|
|
|
const [deleting, setDeleting] = useState<GenlockeListItem | null>(null)
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState('')
|
|
|
|
|
|
|
|
|
|
const filtered = useMemo(() => {
|
|
|
|
|
if (!statusFilter) return genlockes
|
|
|
|
|
return genlockes.filter((g) => g.status === statusFilter)
|
|
|
|
|
}, [genlockes, statusFilter])
|
|
|
|
|
|
|
|
|
|
const columns: Column<GenlockeListItem>[] = [
|
|
|
|
|
{ header: 'Name', accessor: (g) => g.name, sortKey: (g) => g.name },
|
|
|
|
|
{
|
|
|
|
|
header: 'Status',
|
|
|
|
|
accessor: (g) => (
|
|
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
g.status === 'active'
|
2026-02-17 20:48:42 +01:00
|
|
|
? 'text-status-active'
|
2026-02-09 10:51:47 +01:00
|
|
|
: g.status === 'completed'
|
2026-02-17 20:48:42 +01:00
|
|
|
? 'text-text-link'
|
|
|
|
|
: 'text-status-failed'
|
2026-02-09 10:51:47 +01:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{g.status}
|
|
|
|
|
</span>
|
|
|
|
|
),
|
|
|
|
|
sortKey: (g) => g.status,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
header: 'Legs',
|
|
|
|
|
accessor: (g) => `${g.completedLegs} / ${g.totalLegs}`,
|
|
|
|
|
sortKey: (g) => g.totalLegs,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
header: 'Created',
|
|
|
|
|
accessor: (g) => new Date(g.createdAt).toLocaleDateString(),
|
|
|
|
|
sortKey: (g) => g.createdAt,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex justify-between items-center mb-4">
|
|
|
|
|
<h2 className="text-xl font-semibold">Genlockes</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mb-4 flex items-center gap-4">
|
|
|
|
|
<select
|
|
|
|
|
value={statusFilter}
|
|
|
|
|
onChange={(e) => setStatusFilter(e.target.value)}
|
2026-02-17 20:48:42 +01:00
|
|
|
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
2026-02-09 10:51:47 +01:00
|
|
|
>
|
|
|
|
|
<option value="">All statuses</option>
|
|
|
|
|
<option value="active">Active</option>
|
|
|
|
|
<option value="completed">Completed</option>
|
|
|
|
|
<option value="failed">Failed</option>
|
|
|
|
|
</select>
|
|
|
|
|
{statusFilter && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setStatusFilter('')}
|
2026-02-17 20:48:42 +01:00
|
|
|
className="text-sm text-text-tertiary hover:text-text-primary"
|
2026-02-09 10:51:47 +01:00
|
|
|
>
|
|
|
|
|
Clear filters
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-02-17 20:48:42 +01:00
|
|
|
<span className="text-sm text-text-tertiary whitespace-nowrap">
|
2026-02-09 10:51:47 +01:00
|
|
|
{filtered.length} genlockes
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<AdminTable
|
|
|
|
|
columns={columns}
|
|
|
|
|
data={filtered}
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
emptyMessage="No genlockes found."
|
|
|
|
|
keyFn={(g) => g.id}
|
|
|
|
|
onRowClick={(g) => navigate(`/admin/genlockes/${g.id}`)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{deleting && (
|
|
|
|
|
<DeleteConfirmModal
|
|
|
|
|
title={`Delete "${deleting.name}"?`}
|
|
|
|
|
message="This will permanently delete the genlocke. Linked runs will be preserved but detached."
|
|
|
|
|
onConfirm={() =>
|
|
|
|
|
deleteGenlocke.mutate(deleting.id, {
|
|
|
|
|
onSuccess: () => setDeleting(null),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
onCancel={() => setDeleting(null)}
|
|
|
|
|
isDeleting={deleteGenlocke.isPending}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|