Kairo-Jinkendo/frontend/src/components/SteeringSnapshotPanel.jsx
Lars 0b57e2ec75 AP2.2b/c: A1-Stufenwechsel, Team-Fix, Agent-Anlage, Steuerungs-Hinweise.
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>
2026-07-12 16:19:55 +02:00

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>
)
}