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>
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
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>
|
|
)
|
|
}
|