Add run management screen to admin panel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 10:54:47 +01:00
parent 5cdcd149b6
commit 3b87397432
7 changed files with 114 additions and 4 deletions

View File

@@ -0,0 +1,11 @@
---
# nuzlocke-tracker-5wsn
title: Add run management to admin panel
status: completed
type: feature
priority: normal
created_at: 2026-02-08T09:53:01Z
updated_at: 2026-02-08T09:53:25Z
---
Create AdminRuns page with table listing all runs (name, game, status, started date) with delete functionality. Wire it into routing and admin navigation.

View File

@@ -1,10 +1,11 @@
--- ---
# nuzlocke-tracker-le62 # nuzlocke-tracker-le62
title: Add run management to admin panel title: Add run management to admin panel
status: todo status: completed
type: feature type: feature
priority: normal
created_at: 2026-02-07T21:09:13Z created_at: 2026-02-07T21:09:13Z
updated_at: 2026-02-07T21:09:13Z updated_at: 2026-02-08T09:54:13Z
--- ---
Add a runs screen to the admin panel that lists all runs and allows deletion. This provides a way to clean up test runs or remove unwanted runs without needing direct database access. Add a runs screen to the admin panel that lists all runs and allows deletion. This provides a way to clean up test runs or remove unwanted runs without needing direct database access.

View File

@@ -1,10 +1,11 @@
--- ---
# nuzlocke-tracker-w7o6 # nuzlocke-tracker-w7o6
title: Improve Run Creation Workflow title: Improve Run Creation Workflow
status: draft status: completed
type: feature type: feature
priority: normal
created_at: 2026-02-07T20:20:51Z created_at: 2026-02-07T20:20:51Z
updated_at: 2026-02-07T20:20:51Z updated_at: 2026-02-08T09:54:24Z
--- ---
Improve the run creation flow with better game filtering, box art display, and UX fixes. Improve the run creation flow with better game filtering, box art display, and UX fixes.

View File

@@ -8,6 +8,7 @@ import {
AdminPokemon, AdminPokemon,
AdminRouteDetail, AdminRouteDetail,
AdminEvolutions, AdminEvolutions,
AdminRuns,
} from './pages/admin' } from './pages/admin'
function App() { function App() {
@@ -27,6 +28,7 @@ function App() {
<Route path="games/:gameId/routes/:routeId" element={<AdminRouteDetail />} /> <Route path="games/:gameId/routes/:routeId" element={<AdminRouteDetail />} />
<Route path="pokemon" element={<AdminPokemon />} /> <Route path="pokemon" element={<AdminPokemon />} />
<Route path="evolutions" element={<AdminEvolutions />} /> <Route path="evolutions" element={<AdminEvolutions />} />
<Route path="runs" element={<AdminRuns />} />
</Route> </Route>
</Route> </Route>
</Routes> </Routes>

View File

@@ -4,6 +4,7 @@ const navItems = [
{ to: '/admin/games', label: 'Games' }, { to: '/admin/games', label: 'Games' },
{ to: '/admin/pokemon', label: 'Pokemon' }, { to: '/admin/pokemon', label: 'Pokemon' },
{ to: '/admin/evolutions', label: 'Evolutions' }, { to: '/admin/evolutions', label: 'Evolutions' },
{ to: '/admin/runs', label: 'Runs' },
] ]
export function AdminLayout() { export function AdminLayout() {

View File

@@ -0,0 +1,93 @@
import { useState, useMemo } from 'react'
import { AdminTable, type Column } from '../../components/admin/AdminTable'
import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal'
import { useRuns, useDeleteRun } from '../../hooks/useRuns'
import { useGames } from '../../hooks/useGames'
import type { NuzlockeRun } from '../../types/game'
export function AdminRuns() {
const { data: runs = [], isLoading: runsLoading } = useRuns()
const { data: games = [], isLoading: gamesLoading } = useGames()
const deleteRun = useDeleteRun()
const [deleting, setDeleting] = useState<NuzlockeRun | null>(null)
const gameMap = useMemo(
() => new Map(games.map((g) => [g.id, g.name])),
[games],
)
const columns: Column<NuzlockeRun>[] = [
{ header: 'Run Name', accessor: (r) => r.name, sortKey: (r) => r.name },
{
header: 'Game',
accessor: (r) => gameMap.get(r.gameId) ?? `Game #${r.gameId}`,
sortKey: (r) => gameMap.get(r.gameId) ?? '',
},
{
header: 'Status',
accessor: (r) => (
<span
className={
r.status === 'active'
? 'text-green-600 dark:text-green-400'
: r.status === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
}
>
{r.status}
</span>
),
sortKey: (r) => r.status,
},
{
header: 'Started',
accessor: (r) => new Date(r.startedAt).toLocaleDateString(),
sortKey: (r) => r.startedAt,
},
{
header: 'Actions',
accessor: (r) => (
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => setDeleting(r)}
className="text-red-600 hover:text-red-800 dark:text-red-400 text-sm"
>
Delete
</button>
</div>
),
},
]
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Runs</h2>
</div>
<AdminTable
columns={columns}
data={runs}
isLoading={runsLoading || gamesLoading}
emptyMessage="No runs yet."
keyFn={(r) => r.id}
/>
{deleting && (
<DeleteConfirmModal
title={`Delete "${deleting.name}"?`}
message="This will permanently delete the run and all its encounters."
onConfirm={() =>
deleteRun.mutate(deleting.id, {
onSuccess: () => setDeleting(null),
})
}
onCancel={() => setDeleting(null)}
isDeleting={deleteRun.isPending}
/>
)}
</div>
)
}

View File

@@ -3,3 +3,4 @@ export { AdminGameDetail } from './AdminGameDetail'
export { AdminPokemon } from './AdminPokemon' export { AdminPokemon } from './AdminPokemon'
export { AdminRouteDetail } from './AdminRouteDetail' export { AdminRouteDetail } from './AdminRouteDetail'
export { AdminEvolutions } from './AdminEvolutions' export { AdminEvolutions } from './AdminEvolutions'
export { AdminRuns } from './AdminRuns'