Kairo-Jinkendo/frontend/src/components/GateDependenciesSection.jsx
Lars 4f8725e234
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 2m17s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 13s
AP1.4d: Graph Engine mit blocked/ready Read Models.
Migration 020 fuer Kantenarten und Parallelgruppe; steering/graph berechnet Fortschritt; UI zeigt Bereit/Blockiert in Liste und Graph.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 07:59:18 +02:00

222 lines
6.8 KiB
JavaScript

import { Link } from 'react-router-dom'
import { gatePath } from '../utils/routes.js'
import { gateTitleById } from './GateSelect.jsx'
const DEPENDENCY_TYPE_LABELS = {
requires: 'Voraussetzung',
blocks: 'Blockiert',
related: 'Bezug',
parallel_group: 'Parallelgruppe',
optional_branch: 'Optional',
}
const OUTGOING_PHRASES = {
requires: 'benötigt',
blocks: 'blockiert',
related: 'Bezug zu',
}
const INCOMING_PHRASES = {
requires: 'Voraussetzung für',
blocks: 'Wird blockiert von',
related: 'Bezug von',
}
function groupDependencies(itemId, dependencies) {
/** @type {{ outgoing: typeof dependencies, incoming: typeof dependencies }} */
const groups = { outgoing: [], incoming: [] }
for (const dep of dependencies) {
if (dep.from_item_id === itemId) groups.outgoing.push(dep)
else if (dep.to_item_id === itemId) groups.incoming.push(dep)
}
return groups
}
function DependencyRow({ dep, itemId, siblingItems, direction, canManage, busy, onDelete }) {
const otherId = direction === 'outgoing' ? dep.to_item_id : dep.from_item_id
const otherTitle = gateTitleById(siblingItems, otherId) || otherId
const phrase =
direction === 'outgoing'
? OUTGOING_PHRASES[dep.dependency_type] || dep.dependency_type
: INCOMING_PHRASES[dep.dependency_type] || dep.dependency_type
return (
<li className="list-item card-list-item gate-dependency-item">
<div className="list-item-main">
<p className="gate-dependency-item__phrase">
{direction === 'outgoing' ? (
<>
Dieses Gate <strong>{phrase}</strong>{' '}
<Link to={gatePath(otherId)}>{otherTitle}</Link>
</>
) : (
<>
<strong>{phrase}</strong>{' '}
<Link to={gatePath(otherId)}>{otherTitle}</Link>
</>
)}
</p>
<p className="muted list-item-sub">
{DEPENDENCY_TYPE_LABELS[dep.dependency_type] || dep.dependency_type}
{dep.group_key ? ` · Gruppe ${dep.group_key}` : ''}
</p>
</div>
{canManage && (
<div className="list-item-meta action-controls">
<button
type="button"
className="btn btn-secondary btn-sm"
disabled={busy}
onClick={() => onDelete(dep.id)}
>
Entfernen
</button>
</div>
)}
</li>
)
}
export function GateDependenciesSection({
itemId,
siblingItems = [],
dependencies = [],
canManage,
busy,
onAdd,
onDelete,
}) {
const candidates = siblingItems.filter((item) => item.id !== itemId)
const { outgoing, incoming } = groupDependencies(itemId, dependencies)
const hasAny = outgoing.length > 0 || incoming.length > 0
async function handleAddSubmit(event) {
event.preventDefault()
const form = event.target
const toItemId = form.to_item_id.value
const dependencyType = form.dependency_type.value
const groupKey = form.group_key?.value?.trim()
if (!toItemId) return
const body = {
to_item_id: toItemId,
dependency_type: dependencyType,
}
if (dependencyType === 'parallel_group') {
if (!groupKey) return
body.group_key = groupKey
}
await onAdd(body)
form.reset()
}
return (
<section className="gate-dependencies-section">
<div className="section-header">
<div>
<h3>Abhängigkeiten</h3>
<p className="section-lead muted">
Kanten für den Gate-Graph ausgehende Abhängigkeiten kannst du hier anlegen und
entfernen.
</p>
</div>
</div>
{!hasAny && (
<p className="muted gate-dependencies-section__empty">
Noch keine Kanten im Graph erscheinen Gates sonst nach Reihenfolge.
</p>
)}
{outgoing.length > 0 && (
<>
<h4 className="gate-dependencies-section__group-title">Ausgehend</h4>
<ul className="item-list gate-dependency-list">
{outgoing.map((dep) => (
<DependencyRow
key={dep.id}
dep={dep}
itemId={itemId}
siblingItems={siblingItems}
direction="outgoing"
canManage={canManage}
busy={busy}
onDelete={onDelete}
/>
))}
</ul>
</>
)}
{incoming.length > 0 && (
<>
<h4 className="gate-dependencies-section__group-title">Eingehend</h4>
<ul className="item-list gate-dependency-list">
{incoming.map((dep) => (
<DependencyRow
key={dep.id}
dep={dep}
itemId={itemId}
siblingItems={siblingItems}
direction="incoming"
canManage={canManage}
busy={busy}
onDelete={onDelete}
/>
))}
</ul>
</>
)}
{canManage && candidates.length > 0 && (
<form className="inline-form-block gate-dependency-form" onSubmit={handleAddSubmit}>
<h4>Kante hinzufügen</h4>
<p className="muted form-hint gate-dependency-form__hint">
Dieses Gate
</p>
<label>
Beziehung
<select name="dependency_type" defaultValue="requires">
<option value="requires">benötigt (Voraussetzung)</option>
<option value="blocks">blockiert</option>
<option value="parallel_group">Parallelgruppe</option>
<option value="optional_branch">optionaler Zweig</option>
<option value="related">steht in Bezug zu</option>
</select>
</label>
<label className="gate-dependency-form__group-key">
Parallelgruppe (Schlüssel)
<input
name="group_key"
maxLength={128}
placeholder="z. B. phase-1"
/>
<span className="muted form-hint">Nur bei Kantenart Parallelgruppe.</span>
</label>
<label>
Anderes Gate
<select name="to_item_id" required defaultValue="">
<option value="" disabled>
Gate wählen
</option>
{candidates.map((item) => (
<option key={item.id} value={item.id}>
{item.title}
</option>
))}
</select>
</label>
<button type="submit" className="btn btn-primary" disabled={busy}>
Kante anlegen
</button>
</form>
)}
{canManage && candidates.length === 0 && (
<p className="muted gate-dependencies-section__empty">
Mindestens ein weiteres Gate im Vorhaben nötig, um Kanten zu definieren.
</p>
)}
</section>
)
}