Compare commits
No commits in common. "252b3f05f220436c85fce4b20c96e02afd15a689" and "84d912b8ba76e47452bc82fb32108e860ef735f4" have entirely different histories.
252b3f05f2
...
84d912b8ba
|
|
@ -51,25 +51,8 @@ docker compose -f docker-compose.dev-env.yml exec backend python run_seeds.py --
|
|||
| `seed_001_cleanup_pytest_artifacts` | SQL (dev) | Entfernt pytest/CI-User (`*@example.com`), verwaiste Test-Tenants; löscht zuerst Initiatives (AP0.5 FK) |
|
||||
| `seed_002_bootstrap_admin` | Python | Legt Systemadmin aus `KAIRO_BOOTSTRAP_*` an, wenn noch kein User existiert |
|
||||
| `seed_003_ensure_dev_admin` | Python (dev) | Entfernt fremde Dev-User, stellt `lars@stommer.com` als Portal-Admin sicher (jeder Start) |
|
||||
| `seed_004_dogfooding_kairo_jinkendo` | Python (dev) | Deprecated — Dogfooding-Visualisierung, **nicht** Produkt; nur Nicht-Prod |
|
||||
|
||||
Dev-Seeds (`.dev.*` im Dateinamen) laufen **nicht in Production** (`ENVIRONMENT=production`) und werden in Dev **bei jedem Backend-Start** erneut ausgeführt.
|
||||
|
||||
### Prod-taugliche Seeds (laufen auch auf main)
|
||||
|
||||
| Seed | Verhalten Prod |
|
||||
|------|----------------|
|
||||
| `seed_002_bootstrap_admin` | Legt Admin **nur** an, wenn DB leer **und** `KAIRO_BOOTSTRAP_*` in `.env` gesetzt |
|
||||
|
||||
### Code-Registry (kein Data-Seed — identisch Dev/Prod)
|
||||
|
||||
Beim Backend-Start (`entrypoint.sh`, überspringbar via `SKIP_REGISTRY_SYNC=1`):
|
||||
|
||||
- `sync_rights_registry.py` — Capabilities
|
||||
- `sync_prompt_feature_config.py` — Features/Prompts
|
||||
- `sync_entity_field_registry.py` — Archetypen + Felddefinitionen aus `entity_archetypes/registry.py`
|
||||
|
||||
Zusätzlich im Code (Deploy-Artifact, keine DB-Seed-Datei): `method_profiles/registry.py`, `steering/methods/registrations/*`, Starter-Kits in `archetype_starter_kit.py`.
|
||||
Dev-Seeds (`.dev.*`) werden in Nicht-Prod-Umgebungen **bei jedem Backend-Start** erneut ausgeführt.
|
||||
|
||||
## Neuen Seed anlegen
|
||||
|
||||
|
|
|
|||
|
|
@ -71,14 +71,8 @@ Slots sind **stabil benannte Flächen** — unabhängig von React-Page-Struktur.
|
|||
| `plan.inbox.proposals` | plan | inbox | Triage-Vorschläge |
|
||||
| `plan.sprint.proposals` | plan | sprint | Sprint-Commit-Vorschläge |
|
||||
| `plan.gates.proposals` | plan | gates | Gate-Vorschläge |
|
||||
| `work.today.primary` | work | today | Next Action (Steuerung) |
|
||||
| `work.today.actions` | work | today | Arbeitspakete (Drill-down) |
|
||||
| `work.sprint.primary` | work | sprint | Next Action im Sprint |
|
||||
| `work.sprint.backlog` | work | sprint | Sprint-Backlog (Drill-down) |
|
||||
|
||||
**Erweiterung (post-MVP):** `control.plan-ist.read_models`, `plan.inbox.read_models`.
|
||||
|
||||
**AP1.9d (2026-07-27):** Work-Modus-Slots `work.today.*`, `work.sprint.*` — Next Action als Hauptbild, Arbeitspaket-Listen als Drill-down. Layout-neutral für späteres UI-Redesign; Provider-Vertrag bleibt stabil.
|
||||
**Erweiterung (post-MVP):** `control.plan-ist.read_models`, `plan.inbox.read_models`, Work-Modus-Slots.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -232,7 +232,6 @@ Details: `Kairo_Status_Review_and_Next_Steps_v0.1.md` §2.1
|
|||
| Steering Kernel Proposals | ✓ | sprint_commit · gate_next_actions · intake_triage (Kernel v0.4) |
|
||||
| AP2.2b | A2 Linear E2E ✓ (2026-07-27) |
|
||||
| AP2.2c | A1 Reifegrad E2E ✓ (2026-07-27) |
|
||||
| AP1.9d | Work-Composition + Drill-down ◐→✓ (2026-07-27) |
|
||||
| AP2.2c–e | Referenz-Archetypen End-to-End ◐→✓ |
|
||||
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
||||
| AP1.7b | Op-API Parität |
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ Phase 0 DOC + Kurskorrektur (dieses Dokument) ✓
|
|||
Phase 0b AP2.3/AP2.4 Plugin-Architektur (Archetyp ↔ Methode) ✓
|
||||
Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ✓
|
||||
Phase 1b Specs Welle 1 finalisieren (A1,A2,B2a,B2b,B3) ◐
|
||||
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐→✓ (Work-Composition + Drill-down)
|
||||
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐ (Hints + Recurring)
|
||||
Phase 3 AP2.2b A2 Linear End-to-End ✓
|
||||
AP2.2c A1 Reifegrad + AP2.0e ✓
|
||||
AP2.2d B2b Product + B3 Sprint ✓
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ export function BacklogSection({
|
|||
busy,
|
||||
sectionTitle = 'Product Backlog',
|
||||
sectionLead = 'Eingang vor dem Commit — triagieren, dann in den Sprint planen.',
|
||||
defaultSprintTargetId = null,
|
||||
lockSprintTarget = false,
|
||||
}) {
|
||||
const { operatingContext, steeringElements, steeringSnapshot } = useInitiativeOperations()
|
||||
const [modalMode, setModalMode] = useState(null)
|
||||
|
|
@ -76,13 +74,6 @@ export function BacklogSection({
|
|||
if (!showSprintPlanning || !initiativeId) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
defaultSprintTargetId
|
||||
&& planableSprints.some((cycle) => cycle.id === defaultSprintTargetId)
|
||||
) {
|
||||
setSprintTargetId(defaultSprintTargetId)
|
||||
return
|
||||
}
|
||||
const stored = sessionStorage.getItem(SPRINT_TARGET_STORAGE_PREFIX + initiativeId)
|
||||
if (stored && planableSprints.some((cycle) => cycle.id === stored)) {
|
||||
setSprintTargetId(stored)
|
||||
|
|
@ -93,13 +84,7 @@ export function BacklogSection({
|
|||
}
|
||||
const preferred = activeWorkCycle?.id || planableSprints[0]?.id || ''
|
||||
setSprintTargetId(preferred)
|
||||
}, [
|
||||
showSprintPlanning,
|
||||
initiativeId,
|
||||
planableSprints,
|
||||
activeWorkCycle?.id,
|
||||
defaultSprintTargetId,
|
||||
])
|
||||
}, [showSprintPlanning, initiativeId, planableSprints, activeWorkCycle?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId || !sprintTargetId) return
|
||||
|
|
@ -307,16 +292,6 @@ export function BacklogSection({
|
|||
|
||||
{showSprintPlanning && canManage && (
|
||||
<div className="backlog-sprint-planning card-list-item">
|
||||
{lockSprintTarget && selectedSprint ? (
|
||||
<p className="muted backlog-sprint-planning__locked">
|
||||
Ziel-Sprint: <strong>{selectedSprint.title}</strong>
|
||||
{selectedSprint.status === 'active'
|
||||
? ' (aktiv)'
|
||||
: selectedSprint.status === 'planned'
|
||||
? ' (geplant)'
|
||||
: ''}
|
||||
</p>
|
||||
) : (
|
||||
<label className="backlog-sprint-planning__field">
|
||||
<span className="muted">Ziel-Sprint</span>
|
||||
<select
|
||||
|
|
@ -327,16 +302,11 @@ export function BacklogSection({
|
|||
{planableSprints.map((cycle) => (
|
||||
<option key={cycle.id} value={cycle.id}>
|
||||
{cycle.title}
|
||||
{cycle.status === 'active'
|
||||
? ' (aktiv)'
|
||||
: cycle.status === 'planned'
|
||||
? ' (geplant)'
|
||||
: ''}
|
||||
{cycle.status === 'active' ? ' (aktiv)' : cycle.status === 'planned' ? ' (geplant)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{selectedCommittable.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,55 +1,29 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* Generischer Drill-down-Container — AP1.9d Anti-Todo-Wand.
|
||||
* Bewusst layout-neutral für späteres UI-Redesign.
|
||||
*/
|
||||
export function CollapsibleSection({
|
||||
title,
|
||||
lead,
|
||||
defaultCollapsed = true,
|
||||
expandLabel = 'Alle anzeigen',
|
||||
collapseLabel = 'Einklappen',
|
||||
headerActions = null,
|
||||
className = 'card',
|
||||
summary,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
className = '',
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(!defaultCollapsed)
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
|
||||
return (
|
||||
<section className={className}>
|
||||
<div className="section-header">
|
||||
<div>
|
||||
{title && <h2>{title}</h2>}
|
||||
{lead && <p className="section-lead muted">{lead}</p>}
|
||||
</div>
|
||||
{headerActions}
|
||||
</div>
|
||||
|
||||
{expanded ? (
|
||||
<>
|
||||
{children}
|
||||
<p className="collapsible-section__toggle">
|
||||
<section className={`collapsible-section card ${className}`.trim()}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => setExpanded(false)}
|
||||
className="collapsible-section-trigger"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
{collapseLabel}
|
||||
<span className="collapsible-section-title">{title}</span>
|
||||
{summary && <span className="collapsible-section-summary muted">{summary}</span>}
|
||||
<span className="collapsible-section-chevron" aria-hidden="true">
|
||||
{open ? '▾' : '▸'}
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="collapsible-section__toggle">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => setExpanded(true)}
|
||||
>
|
||||
{expandLabel}
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
{open && <div className="collapsible-section-body">{children}</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ActionHubCard } from './ActionHubCard.jsx'
|
||||
import { ActionForm } from './ActionForm.jsx'
|
||||
import { CollapsibleSection } from './CollapsibleSection.jsx'
|
||||
import { EmptyState } from './EmptyState.jsx'
|
||||
|
||||
export function InitiativeActionsHub({
|
||||
|
|
@ -35,12 +34,14 @@ export function InitiativeActionsHub({
|
|||
sectionTitle = 'Arbeitspakete',
|
||||
sectionLead = 'Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews hängen am Arbeitspaket.',
|
||||
allActions = [],
|
||||
collapsible = false,
|
||||
defaultCollapsed = true,
|
||||
expandLabel = 'Alle anzeigen',
|
||||
collapseLabel = 'Liste einklappen',
|
||||
}) {
|
||||
const headerActions = (
|
||||
return (
|
||||
<section className="card initiative-actions-hub">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>{sectionTitle}</h2>
|
||||
<p className="section-lead muted">{sectionLead}</p>
|
||||
</div>
|
||||
<div className="section-actions">
|
||||
<label className="checkbox-label inline-filter">
|
||||
<input
|
||||
|
|
@ -60,10 +61,8 @@ export function InitiativeActionsHub({
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
|
||||
const body = (
|
||||
<>
|
||||
{showForm && canManage && (
|
||||
<div className="inline-form-block">
|
||||
<ActionForm
|
||||
|
|
@ -119,35 +118,6 @@ export function InitiativeActionsHub({
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
if (collapsible) {
|
||||
return (
|
||||
<CollapsibleSection
|
||||
className="card initiative-actions-hub"
|
||||
title={sectionTitle}
|
||||
lead={sectionLead}
|
||||
defaultCollapsed={defaultCollapsed}
|
||||
expandLabel={expandLabel}
|
||||
collapseLabel={collapseLabel}
|
||||
headerActions={headerActions}
|
||||
>
|
||||
{body}
|
||||
</CollapsibleSection>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card initiative-actions-hub">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>{sectionTitle}</h2>
|
||||
<p className="section-lead muted">{sectionLead}</p>
|
||||
</div>
|
||||
{headerActions}
|
||||
</div>
|
||||
{body}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,6 @@ export function InitiativeCompositionSurface({
|
|||
handleMethodChange: ops.handleMethodChange,
|
||||
methodBusy: ops.methodBusy,
|
||||
formBusy: ops.formBusy,
|
||||
activeWorkCycle: ops.activeWorkCycle,
|
||||
uiFeatures: ops.uiFeatures,
|
||||
dataSlices: ops.dataSlices,
|
||||
onAcceptBacklogProposal,
|
||||
}),
|
||||
[
|
||||
|
|
@ -49,9 +46,6 @@ export function InitiativeCompositionSurface({
|
|||
ops.handleMethodChange,
|
||||
ops.methodBusy,
|
||||
ops.formBusy,
|
||||
ops.activeWorkCycle,
|
||||
ops.uiFeatures,
|
||||
ops.dataSlices,
|
||||
onAcceptBacklogProposal,
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import { useInitiativeOperations } from '../context/InitiativeOperationsContext.jsx'
|
||||
import { InitiativeActionsHub } from '../components/InitiativeActionsHub.jsx'
|
||||
import { EmptyState } from '../components/EmptyState.jsx'
|
||||
import { PRODUCT_TERMS, actionCountLabel } from '../constants/productLanguage.js'
|
||||
|
||||
/**
|
||||
* Composition-Provider: committete Arbeitspakete als Drill-down (nicht Hauptbild).
|
||||
* @param {{ variant?: 'today' | 'sprint' | 'sprint-product', defaultCollapsed?: boolean }} props
|
||||
*/
|
||||
export function WorkActionsPanel({ variant = 'today', defaultCollapsed = true }) {
|
||||
const ops = useInitiativeOperations()
|
||||
const {
|
||||
initiativeId,
|
||||
actions,
|
||||
visibleActions,
|
||||
sprintActions,
|
||||
activeWorkCycle,
|
||||
uiFeatures,
|
||||
dataSlices,
|
||||
projects,
|
||||
roadmapItems,
|
||||
actionContextById,
|
||||
hideDone,
|
||||
setHideDone,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingActionId,
|
||||
setEditingActionId,
|
||||
formBusy,
|
||||
actors,
|
||||
actorsLoading,
|
||||
actorsError,
|
||||
actorsUsedFallback,
|
||||
reloadActors,
|
||||
handleCreateAction,
|
||||
handleUpdateAction,
|
||||
handleQuickStatus,
|
||||
handleCreateBlockerForAction,
|
||||
handleMoveActionToSprint,
|
||||
handleUnplanAction,
|
||||
capabilities,
|
||||
workCycles,
|
||||
} = ops
|
||||
|
||||
const supportsWorkCycles = dataSlices.includes('work_cycles')
|
||||
const isProductMode = uiFeatures.continuousProductWorkMode && supportsWorkCycles
|
||||
|
||||
let panelActions = visibleActions
|
||||
let sectionTitle = PRODUCT_TERMS.actionPlural
|
||||
let sectionLead =
|
||||
'Committete operative Einheit — nur bei Bedarf die volle Liste öffnen.'
|
||||
|
||||
if (variant === 'sprint' && activeWorkCycle) {
|
||||
panelActions = sprintActions
|
||||
sectionTitle = PRODUCT_TERMS.sprintBacklog
|
||||
sectionLead = `Arbeitspakete im ${PRODUCT_TERMS.workCycle} „${activeWorkCycle.title}“.`
|
||||
} else if (variant === 'sprint-product' && isProductMode && !activeWorkCycle) {
|
||||
panelActions = visibleActions
|
||||
sectionTitle = `Committete ${PRODUCT_TERMS.actionPlural}`
|
||||
sectionLead = `Ohne aktiven ${PRODUCT_TERMS.workCycle} — Product-Ist im Vorhaben.`
|
||||
} else if (variant === 'sprint' && !activeWorkCycle && !isProductMode) {
|
||||
return (
|
||||
<EmptyState
|
||||
message={`${PRODUCT_TERMS.sprintBacklog} ist für diesen Vorhabentyp nicht verfügbar.`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const openCount = panelActions.filter((a) => a.status !== 'done' && a.status !== 'discarded').length
|
||||
const expandLabel =
|
||||
openCount > 0
|
||||
? `${actionCountLabel(openCount)} anzeigen`
|
||||
: `Alle ${PRODUCT_TERMS.actionPlural} anzeigen`
|
||||
|
||||
return (
|
||||
<InitiativeActionsHub
|
||||
initiativeId={initiativeId}
|
||||
actions={panelActions}
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
actionContextById={actionContextById}
|
||||
hideDone={hideDone}
|
||||
onHideDoneChange={setHideDone}
|
||||
showForm={showActionForm}
|
||||
onToggleForm={() => setShowActionForm((v) => !v)}
|
||||
editingActionId={editingActionId}
|
||||
onEditAction={setEditingActionId}
|
||||
onCancelEdit={() => setEditingActionId(null)}
|
||||
onCreateAction={handleCreateAction}
|
||||
onUpdateAction={handleUpdateAction}
|
||||
onQuickStatus={handleQuickStatus}
|
||||
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||
onMoveToSprint={handleMoveActionToSprint}
|
||||
onUnplanToBacklog={handleUnplanAction}
|
||||
canManage={capabilities.has('kairo.action.manage')}
|
||||
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||
formBusy={formBusy}
|
||||
actors={actors}
|
||||
actorsLoading={actorsLoading}
|
||||
actorsError={actorsError}
|
||||
actorsUsedFallback={actorsUsedFallback}
|
||||
onReloadActors={reloadActors}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
allActions={actions}
|
||||
sectionTitle={sectionTitle}
|
||||
sectionLead={sectionLead}
|
||||
collapsible
|
||||
defaultCollapsed={defaultCollapsed}
|
||||
expandLabel={expandLabel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ import { WIDGETS } from '../registry/widgetRegistry.js'
|
|||
* @property {string} [proposalKey]
|
||||
* @property {string} [widgetKey]
|
||||
* @property {string} [predicateKey]
|
||||
* @property {string} [workActionsVariant]
|
||||
*/
|
||||
|
||||
const WIDGET_PROVIDERS = WIDGETS.filter((w) => w.area === 'workspace').map((widget) => ({
|
||||
|
|
@ -82,7 +81,7 @@ export const COMPOSITION_PROVIDERS = [
|
|||
key: 'steering.next_action',
|
||||
kind: 'steering_element',
|
||||
steeringElement: 'next_action_primary',
|
||||
slotKeys: ['control.status.steering', 'work.today.primary', 'work.sprint.primary'],
|
||||
slotKeys: ['control.status.steering'],
|
||||
componentKey: 'NextActionWidget',
|
||||
requiresCapability: 'kairo.workspace.read',
|
||||
scopeTypes: ['initiative'],
|
||||
|
|
@ -138,40 +137,6 @@ export const COMPOSITION_PROVIDERS = [
|
|||
scopeTypes: ['initiative'],
|
||||
order: 0,
|
||||
},
|
||||
|
||||
// —— Work / Ausführen (AP1.9d — Drill-down, kein Todo-Wand-Hauptbild) ——
|
||||
{
|
||||
key: 'work.actions.today',
|
||||
kind: 'core',
|
||||
slotKeys: ['work.today.actions'],
|
||||
componentKey: 'WorkActionsPanel',
|
||||
workActionsVariant: 'today',
|
||||
requiresCapability: 'kairo.action.read',
|
||||
scopeTypes: ['initiative'],
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
key: 'work.actions.sprint',
|
||||
kind: 'conditional',
|
||||
predicateKey: 'hasActiveWorkCycle',
|
||||
slotKeys: ['work.sprint.backlog'],
|
||||
componentKey: 'WorkActionsPanel',
|
||||
workActionsVariant: 'sprint',
|
||||
requiresCapability: 'kairo.action.read',
|
||||
scopeTypes: ['initiative'],
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
key: 'work.actions.sprint_product',
|
||||
kind: 'conditional',
|
||||
predicateKey: 'isProductWithoutActiveSprint',
|
||||
slotKeys: ['work.sprint.backlog'],
|
||||
componentKey: 'WorkActionsPanel',
|
||||
workActionsVariant: 'sprint-product',
|
||||
requiresCapability: 'kairo.action.read',
|
||||
scopeTypes: ['initiative'],
|
||||
order: 5,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { RecurringRhythmPanel } from '../components/RecurringRhythmPanel.jsx'
|
|||
import { AgentSlotsPanel } from '../components/AgentSlotsPanel.jsx'
|
||||
import { SteeringProposalsPanel } from '../components/SteeringProposalsPanel.jsx'
|
||||
import { NextActionWidget } from '../widgets/NextActionWidget.jsx'
|
||||
import { WorkActionsPanel } from './WorkActionsPanel.jsx'
|
||||
|
||||
/** Roadblocker-Strip — extrahiert aus InitiativeOverviewPage für Composition. */
|
||||
export function RoadblockersStrip({ initiativeId, actionsBlocked = 0, blockersOpen = 0 }) {
|
||||
|
|
@ -44,7 +43,6 @@ export const PROVIDER_COMPONENTS = {
|
|||
SteeringProposalsPanel,
|
||||
RoadblockersStrip,
|
||||
WidgetHost,
|
||||
WorkActionsPanel,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -33,25 +33,6 @@ const CONDITIONAL_PREDICATES = {
|
|||
const counts = ctx.steeringSnapshot?.counts || {}
|
||||
return (counts.actions_blocked || 0) > 0 || (counts.blockers_open || 0) > 0
|
||||
},
|
||||
hasActiveWorkCycle(ctx) {
|
||||
const cycle =
|
||||
ctx.steeringSnapshot?.active_work_cycle
|
||||
|| ctx.opsContext?.activeWorkCycle
|
||||
|| null
|
||||
return Boolean(cycle?.id)
|
||||
},
|
||||
isProductWithoutActiveSprint(ctx) {
|
||||
if (CONDITIONAL_PREDICATES.hasActiveWorkCycle(ctx)) return false
|
||||
const features =
|
||||
ctx.operatingContext?.ui_features
|
||||
|| ctx.opsContext?.uiFeatures
|
||||
|| {}
|
||||
const slices =
|
||||
ctx.operatingContext?.data_slices
|
||||
|| ctx.opsContext?.dataSlices
|
||||
|| []
|
||||
return Boolean(features.continuousProductWorkMode && slices.includes('work_cycles'))
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -240,11 +221,6 @@ export function buildProviderProps(provider, input) {
|
|||
const widget = getWidgetByKey(provider.widgetKey)
|
||||
return { widget }
|
||||
}
|
||||
case 'WorkActionsPanel':
|
||||
return {
|
||||
variant: provider.workActionsVariant || 'today',
|
||||
defaultCollapsed: true,
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ describe('uiSlotRegistry', () => {
|
|||
it('defines MVP surfaces', () => {
|
||||
expect(COMPOSITION_SURFACES['control.status']).toBeTruthy()
|
||||
expect(COMPOSITION_SURFACES['plan.inbox']).toBeTruthy()
|
||||
expect(COMPOSITION_SURFACES['work.today']).toBeTruthy()
|
||||
expect(COMPOSITION_SURFACES['work.sprint']).toBeTruthy()
|
||||
expect(COMPOSITION_SURFACES.cockpit).toBeTruthy()
|
||||
})
|
||||
|
||||
|
|
@ -260,67 +258,4 @@ describe('resolveSteeringComposition', () => {
|
|||
expect(withGate.showGateHorizon).toBe(true)
|
||||
expect(withoutGate.showGateHorizon).toBe(false)
|
||||
})
|
||||
|
||||
it('activates work.today next action and actions providers', () => {
|
||||
const result = resolveSteeringComposition({
|
||||
surfaceKey: 'work.today',
|
||||
scope: 'initiative',
|
||||
capabilities: CAPS,
|
||||
steeringElements: ['next_action_primary', 'critical_path'],
|
||||
steeringSnapshot: { counts: {}, next_actions: [{ kind: 'action', title: 'X' }] },
|
||||
opsContext: { initiativeId: 'init-1' },
|
||||
})
|
||||
expect(result.slots['work.today.primary']?.some((p) => p.key === 'steering.next_action')).toBe(
|
||||
true,
|
||||
)
|
||||
expect(result.slots['work.today.actions']?.some((p) => p.key === 'work.actions.today')).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('activates sprint backlog provider when active work cycle', () => {
|
||||
const result = resolveSteeringComposition({
|
||||
surfaceKey: 'work.sprint',
|
||||
scope: 'initiative',
|
||||
capabilities: CAPS,
|
||||
steeringElements: ['next_action_primary', 'work_cycle_scope'],
|
||||
steeringSnapshot: {
|
||||
counts: {},
|
||||
next_actions: [],
|
||||
active_work_cycle: { id: 'wc-1', title: 'Sprint R1' },
|
||||
},
|
||||
opsContext: { initiativeId: 'init-1', activeWorkCycle: { id: 'wc-1' } },
|
||||
})
|
||||
expect(result.slots['work.sprint.backlog']?.some((p) => p.key === 'work.actions.sprint')).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('activates product backlog panel without active sprint', () => {
|
||||
const result = resolveSteeringComposition({
|
||||
surfaceKey: 'work.sprint',
|
||||
scope: 'initiative',
|
||||
capabilities: CAPS,
|
||||
operatingContext: {
|
||||
ui_features: { continuousProductWorkMode: true },
|
||||
data_slices: ['work_cycles'],
|
||||
},
|
||||
steeringSnapshot: { counts: {}, next_actions: [] },
|
||||
opsContext: {
|
||||
initiativeId: 'init-1',
|
||||
uiFeatures: { continuousProductWorkMode: true },
|
||||
dataSlices: ['work_cycles'],
|
||||
},
|
||||
})
|
||||
expect(
|
||||
result.slots['work.sprint.backlog']?.some((p) => p.key === 'work.actions.sprint_product'),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('buildProviderProps passes work actions variant', () => {
|
||||
const provider = COMPOSITION_PROVIDERS.find((p) => p.key === 'work.actions.today')
|
||||
const props = buildProviderProps(provider, { opsContext: {} })
|
||||
expect(props.variant).toBe('today')
|
||||
expect(props.defaultCollapsed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -26,11 +26,6 @@ export const UI_SLOTS = [
|
|||
{ key: 'plan.inbox.proposals', mode: 'plan', routeKey: 'inbox', label: 'Triage-Vorschläge', order: 0 },
|
||||
{ key: 'plan.sprint.proposals', mode: 'plan', routeKey: 'sprint', label: 'Sprint-Vorschläge', order: 0 },
|
||||
{ key: 'plan.gates.proposals', mode: 'plan', routeKey: 'gates', label: 'Gate-Vorschläge', order: 0 },
|
||||
|
||||
{ key: 'work.today.primary', mode: 'work', routeKey: 'today', label: 'Nächster Schritt', order: 0 },
|
||||
{ key: 'work.today.actions', mode: 'work', routeKey: 'today', label: 'Arbeitspakete', order: 10 },
|
||||
{ key: 'work.sprint.primary', mode: 'work', routeKey: 'sprint', label: 'Nächster Schritt', order: 0 },
|
||||
{ key: 'work.sprint.backlog', mode: 'work', routeKey: 'sprint', label: 'Sprint-Backlog', order: 10 },
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
@ -73,16 +68,6 @@ export const COMPOSITION_SURFACES = {
|
|||
routeKey: 'gates',
|
||||
slotKeys: ['plan.gates.proposals'],
|
||||
},
|
||||
'work.today': {
|
||||
mode: 'work',
|
||||
routeKey: 'today',
|
||||
slotKeys: ['work.today.primary', 'work.today.actions'],
|
||||
},
|
||||
'work.sprint': {
|
||||
mode: 'work',
|
||||
routeKey: 'sprint',
|
||||
slotKeys: ['work.sprint.primary', 'work.sprint.backlog'],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* AP1.9d — zentrale UI-Begriffe (Product Language).
|
||||
* Spätere UI-Neuimplementierung kann diese Konstanten ersetzen, ohne Pages zu patchen.
|
||||
* @see docs/product/Kairo_MVP_Definition_v0.3.md §7
|
||||
*/
|
||||
|
||||
export const PRODUCT_TERMS = {
|
||||
action: 'Arbeitspaket',
|
||||
actionPlural: 'Arbeitspakete',
|
||||
actionPossessive: 'meine Arbeitspakete',
|
||||
workCycle: 'Sprint',
|
||||
workCyclePlural: 'Sprints',
|
||||
productBacklog: 'Product Backlog',
|
||||
sprintBacklog: 'Sprint-Backlog',
|
||||
intake: 'Eingang',
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} count
|
||||
*/
|
||||
export function actionCountLabel(count) {
|
||||
if (count === 1) return `1 ${PRODUCT_TERMS.action}`
|
||||
return `${count} ${PRODUCT_TERMS.actionPlural}`
|
||||
}
|
||||
|
|
@ -2,13 +2,11 @@ import { useEffect, useMemo, useCallback } from 'react'
|
|||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||
import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
|
||||
import { BacklogSection } from '../../components/BacklogSection.jsx'
|
||||
import { InitiativeCompositionSurface } from '../../composition/InitiativeCompositionSurface.jsx'
|
||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { countActionsByWorkCycle } from '../../utils/workCycleActions.js'
|
||||
import { PRODUCT_TERMS } from '../../constants/productLanguage.js'
|
||||
|
||||
function PlanSprintInner() {
|
||||
const ops = useInitiativeOperations()
|
||||
|
|
@ -21,10 +19,8 @@ function PlanSprintInner() {
|
|||
selectedSprintActions,
|
||||
workCycles,
|
||||
actions,
|
||||
backlogItems,
|
||||
projects,
|
||||
roadmapItems,
|
||||
featureParentActions,
|
||||
capabilities,
|
||||
hideDone,
|
||||
setHideDone,
|
||||
|
|
@ -53,50 +49,19 @@ function PlanSprintInner() {
|
|||
handleMoveActionToSprint,
|
||||
handleUnplanAction,
|
||||
handleConvertBacklog,
|
||||
handleBulkConvertBacklog,
|
||||
handleCreateBacklog,
|
||||
handleUpdateBacklog,
|
||||
handleReorderBacklog,
|
||||
handleDeleteBacklog,
|
||||
reloadSlices,
|
||||
} = ops
|
||||
|
||||
useEffect(() => {
|
||||
reloadSlices(['steering_snapshot', 'backlog'])
|
||||
}, [reloadSlices])
|
||||
|
||||
const reloadAfterSprintCommit = useCallback(async () => {
|
||||
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
||||
reloadSlices(['steering_snapshot'])
|
||||
}, [reloadSlices])
|
||||
|
||||
const onAcceptBacklogProposal = useCallback(
|
||||
async (itemId, options) => {
|
||||
await handleConvertBacklog(itemId, {
|
||||
work_cycle_id: options?.work_cycle_id || selectedWorkCycleId,
|
||||
})
|
||||
await reloadAfterSprintCommit()
|
||||
await handleConvertBacklog(itemId, options)
|
||||
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
||||
},
|
||||
[handleConvertBacklog, selectedWorkCycleId, reloadAfterSprintCommit],
|
||||
)
|
||||
|
||||
const onConvertToSprint = useCallback(
|
||||
async (itemId, options) => {
|
||||
await handleConvertBacklog(itemId, {
|
||||
work_cycle_id: options?.work_cycle_id || selectedWorkCycleId,
|
||||
})
|
||||
await reloadAfterSprintCommit()
|
||||
},
|
||||
[handleConvertBacklog, selectedWorkCycleId, reloadAfterSprintCommit],
|
||||
)
|
||||
|
||||
const onBulkConvertToSprint = useCallback(
|
||||
async (itemIds, options) => {
|
||||
await handleBulkConvertBacklog(itemIds, {
|
||||
work_cycle_id: options?.work_cycle_id || selectedWorkCycleId,
|
||||
})
|
||||
await reloadAfterSprintCommit()
|
||||
},
|
||||
[handleBulkConvertBacklog, selectedWorkCycleId, reloadAfterSprintCommit],
|
||||
[handleConvertBacklog, reloadSlices],
|
||||
)
|
||||
|
||||
const proposalFilterContext = useMemo(
|
||||
|
|
@ -148,33 +113,11 @@ function PlanSprintInner() {
|
|||
busy={formBusy}
|
||||
/>
|
||||
{selectedWorkCycleId && (
|
||||
<>
|
||||
<InitiativeCompositionSurface
|
||||
surfaceKey="plan.sprint"
|
||||
filterContext={proposalFilterContext}
|
||||
onAcceptBacklogProposal={onAcceptBacklogProposal}
|
||||
/>
|
||||
<BacklogSection
|
||||
items={backlogItems}
|
||||
initiativeId={initiativeId}
|
||||
roadmapItems={roadmapItems}
|
||||
featureActions={featureParentActions}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
canManage={capabilities.has('kairo.backlog.manage')}
|
||||
onCreate={handleCreateBacklog}
|
||||
onUpdate={handleUpdateBacklog}
|
||||
onReorder={handleReorderBacklog}
|
||||
onConvert={onConvertToSprint}
|
||||
onBulkConvert={onBulkConvertToSprint}
|
||||
onDelete={handleDeleteBacklog}
|
||||
busy={formBusy}
|
||||
defaultSprintTargetId={selectedWorkCycleId}
|
||||
lockSprintTarget
|
||||
sectionTitle={`Aus ${PRODUCT_TERMS.intake} in ${PRODUCT_TERMS.workCycle} planen`}
|
||||
sectionLead={`Markiere commit-bereite Items und lege sie in „${selectedWorkCycle?.title || PRODUCT_TERMS.workCycle}" — oder nutze die Sprint-Vorschläge oben.`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{selectedWorkCycleId && selectedWorkCycle && (
|
||||
<InitiativeActionsHub
|
||||
|
|
@ -207,7 +150,7 @@ function PlanSprintInner() {
|
|||
workCycles={workCycles}
|
||||
activeWorkCycle={selectedWorkCycle}
|
||||
allActions={actions}
|
||||
sectionTitle={`${PRODUCT_TERMS.sprintBacklog} — ${selectedWorkCycle.title}`}
|
||||
sectionTitle={`Sprint-Backlog — ${selectedWorkCycle.title}`}
|
||||
sectionLead={sprintLead}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,51 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { InitiativeCompositionSurface } from '../../composition/InitiativeCompositionSurface.jsx'
|
||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { EmptyState } from '../../components/EmptyState.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { scopedPath } from '../../utils/routes.js'
|
||||
import { PRODUCT_TERMS } from '../../constants/productLanguage.js'
|
||||
|
||||
function WorkSprintInner() {
|
||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||
const ops = useInitiativeOperations()
|
||||
const {
|
||||
activeWorkCycle,
|
||||
sprintActions,
|
||||
projects,
|
||||
roadmapItems,
|
||||
capabilities,
|
||||
hideDone,
|
||||
setHideDone,
|
||||
loading,
|
||||
error,
|
||||
formBusy,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingActionId,
|
||||
setEditingActionId,
|
||||
actionContextById,
|
||||
actors,
|
||||
actorsLoading,
|
||||
actorsError,
|
||||
actorsUsedFallback,
|
||||
reloadActors,
|
||||
handleCreateAction,
|
||||
handleUpdateAction,
|
||||
handleQuickStatus,
|
||||
handleCreateBlockerForAction,
|
||||
handleMoveActionToSprint,
|
||||
handleUnplanAction,
|
||||
initiative,
|
||||
operatingContext,
|
||||
dataSlices,
|
||||
uiFeatures,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
visibleActions,
|
||||
reloadSlices,
|
||||
} = ops
|
||||
|
||||
|
|
@ -40,63 +65,157 @@ function WorkSprintInner() {
|
|||
reloadSlices,
|
||||
])
|
||||
|
||||
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
||||
|
||||
const createAction = useMemo(
|
||||
() => async (payload) => {
|
||||
await handleCreateAction({
|
||||
...payload,
|
||||
work_cycle_id: payload.work_cycle_id || defaultWorkCycleId || undefined,
|
||||
})
|
||||
},
|
||||
[handleCreateAction, defaultWorkCycleId],
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message={`Lade ${PRODUCT_TERMS.sprintBacklog} …`} />
|
||||
return <LoadingState message="Lade Sprint-Backlog …" />
|
||||
}
|
||||
|
||||
if (!supportsWorkCycles) {
|
||||
return (
|
||||
<EmptyState
|
||||
message={`${PRODUCT_TERMS.sprintBacklog} ist für diesen Vorhabentyp nicht verfügbar.`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const isProductArchetype = isContinuousProductMode
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{!activeWorkCycle && isContinuousProductMode && (
|
||||
{!activeWorkCycle && isProductArchetype && (
|
||||
<>
|
||||
<section className="card sprint-context-banner">
|
||||
<h2>Product-Ist (ohne aktiven {PRODUCT_TERMS.workCycle})</h2>
|
||||
<h2>Product-Ist (ohne aktiven Sprint)</h2>
|
||||
<p className="section-lead muted">
|
||||
Kein aktiver {PRODUCT_TERMS.workCycle} — Kairo steuert nach{' '}
|
||||
<strong>{operatingContext?.method_key || 'continuous_product'}</strong> (wirkungsvollster
|
||||
Schritt). {PRODUCT_TERMS.workCycle}-Planung unter{' '}
|
||||
Kein aktiver Sprint — Kairo steuert nach{' '}
|
||||
<strong>{operatingContext?.method_key || 'continuous_product'}</strong> (wirkungsvollster Schritt). Sprint-Planung
|
||||
unter{' '}
|
||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||
Plan → Sprint planen
|
||||
Plan → Sprint
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<NextActionWidget
|
||||
scope="initiative"
|
||||
initiativeId={initiativeId}
|
||||
items={steeringSnapshot?.next_actions}
|
||||
loading={steeringSnapshotLoading}
|
||||
embedded
|
||||
title="Nächster Schritt (Product)"
|
||||
subtitle="Empfehlung ohne Sprint-Zeitbox — committete Arbeitspakete im Vorhaben."
|
||||
/>
|
||||
|
||||
<InitiativeActionsHub
|
||||
initiativeId={initiativeId}
|
||||
actions={visibleActions}
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
actionContextById={actionContextById}
|
||||
hideDone={hideDone}
|
||||
onHideDoneChange={setHideDone}
|
||||
showForm={showActionForm}
|
||||
onToggleForm={() => setShowActionForm((v) => !v)}
|
||||
editingActionId={editingActionId}
|
||||
onEditAction={setEditingActionId}
|
||||
onCancelEdit={() => setEditingActionId(null)}
|
||||
onCreateAction={handleCreateAction}
|
||||
onUpdateAction={handleUpdateAction}
|
||||
onQuickStatus={handleQuickStatus}
|
||||
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||
onMoveToSprint={handleMoveActionToSprint}
|
||||
onUnplanToBacklog={handleUnplanAction}
|
||||
canManage={capabilities.has('kairo.action.manage')}
|
||||
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||
formBusy={formBusy}
|
||||
actors={actors}
|
||||
actorsLoading={actorsLoading}
|
||||
actorsError={actorsError}
|
||||
actorsUsedFallback={actorsUsedFallback}
|
||||
onReloadActors={reloadActors}
|
||||
workCycles={ops.workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
sectionTitle="Committete Arbeitspakete"
|
||||
sectionLead="Ohne aktiven Sprint — alle offenen Arbeitspakete im Product-Vorhaben."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeWorkCycle && (
|
||||
<>
|
||||
<section className="card sprint-context-banner">
|
||||
<h2>{PRODUCT_TERMS.sprintBacklog}</h2>
|
||||
<h2>Sprint-Backlog</h2>
|
||||
<p className="section-lead muted">
|
||||
<strong>{activeWorkCycle.title}</strong>
|
||||
{activeWorkCycle.goal_description && ` — ${activeWorkCycle.goal_description}`}
|
||||
{' · '}
|
||||
{PRODUCT_TERMS.productBacklog} triagierst du unter{' '}
|
||||
Product Backlog triagierst du unter{' '}
|
||||
<Link to={scopedPath('/plan/inbox', { initiativeId })} className="link-inline">
|
||||
Plan → {PRODUCT_TERMS.intake}
|
||||
Plan → Eingang
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<NextActionWidget
|
||||
scope="initiative"
|
||||
initiativeId={initiativeId}
|
||||
items={steeringSnapshot?.next_actions}
|
||||
loading={steeringSnapshotLoading}
|
||||
embedded
|
||||
title="Nächster Schritt im Sprint"
|
||||
/>
|
||||
|
||||
<InitiativeActionsHub
|
||||
initiativeId={initiativeId}
|
||||
actions={sprintActions}
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
actionContextById={actionContextById}
|
||||
hideDone={hideDone}
|
||||
onHideDoneChange={setHideDone}
|
||||
showForm={showActionForm}
|
||||
onToggleForm={() => setShowActionForm((v) => !v)}
|
||||
editingActionId={editingActionId}
|
||||
onEditAction={setEditingActionId}
|
||||
onCancelEdit={() => setEditingActionId(null)}
|
||||
onCreateAction={createAction}
|
||||
onUpdateAction={handleUpdateAction}
|
||||
onQuickStatus={handleQuickStatus}
|
||||
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||
onMoveToSprint={handleMoveActionToSprint}
|
||||
onUnplanToBacklog={handleUnplanAction}
|
||||
canManage={capabilities.has('kairo.action.manage')}
|
||||
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||
formBusy={formBusy}
|
||||
actors={actors}
|
||||
actorsLoading={actorsLoading}
|
||||
actorsError={actorsError}
|
||||
actorsUsedFallback={actorsUsedFallback}
|
||||
onReloadActors={reloadActors}
|
||||
workCycles={ops.workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
allActions={ops.actions}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<InitiativeCompositionSurface surfaceKey="work.sprint" />
|
||||
{!activeWorkCycle && !supportsWorkCycles && (
|
||||
<EmptyState message="Sprint-Ausführung ist für diesen Vorhabentyp nicht verfügbar." />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkSprintPage() {
|
||||
return (
|
||||
<RequireInitiativeScope
|
||||
lead={`${PRODUCT_TERMS.sprintBacklog} ist vorhabenspezifisch — wähle ein passendes Vorhaben im Scope.`}
|
||||
>
|
||||
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein passendes Vorhaben im Scope.">
|
||||
<WorkSprintInner />
|
||||
</RequireInitiativeScope>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,79 +1,9 @@
|
|||
import { useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
||||
import { WorkspaceTodayWidget } from '../../widgets/WorkspaceTodayWidget.jsx'
|
||||
import { InitiativeCompositionSurface } from '../../composition/InitiativeCompositionSurface.jsx'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { PRODUCT_TERMS } from '../../constants/productLanguage.js'
|
||||
|
||||
function WorkTodayPortfolioView() {
|
||||
return (
|
||||
<>
|
||||
<NextActionWidget
|
||||
scope="portfolio"
|
||||
embedded
|
||||
title="Nächste sinnvolle Schritte"
|
||||
subtitle="Portfolio-weit — wähle ein Vorhaben im Scope für methodengebundene Ausführung."
|
||||
/>
|
||||
<section className="card">
|
||||
<WorkspaceTodayWidget limit={3} />
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkTodayInitiativeInner() {
|
||||
const { hrefWithScope } = useProgramScope()
|
||||
const ops = useInitiativeOperations()
|
||||
const { loading, error, reloadSlices, uiFeatures } = ops
|
||||
|
||||
useEffect(() => {
|
||||
reloadSlices(['steering_snapshot'])
|
||||
}, [reloadSlices])
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Lade Ausführung …" />
|
||||
}
|
||||
|
||||
const isProductWithSprint = uiFeatures.continuousProductWorkMode
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <p className="error">{error}</p>}
|
||||
{isProductWithSprint && (
|
||||
<section className="card work-context-banner">
|
||||
<p className="section-lead muted">
|
||||
Ausführen zeigt den <strong>nächsten Schritt</strong> — nicht die Gesamtliste.{' '}
|
||||
{PRODUCT_TERMS.sprintBacklog} unter{' '}
|
||||
<Link to={hrefWithScope('/work/sprint')} className="link-inline">
|
||||
Ausführen → Sprint
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
<InitiativeCompositionSurface surfaceKey="work.today" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkTodayInitiativeView() {
|
||||
return (
|
||||
<RequireInitiativeScope lead="Ausführung im Vorhaben-Kontext braucht ein gewähltes Vorhaben im Scope.">
|
||||
<WorkTodayInitiativeInner />
|
||||
</RequireInitiativeScope>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkTodayPage() {
|
||||
const { initiativeId } = useProgramScope()
|
||||
|
||||
if (!initiativeId) {
|
||||
return <WorkTodayPortfolioView />
|
||||
}
|
||||
|
||||
return <WorkTodayInitiativeView />
|
||||
return (
|
||||
<section className="card">
|
||||
<WorkspaceTodayWidget />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const STEERING_ELEMENT_UI = {
|
|||
|
||||
/** Labels für Steuerungselemente auf Plan-Profil (Anzeige, nicht Verhalten). */
|
||||
export const STEERING_ELEMENT_PLAN_LABELS = {
|
||||
work_cycle_scope: 'Sprint',
|
||||
work_cycle_scope: 'Sprint-Zeitbox',
|
||||
backlog_epic_hierarchy: 'Epic-Hierarchie im Eingang',
|
||||
gate_fulfillment: 'Gate-Fortschritt',
|
||||
queue_inbox: 'Queue-Eingang',
|
||||
|
|
|
|||
|
|
@ -1093,11 +1093,6 @@
|
|||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.collapsible-section__toggle {
|
||||
margin: 0.75rem 0 0;
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.initiative-advanced-stack {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { ErrorState } from '../components/ErrorState.jsx'
|
|||
import { LoadingState } from '../components/LoadingState.jsx'
|
||||
import { WidgetCard } from '../components/WidgetCard.jsx'
|
||||
import { actionPath, scopedPath } from '../utils/routes.js'
|
||||
import { PRODUCT_TERMS } from '../constants/productLanguage.js'
|
||||
|
||||
function formatDue(iso) {
|
||||
if (!iso) return null
|
||||
|
|
@ -24,7 +23,7 @@ function formatDue(iso) {
|
|||
}
|
||||
}
|
||||
|
||||
export function WorkspaceTodayWidget({ limit = 10 }) {
|
||||
export function WorkspaceTodayWidget() {
|
||||
const [actions, setActions] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
|
@ -33,34 +32,32 @@ export function WorkspaceTodayWidget({ limit = 10 }) {
|
|||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await listWorkspaceTodayActions(limit)
|
||||
const data = await listWorkspaceTodayActions(10)
|
||||
setActions(Array.isArray(data) ? data : [])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [limit])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const hasMoreHint = limit < 10
|
||||
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Heute"
|
||||
subtitle={`Deine ${PRODUCT_TERMS.actionPlural} — überfällig und dringend zuerst (max. ${limit})`}
|
||||
subtitle="Deine Maßnahmen — überfällig und dringend zuerst"
|
||||
actions={
|
||||
<button type="button" className="btn btn-ghost" onClick={load} disabled={loading}>
|
||||
Aktualisieren
|
||||
</button>
|
||||
}
|
||||
footer={
|
||||
actions.length > 0 || hasMoreHint ? (
|
||||
actions.length > 0 ? (
|
||||
<Link to="/work/mine" className="link-inline">
|
||||
Alle {PRODUCT_TERMS.actionPossessive} →
|
||||
Alle meine Maßnahmen →
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
|
|
@ -68,7 +65,7 @@ export function WorkspaceTodayWidget({ limit = 10 }) {
|
|||
{loading && <LoadingState />}
|
||||
{!loading && error && <ErrorState message={error} onRetry={load} />}
|
||||
{!loading && !error && actions.length === 0 && (
|
||||
<EmptyState message={`Keine ${PRODUCT_TERMS.actionPlural} für heute — wähle ein Vorhaben für die Steuerungsempfehlung.`} />
|
||||
<EmptyState message="Keine Maßnahmen für heute — prüfe „Nächste sinnvolle Schritte“ oben." />
|
||||
)}
|
||||
{!loading && !error && actions.length > 0 && (
|
||||
<ul className="item-list">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user