feat(roadmaps): Ticket-Integration — Roadmap-Panel + Projekt-Kontextbox
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3cc6ab3343
commit
98601b4b3d
3 files changed
+203
-6
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user