feat(roadmaps): Ticket-Integration — Roadmap-Panel + Projekt-Kontextbox

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5 committed 2026-07-22 20:01:21 +00:00
1 parent 3cc6ab3343
commit 98601b4b3d
3 files changed
+203 -6

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { IconRoute, IconAlertTriangle } from '@tabler/icons-react'
import RoadmapProgressBar from './roadmap/RoadmapProgressBar'
import {
fetchRoadmapByTicketId,
fetchRoadmapNodes,
computeProgress,
isNodeOverdue,
NODE_KIND,
NODE_STATUS,
ROADMAP_STATUS,
} from '../lib/roadmaps'
/**
* Roadmap-Tracking im Projektticket: Fortschritt, Mini-Uebersicht und Link zum Board.
* Rendert nichts, wenn zum Ticket keine Roadmap existiert.
*/
export default function TicketRoadmapPanel({ ticket }) {
const [roadmap, setRoadmap] = useState(null)
const [nodes, setNodes] = useState([])
const [loaded, setLoaded] = useState(false)
useEffect(() => {
let cancelled = false
async function load() {
try {
const doc = await fetchRoadmapByTicketId(ticket.$id)
if (cancelled) return
setRoadmap(doc)
if (doc) setNodes(await fetchRoadmapNodes(doc.$id))
} catch {
/* Panel ist optional */
} finally {
if (!cancelled) setLoaded(true)
}
}
load()
return () => { cancelled = true }
}, [ticket.$id])
if (!loaded || !roadmap) return null
const progress = computeProgress(nodes)
const relevant = nodes.filter((n) => n.kind !== NODE_KIND.START)
const counts = {
open: relevant.filter((n) => (n.status || NODE_STATUS.OPEN) === NODE_STATUS.OPEN).length,
inProgress: relevant.filter((n) => n.status === NODE_STATUS.IN_PROGRESS).length,
done: relevant.filter((n) => n.status === NODE_STATUS.DONE).length,
}
const nextDue = relevant
.filter((n) => n.deadline && n.status !== NODE_STATUS.DONE)
.sort((a, b) => a.deadline.localeCompare(b.deadline))
.slice(0, 3)
const closedButActive = ticket.status === 'Closed' && roadmap.status === ROADMAP_STATUS.ACTIVE
return (
<div className="ui-card pad" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
<h5 style={{ fontWeight: 700, margin: 0, display: 'flex', alignItems: 'center', gap: 8 }}>
<IconRoute size={18} /> Roadmap: {roadmap.title}
</h5>
<Link to={`/roadmaps/${roadmap.$id}`} className="ui-btn">Zur Roadmap</Link>
</div>
<RoadmapProgressBar progress={progress} />
<div className="muted" style={{ display: 'flex', gap: 14, fontSize: 13, marginTop: 10, flexWrap: 'wrap' }}>
<span>Offen: {counts.open}</span>
<span>In Arbeit: {counts.inProgress}</span>
<span>Fertig: {counts.done}</span>
{roadmap.status === ROADMAP_STATUS.COMPLETED && <span style={{ color: '#34d399' }}>Plan abgeschlossen</span>}
</div>
{nextDue.length > 0 && (
<div style={{ marginTop: 10, fontSize: 13 }}>
<span className="faint">Naechste Deadlines: </span>
{nextDue.map((n, i) => (
<span key={n.$id} style={isNodeOverdue(n) ? { color: 'var(--danger, #ef4444)', fontWeight: 700 } : undefined}>
{i > 0 && ' · '}{n.title} ({n.deadline.split('-').reverse().join('.')})
</span>
))}
</div>
)}
{closedButActive && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, fontSize: 13, color: '#fbbf24' }}>
<IconAlertTriangle size={16} style={{ flexShrink: 0 }} />
Ticket ist geschlossen, aber der Plan ist erst zu {progress.percent}% fertig.
</div>
)}
</div>
)
}