AP2.0e rotiert Recurring bei maturity_stage reached. Maturity-Strategie priorisiert Routinen. Team zeigt Namen und Agent-Formular. Archetyp-Hints in Kontrolle und Stufe-A-Gruppierung bei Anlage. Co-authored-by: Cursor <cursoragent@cursor.com>
244 lines
8.1 KiB
JavaScript
244 lines
8.1 KiB
JavaScript
import {
|
|
LIFECYCLE_LABELS,
|
|
SIGNAL_LABELS,
|
|
NEXT_ACTION_KIND_LABELS,
|
|
} from '../constants/operating.js'
|
|
import { MILESTONE_STATUS_LABELS } from '../constants/status.js'
|
|
import { methodDescriptionForKey } from '../utils/archetypes.js'
|
|
import { ArchetypeSteeringHints } from './ArchetypeSteeringHints.jsx'
|
|
import { Link } from 'react-router-dom'
|
|
import { scopedPath } from '../utils/routes.js'
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return '—'
|
|
try {
|
|
return new Date(iso).toLocaleDateString('de-DE')
|
|
} catch {
|
|
return iso
|
|
}
|
|
}
|
|
|
|
export function SteeringSnapshotPanel({
|
|
snapshot,
|
|
loading,
|
|
error,
|
|
methods = [],
|
|
canManageMethod = false,
|
|
onMethodChange,
|
|
methodBusy = false,
|
|
}) {
|
|
if (loading) {
|
|
return (
|
|
<section className="card steering-snapshot">
|
|
<p className="muted">Steuerungszustand wird geladen …</p>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
if (error || !snapshot) {
|
|
return (
|
|
<section className="card steering-snapshot steering-snapshot--unavailable">
|
|
<div className="section-header">
|
|
<h2>Steuerungszustand</h2>
|
|
</div>
|
|
<p className="muted">
|
|
{error || 'Snapshot nicht verfügbar — bitte Seite neu laden (Strg+F5).'}
|
|
</p>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
const {
|
|
lifecycle_state,
|
|
lifecycle_label,
|
|
method_key,
|
|
method_label,
|
|
archetype_key,
|
|
archetype_label,
|
|
steering_guidance,
|
|
attention_items = [],
|
|
signals = [],
|
|
counts,
|
|
upcoming_milestones = [],
|
|
next_actions = [],
|
|
active_work_cycle = null,
|
|
} = snapshot
|
|
const displayLabel =
|
|
lifecycle_label ||
|
|
LIFECYCLE_LABELS[lifecycle_state] ||
|
|
lifecycle_state
|
|
|
|
const statPills = [
|
|
{ label: 'Offen', value: counts.actions_open, warn: false },
|
|
{ label: 'Blockiert', value: counts.actions_blocked, warn: counts.actions_blocked > 0 },
|
|
{ label: 'Review', value: counts.actions_review_required, warn: counts.actions_review_required > 0 },
|
|
{ label: 'Blocker', value: counts.blockers_open, warn: counts.blockers_open > 0 },
|
|
]
|
|
|
|
const methodDescription = methodDescriptionForKey(method_key, methods)
|
|
|
|
function nextActionLink(item) {
|
|
if (item.action_id) {
|
|
return { to: `/actions/${item.action_id}`, label: 'Arbeitspaket öffnen' }
|
|
}
|
|
if (item.backlog_item_id && item.initiative_id) {
|
|
return {
|
|
to: scopedPath('/plan/inbox', { initiativeId: item.initiative_id }),
|
|
label: 'Zum Eingang',
|
|
}
|
|
}
|
|
if (item.kind === 'create_action' && item.initiative_id) {
|
|
return {
|
|
to: scopedPath('/plan/work', { initiativeId: item.initiative_id }),
|
|
label: 'Arbeitspaket anlegen',
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<section className="card steering-snapshot steering-snapshot--hero">
|
|
<header className="steering-hero-header">
|
|
<div>
|
|
<p className="steering-hero-kicker">Leitfrage</p>
|
|
<h2 className="steering-hero-title">
|
|
Wo stehe ich — und was ist als Nächstes dran?
|
|
</h2>
|
|
{steering_guidance && (
|
|
<p className="steering-guidance">{steering_guidance}</p>
|
|
)}
|
|
<ArchetypeSteeringHints archetypeKey={archetype_key} />
|
|
</div>
|
|
<span className="badge status-badge status-active steering-lifecycle-badge">
|
|
{displayLabel}
|
|
</span>
|
|
</header>
|
|
|
|
<div className="steering-hero-toolbar">
|
|
{(archetype_label || archetype_key) && (
|
|
<p className="steering-archetype-row muted">
|
|
Archetyp: <strong>{archetype_label || archetype_key}</strong>
|
|
</p>
|
|
)}
|
|
{(method_label || canManageMethod) && (
|
|
<p className="steering-method-row">
|
|
<span className="muted">Methode: </span>
|
|
{canManageMethod && methods.length > 0 ? (
|
|
<select
|
|
className="inline-select steering-method-select"
|
|
value={method_key || 'generic_operating'}
|
|
disabled={methodBusy}
|
|
onChange={(e) => onMethodChange?.(e.target.value)}
|
|
aria-label="Steuerungsmethode"
|
|
>
|
|
{methods.map((m) => (
|
|
<option key={m.key} value={m.key}>
|
|
{m.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<strong>{method_label || method_key}</strong>
|
|
)}
|
|
{methodDescription && (
|
|
<span className="steering-method-description muted">{methodDescription}</span>
|
|
)}
|
|
</p>
|
|
)}
|
|
|
|
{active_work_cycle && (
|
|
<p className="steering-work-cycle-row">
|
|
<span className="muted">Aktive Zeitbox: </span>
|
|
<strong>{active_work_cycle.title}</strong>
|
|
{active_work_cycle.goal_description && (
|
|
<span className="steering-method-description muted">
|
|
{' '}
|
|
— {active_work_cycle.goal_description}
|
|
</span>
|
|
)}
|
|
</p>
|
|
)}
|
|
|
|
<ul className="steering-stat-pills" aria-label="Kurzüberblick">
|
|
{statPills.map((pill) => (
|
|
<li
|
|
key={pill.label}
|
|
className={`steering-stat-pill${pill.warn ? ' steering-stat-pill--warn' : ''}`}
|
|
>
|
|
<span className="steering-stat-pill-value">{pill.value}</span>
|
|
<span className="steering-stat-pill-label">{pill.label}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
{(attention_items?.length > 0 || signals?.length > 0) && (
|
|
<p className="muted snapshot-signals snapshot-attention">
|
|
Aufmerksamkeit:{' '}
|
|
{(attention_items.length > 0
|
|
? attention_items.map((a) => a.label)
|
|
: signals.map((s) => SIGNAL_LABELS[s] || s)
|
|
).join(' · ')}
|
|
</p>
|
|
)}
|
|
|
|
<div className="steering-hero-columns">
|
|
{next_actions.length > 0 && (
|
|
<div className="steering-next-actions">
|
|
<h3>Nächste Schritte</h3>
|
|
<ol className="item-list compact-list">
|
|
{next_actions.map((item, i) => {
|
|
const link = nextActionLink(item)
|
|
return (
|
|
<li key={`${item.kind}-${i}`} className="list-item card-list-item compact">
|
|
<div className="list-item-main">
|
|
<span className="next-action-rank">{i + 1}</span>
|
|
<strong>{item.title}</strong>
|
|
<span className="muted list-item-sub">
|
|
{NEXT_ACTION_KIND_LABELS[item.kind] || item.kind}
|
|
{item.recommended_action ? ` — ${item.recommended_action}` : ''}
|
|
</span>
|
|
</div>
|
|
{link && (
|
|
<Link to={link.to} className="btn btn-primary btn-sm">
|
|
{link.label}
|
|
</Link>
|
|
)}
|
|
</li>
|
|
)})}
|
|
</ol>
|
|
</div>
|
|
)}
|
|
|
|
{upcoming_milestones.length > 0 && (
|
|
<div className="steering-milestones">
|
|
<h3>Meilenstein-Horizont</h3>
|
|
<ul className="item-list compact-list">
|
|
{upcoming_milestones.map((ms) => (
|
|
<li key={ms.id} className="list-item card-list-item compact">
|
|
<div className="list-item-main">
|
|
<strong>{ms.title}</strong>
|
|
<span className="muted list-item-sub">
|
|
{MILESTONE_STATUS_LABELS[ms.status] || ms.status}
|
|
{ms.target_date ? ` · Ziel: ${formatDate(ms.target_date)}` : ''}
|
|
</span>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{(counts.unlinked_blockers > 0 || counts.unlinked_evidence > 0) && (
|
|
<p className="steering-unlinked-hint muted">
|
|
{counts.unlinked_blockers > 0 && `${counts.unlinked_blockers} Blocker ohne Maßnahme`}
|
|
{counts.unlinked_blockers > 0 && counts.unlinked_evidence > 0 && ' · '}
|
|
{counts.unlinked_evidence > 0 && `${counts.unlinked_evidence} Nachweise ohne Maßnahme`}
|
|
{' — unter „Erweitert“ verknüpfen oder aufräumen.'}
|
|
</p>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|