refactor(admin): split roadmap page.tsx into colocated components
Split 876-LOC page.tsx into 146 LOC with 7 colocated components (RoadmapCard, CreateRoadmapModal, CreateItemModal, ImportWizard, RoadmapDetailView split into header + items table), plus _types.ts, _constants.ts, and _api.ts. Behavior preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type { Roadmap, RoadmapItem, RoadmapStats } from '../_types'
|
||||
import { itemStatusLabels } from '../_constants'
|
||||
import { api } from '../_api'
|
||||
import { RoadmapDetailHeader } from './RoadmapDetailHeader'
|
||||
import { RoadmapItemsTable } from './RoadmapItemsTable'
|
||||
import { CreateItemModal } from './CreateItemModal'
|
||||
|
||||
export function RoadmapDetailView({ roadmap, onBack, onRefresh }: {
|
||||
roadmap: Roadmap
|
||||
onBack: () => void
|
||||
onRefresh: () => void
|
||||
}) {
|
||||
const [items, setItems] = useState<RoadmapItem[]>([])
|
||||
const [stats, setStats] = useState<RoadmapStats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showCreateItem, setShowCreateItem] = useState(false)
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all')
|
||||
const [filterPriority, setFilterPriority] = useState<string>('all')
|
||||
|
||||
const loadDetails = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [i, s] = await Promise.all([
|
||||
api<RoadmapItem[] | { items: RoadmapItem[] }>(`/${roadmap.id}/items`).catch(() => []),
|
||||
api<RoadmapStats>(`/${roadmap.id}/stats`).catch(() => null),
|
||||
])
|
||||
const itemList = Array.isArray(i) ? i : ((i as { items: RoadmapItem[] }).items || [])
|
||||
setItems(itemList)
|
||||
setStats(s)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [roadmap.id])
|
||||
|
||||
useEffect(() => { loadDetails() }, [loadDetails])
|
||||
|
||||
const handleStatusChange = async (itemId: string, newStatus: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/roadmap-items/${itemId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
loadDetails()
|
||||
} catch (err) {
|
||||
console.error('Status change error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteItem = async (itemId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/roadmap-items/${itemId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
setItems(prev => prev.filter(i => i.id !== itemId))
|
||||
} catch (err) {
|
||||
console.error('Delete item error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = items.filter(i => {
|
||||
if (filterStatus !== 'all' && i.status !== filterStatus) return false
|
||||
if (filterPriority !== 'all' && i.priority !== filterPriority) return false
|
||||
return true
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={onBack} className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 mb-4">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Zurueck zur Uebersicht
|
||||
</button>
|
||||
|
||||
<RoadmapDetailHeader
|
||||
roadmap={roadmap}
|
||||
stats={stats}
|
||||
onCreateItem={() => setShowCreateItem(true)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">Status:</span>
|
||||
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)}
|
||||
className="px-2 py-1 text-sm border rounded-lg">
|
||||
<option value="all">Alle</option>
|
||||
{Object.entries(itemStatusLabels).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">Prioritaet:</span>
|
||||
<select value={filterPriority} onChange={e => setFilterPriority(e.target.value)}
|
||||
className="px-2 py-1 text-sm border rounded-lg">
|
||||
<option value="all">Alle</option>
|
||||
<option value="CRITICAL">Kritisch</option>
|
||||
<option value="HIGH">Hoch</option>
|
||||
<option value="MEDIUM">Mittel</option>
|
||||
<option value="LOW">Niedrig</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-gray-500">Laden...</div>
|
||||
) : (
|
||||
<RoadmapItemsTable
|
||||
items={filteredItems}
|
||||
onStatusChange={handleStatusChange}
|
||||
onDelete={handleDeleteItem}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCreateItem && (
|
||||
<CreateItemModal roadmapId={roadmap.id} onClose={() => setShowCreateItem(false)}
|
||||
onCreated={() => { setShowCreateItem(false); loadDetails(); onRefresh() }} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user