feat(plan): Join-Gate im Gate-Designer für alle Archetypen
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Failing after 4m44s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Failing after 4m44s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Typ Join/Schaltknoten anlegbar, Detail ohne Verify, visuelle Kennzeichnung im Graph. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3a0ef3a734
commit
b80f52cb50
|
|
@ -3,6 +3,7 @@ import { gatePath } from '../utils/routes.js'
|
||||||
import {
|
import {
|
||||||
MILESTONE_STATUS_LABELS,
|
MILESTONE_STATUS_LABELS,
|
||||||
ROADMAP_ITEM_TYPE_LABELS,
|
ROADMAP_ITEM_TYPE_LABELS,
|
||||||
|
isJoinGateItem,
|
||||||
} from '../constants/status.js'
|
} from '../constants/status.js'
|
||||||
import { StatusBadge } from './StatusBadge.jsx'
|
import { StatusBadge } from './StatusBadge.jsx'
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ export function GateDesignerNodePanel({
|
||||||
}) {
|
}) {
|
||||||
if (!item) return null
|
if (!item) return null
|
||||||
|
|
||||||
|
const joinGate = isJoinGateItem(item)
|
||||||
const graphItem = graphState?.items?.[item.id]
|
const graphItem = graphState?.items?.[item.id]
|
||||||
|
|
||||||
const progress = criteriaProgress?.[item.id]
|
const progress = criteriaProgress?.[item.id]
|
||||||
|
|
@ -72,6 +74,13 @@ export function GateDesignerNodePanel({
|
||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
{joinGate && (
|
||||||
|
<p className="gate-designer-node-panel__topology muted">
|
||||||
|
Schaltknoten — schaltet, wenn alle Eingänge (requires) erreicht sind. Keine Checkliste,
|
||||||
|
kein manuelles Verify.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{item.goal_description && (
|
{item.goal_description && (
|
||||||
<p className="gate-designer-node-panel__goal muted">{item.goal_description}</p>
|
<p className="gate-designer-node-panel__goal muted">{item.goal_description}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,7 @@ export function GateGraphDesigner({
|
||||||
const nodeClass =
|
const nodeClass =
|
||||||
'gate-map-view__node gate-map-view__node--' +
|
'gate-map-view__node gate-map-view__node--' +
|
||||||
(item.status || 'planned') +
|
(item.status || 'planned') +
|
||||||
|
(item.item_type === 'join_gate' ? ' gate-map-view__node--join_gate' : '') +
|
||||||
(isSelected ? ' gate-graph-designer__node--selected' : '') +
|
(isSelected ? ' gate-graph-designer__node--selected' : '') +
|
||||||
(draggingNodeId === node.id ? ' gate-graph-designer__node--dragging' : '')
|
(draggingNodeId === node.id ? ' gate-graph-designer__node--dragging' : '')
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp, e
|
||||||
const nodeClass =
|
const nodeClass =
|
||||||
'gate-map-view__node gate-map-view__node--' +
|
'gate-map-view__node gate-map-view__node--' +
|
||||||
(item.status || 'planned') +
|
(item.status || 'planned') +
|
||||||
|
(item.item_type === 'join_gate' ? ' gate-map-view__node--join_gate' : '') +
|
||||||
(enforceBlocking && nodeGraph?.blocked ? ' gate-map-view__node--blocked' : '') +
|
(enforceBlocking && nodeGraph?.blocked ? ' gate-map-view__node--blocked' : '') +
|
||||||
(nodeGraph?.ready ? ' gate-map-view__node--ready' : '')
|
(nodeGraph?.ready ? ' gate-map-view__node--ready' : '')
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
MILESTONE_STATUSES,
|
MILESTONE_STATUSES,
|
||||||
MILESTONE_STATUS_LABELS,
|
MILESTONE_STATUS_LABELS,
|
||||||
|
ROADMAP_ITEM_TYPES_FOR_DESIGNER,
|
||||||
ROADMAP_ITEM_TYPE_LABELS,
|
ROADMAP_ITEM_TYPE_LABELS,
|
||||||
} from '../constants/status.js'
|
} from '../constants/status.js'
|
||||||
|
|
||||||
const ITEM_TYPES = ['milestone', 'review_gate', 'maturity_stage']
|
|
||||||
const EDITABLE_STATUSES = MILESTONE_STATUSES.filter(
|
const EDITABLE_STATUSES = MILESTONE_STATUSES.filter(
|
||||||
(s) => !['reached', 'moved', 'discarded'].includes(s),
|
(s) => !['reached', 'moved', 'discarded'].includes(s),
|
||||||
)
|
)
|
||||||
|
|
@ -17,6 +18,9 @@ export function RoadmapItemForm({
|
||||||
submitLabel = 'Anlegen',
|
submitLabel = 'Anlegen',
|
||||||
mode = 'create',
|
mode = 'create',
|
||||||
}) {
|
}) {
|
||||||
|
const [itemType, setItemType] = useState(initial.item_type || 'milestone')
|
||||||
|
const isJoinGate = itemType === 'join_gate'
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const form = e.target
|
const form = e.target
|
||||||
|
|
@ -27,7 +31,7 @@ export function RoadmapItemForm({
|
||||||
item_type: form.item_type.value,
|
item_type: form.item_type.value,
|
||||||
sequencing_mode: initial.sequencing_mode || 'sequential',
|
sequencing_mode: initial.sequencing_mode || 'sequential',
|
||||||
}
|
}
|
||||||
if (mode === 'create') {
|
if (mode === 'create' && !isJoinGate) {
|
||||||
const initialCriterion = form.initial_criterion_title?.value?.trim()
|
const initialCriterion = form.initial_criterion_title?.value?.trim()
|
||||||
if (initialCriterion) {
|
if (initialCriterion) {
|
||||||
payload.initial_criterion_title = initialCriterion
|
payload.initial_criterion_title = initialCriterion
|
||||||
|
|
@ -46,13 +50,23 @@ export function RoadmapItemForm({
|
||||||
{mode === 'create' && (
|
{mode === 'create' && (
|
||||||
<label>
|
<label>
|
||||||
Typ
|
Typ
|
||||||
<select name="item_type" defaultValue={initial.item_type || 'milestone'}>
|
<select
|
||||||
{ITEM_TYPES.map((t) => (
|
name="item_type"
|
||||||
|
value={itemType}
|
||||||
|
onChange={(e) => setItemType(e.target.value)}
|
||||||
|
>
|
||||||
|
{ROADMAP_ITEM_TYPES_FOR_DESIGNER.map((t) => (
|
||||||
<option key={t} value={t}>
|
<option key={t} value={t}>
|
||||||
{ROADMAP_ITEM_TYPE_LABELS[t] || t}
|
{ROADMAP_ITEM_TYPE_LABELS[t] || t}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{isJoinGate && (
|
||||||
|
<span className="muted form-hint">
|
||||||
|
Schaltknoten: sammelt Eingänge (AND) und schaltet Ausgänge — keine Übungen, kein
|
||||||
|
manuelles Verify.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<label>
|
<label>
|
||||||
|
|
@ -62,7 +76,9 @@ export function RoadmapItemForm({
|
||||||
defaultValue={initial.title || ''}
|
defaultValue={initial.title || ''}
|
||||||
maxLength={255}
|
maxLength={255}
|
||||||
required
|
required
|
||||||
placeholder="z. B. MVP nutzbar im Alltag"
|
placeholder={
|
||||||
|
isJoinGate ? 'z. B. Integration abgeschlossen' : 'z. B. MVP nutzbar im Alltag'
|
||||||
|
}
|
||||||
disabled={statusLocked}
|
disabled={statusLocked}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -72,14 +88,20 @@ export function RoadmapItemForm({
|
||||||
name="goal_description"
|
name="goal_description"
|
||||||
rows={3}
|
rows={3}
|
||||||
defaultValue={initial.goal_description || ''}
|
defaultValue={initial.goal_description || ''}
|
||||||
placeholder="Kurzer Kontext — prüfbare Akzeptanz legst du als Kriterien an."
|
placeholder={
|
||||||
|
isJoinGate
|
||||||
|
? 'Konsolidierter Meilenstein — welche Pfade laufen hier zusammen?'
|
||||||
|
: 'Kurzer Kontext — prüfbare Akzeptanz legst du als Kriterien an.'
|
||||||
|
}
|
||||||
disabled={statusLocked}
|
disabled={statusLocked}
|
||||||
/>
|
/>
|
||||||
|
{!isJoinGate && (
|
||||||
<span className="muted form-hint">
|
<span className="muted form-hint">
|
||||||
Die Checkliste auf der Gate-Detailseite ist führend für Verify.
|
Die Checkliste auf der Gate-Detailseite ist führend für Verify.
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
{mode === 'create' && (
|
{mode === 'create' && !isJoinGate && (
|
||||||
<label>
|
<label>
|
||||||
Erstes Kriterium (optional)
|
Erstes Kriterium (optional)
|
||||||
<input
|
<input
|
||||||
|
|
@ -104,6 +126,7 @@ export function RoadmapItemForm({
|
||||||
<p className="muted form-hint">
|
<p className="muted form-hint">
|
||||||
Reihenfolge und Parallelität steuerst du über Kanten auf der Gate-Detailseite — nicht mehr
|
Reihenfolge und Parallelität steuerst du über Kanten auf der Gate-Detailseite — nicht mehr
|
||||||
über ein Abfolge-Feld.
|
über ein Abfolge-Feld.
|
||||||
|
{isJoinGate && ' Eingänge: Join → Work-Gate (requires). Ausgänge: Work-Gate → Join.'}
|
||||||
</p>
|
</p>
|
||||||
{mode === 'edit' && (
|
{mode === 'edit' && (
|
||||||
<label>
|
<label>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,18 @@ export const ROADMAP_ITEM_TYPE_LABELS = {
|
||||||
join_gate: 'Join / Schaltknoten',
|
join_gate: 'Join / Schaltknoten',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Plan-Designer: anlegbare Gate-Typen (archetypübergreifend; später pro Archetyp konfigurierbar). */
|
||||||
|
export const ROADMAP_ITEM_TYPES_FOR_DESIGNER = [
|
||||||
|
'milestone',
|
||||||
|
'review_gate',
|
||||||
|
'maturity_stage',
|
||||||
|
'join_gate',
|
||||||
|
]
|
||||||
|
|
||||||
|
export function isJoinGateItem(item) {
|
||||||
|
return item?.item_type === 'join_gate'
|
||||||
|
}
|
||||||
|
|
||||||
export const SEQUENCING_MODE_LABELS = {
|
export const SEQUENCING_MODE_LABELS = {
|
||||||
sequential: 'Sequenziell',
|
sequential: 'Sequenziell',
|
||||||
parallel: 'Parallel',
|
parallel: 'Parallel',
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { StatusBadge } from '../../components/StatusBadge.jsx'
|
||||||
import {
|
import {
|
||||||
MILESTONE_STATUS_LABELS,
|
MILESTONE_STATUS_LABELS,
|
||||||
ROADMAP_ITEM_TYPE_LABELS,
|
ROADMAP_ITEM_TYPE_LABELS,
|
||||||
|
isJoinGateItem,
|
||||||
} from '../../constants/status.js'
|
} from '../../constants/status.js'
|
||||||
import { useCapabilities } from '../../hooks/useCapabilities.js'
|
import { useCapabilities } from '../../hooks/useCapabilities.js'
|
||||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||||
|
|
@ -143,6 +144,8 @@ export function RoadmapItemDetailPage({ overrideItemId }) {
|
||||||
if (error && !item) return <ErrorState message={error} onRetry={load} />
|
if (error && !item) return <ErrorState message={error} onRetry={load} />
|
||||||
if (!item) return <ErrorState message="Plan-Element nicht gefunden." onRetry={load} />
|
if (!item) return <ErrorState message="Plan-Element nicht gefunden." onRetry={load} />
|
||||||
|
|
||||||
|
const joinGate = isJoinGateItem(item)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card roadmap-item-detail">
|
<section className="card roadmap-item-detail">
|
||||||
<p className="breadcrumb muted">
|
<p className="breadcrumb muted">
|
||||||
|
|
@ -197,6 +200,21 @@ export function RoadmapItemDetailPage({ overrideItemId }) {
|
||||||
onDelete={(dependencyId) => runAction(() => deleteRoadmapDependency(dependencyId))}
|
onDelete={(dependencyId) => runAction(() => deleteRoadmapDependency(dependencyId))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{joinGate ? (
|
||||||
|
<section className="card join-gate-detail">
|
||||||
|
<h3>Join / Schaltknoten</h3>
|
||||||
|
<p className="muted">
|
||||||
|
Kein Activity Set und kein manuelles Verify. Der Join schaltet automatisch auf{' '}
|
||||||
|
<strong>Erreicht</strong>, wenn alle eingehenden Voraussetzungen (Kanten{' '}
|
||||||
|
<em>requires</em> von diesem Join zu Work-Gates) erfüllt sind — danach werden
|
||||||
|
ausgehende Gates freigegeben.
|
||||||
|
</p>
|
||||||
|
<p className="muted">
|
||||||
|
Eingang: Join benötigt Gate · Ausgang: Gate benötigt Join (unter Abhängigkeiten
|
||||||
|
konfigurieren).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
<GateCriteriaSection
|
<GateCriteriaSection
|
||||||
itemId={itemId}
|
itemId={itemId}
|
||||||
itemStatus={item.status}
|
itemStatus={item.status}
|
||||||
|
|
@ -207,9 +225,10 @@ export function RoadmapItemDetailPage({ overrideItemId }) {
|
||||||
onChanged={reloadCriteria}
|
onChanged={reloadCriteria}
|
||||||
onAction={runCriteriaAction}
|
onAction={runCriteriaAction}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="gate-detail-actions">
|
<div className="gate-detail-actions">
|
||||||
{canManage && ['planned', 'active', 'at_risk'].includes(item.status) && (
|
{canManage && !joinGate && ['planned', 'active', 'at_risk'].includes(item.status) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
|
|
|
||||||
|
|
@ -2046,6 +2046,13 @@
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gate-map-view__node--join_gate .gate-map-view__node-box {
|
||||||
|
stroke: #5b6ee1;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-dasharray: 6 4;
|
||||||
|
fill: #eef0ff;
|
||||||
|
}
|
||||||
|
|
||||||
.status-pill {
|
.status-pill {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.15rem 0.5rem;
|
padding: 0.15rem 0.5rem;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user