95 lines
3.5 KiB
React
95 lines
3.5 KiB
React
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>
|
|
)
|
|
}
|