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_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_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_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.
|
Dev-Seeds (`.dev.*`) werden in Nicht-Prod-Umgebungen **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`.
|
|
||||||
|
|
||||||
## Neuen Seed anlegen
|
## 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.inbox.proposals` | plan | inbox | Triage-Vorschläge |
|
||||||
| `plan.sprint.proposals` | plan | sprint | Sprint-Commit-Vorschläge |
|
| `plan.sprint.proposals` | plan | sprint | Sprint-Commit-Vorschläge |
|
||||||
| `plan.gates.proposals` | plan | gates | Gate-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`.
|
**Erweiterung (post-MVP):** `control.plan-ist.read_models`, `plan.inbox.read_models`, Work-Modus-Slots.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) |
|
| Steering Kernel Proposals | ✓ | sprint_commit · gate_next_actions · intake_triage (Kernel v0.4) |
|
||||||
| AP2.2b | A2 Linear E2E ✓ (2026-07-27) |
|
| AP2.2b | A2 Linear E2E ✓ (2026-07-27) |
|
||||||
| AP2.2c | A1 Reifegrad 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.2c–e | Referenz-Archetypen End-to-End ◐→✓ |
|
||||||
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
||||||
| AP1.7b | Op-API Parität |
|
| 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 0b AP2.3/AP2.4 Plugin-Architektur (Archetyp ↔ Methode) ✓
|
||||||
Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ✓
|
Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ✓
|
||||||
Phase 1b Specs Welle 1 finalisieren (A1,A2,B2a,B2b,B3) ◐
|
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 ✓
|
Phase 3 AP2.2b A2 Linear End-to-End ✓
|
||||||
AP2.2c A1 Reifegrad + AP2.0e ✓
|
AP2.2c A1 Reifegrad + AP2.0e ✓
|
||||||
AP2.2d B2b Product + B3 Sprint ✓
|
AP2.2d B2b Product + B3 Sprint ✓
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,6 @@ export function BacklogSection({
|
||||||
busy,
|
busy,
|
||||||
sectionTitle = 'Product Backlog',
|
sectionTitle = 'Product Backlog',
|
||||||
sectionLead = 'Eingang vor dem Commit — triagieren, dann in den Sprint planen.',
|
sectionLead = 'Eingang vor dem Commit — triagieren, dann in den Sprint planen.',
|
||||||
defaultSprintTargetId = null,
|
|
||||||
lockSprintTarget = false,
|
|
||||||
}) {
|
}) {
|
||||||
const { operatingContext, steeringElements, steeringSnapshot } = useInitiativeOperations()
|
const { operatingContext, steeringElements, steeringSnapshot } = useInitiativeOperations()
|
||||||
const [modalMode, setModalMode] = useState(null)
|
const [modalMode, setModalMode] = useState(null)
|
||||||
|
|
@ -76,13 +74,6 @@ export function BacklogSection({
|
||||||
if (!showSprintPlanning || !initiativeId) {
|
if (!showSprintPlanning || !initiativeId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
defaultSprintTargetId
|
|
||||||
&& planableSprints.some((cycle) => cycle.id === defaultSprintTargetId)
|
|
||||||
) {
|
|
||||||
setSprintTargetId(defaultSprintTargetId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const stored = sessionStorage.getItem(SPRINT_TARGET_STORAGE_PREFIX + initiativeId)
|
const stored = sessionStorage.getItem(SPRINT_TARGET_STORAGE_PREFIX + initiativeId)
|
||||||
if (stored && planableSprints.some((cycle) => cycle.id === stored)) {
|
if (stored && planableSprints.some((cycle) => cycle.id === stored)) {
|
||||||
setSprintTargetId(stored)
|
setSprintTargetId(stored)
|
||||||
|
|
@ -93,13 +84,7 @@ export function BacklogSection({
|
||||||
}
|
}
|
||||||
const preferred = activeWorkCycle?.id || planableSprints[0]?.id || ''
|
const preferred = activeWorkCycle?.id || planableSprints[0]?.id || ''
|
||||||
setSprintTargetId(preferred)
|
setSprintTargetId(preferred)
|
||||||
}, [
|
}, [showSprintPlanning, initiativeId, planableSprints, activeWorkCycle?.id])
|
||||||
showSprintPlanning,
|
|
||||||
initiativeId,
|
|
||||||
planableSprints,
|
|
||||||
activeWorkCycle?.id,
|
|
||||||
defaultSprintTargetId,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initiativeId || !sprintTargetId) return
|
if (!initiativeId || !sprintTargetId) return
|
||||||
|
|
@ -307,36 +292,21 @@ export function BacklogSection({
|
||||||
|
|
||||||
{showSprintPlanning && canManage && (
|
{showSprintPlanning && canManage && (
|
||||||
<div className="backlog-sprint-planning card-list-item">
|
<div className="backlog-sprint-planning card-list-item">
|
||||||
{lockSprintTarget && selectedSprint ? (
|
<label className="backlog-sprint-planning__field">
|
||||||
<p className="muted backlog-sprint-planning__locked">
|
<span className="muted">Ziel-Sprint</span>
|
||||||
Ziel-Sprint: <strong>{selectedSprint.title}</strong>
|
<select
|
||||||
{selectedSprint.status === 'active'
|
value={sprintTargetId}
|
||||||
? ' (aktiv)'
|
onChange={(e) => setSprintTargetId(e.target.value)}
|
||||||
: selectedSprint.status === 'planned'
|
disabled={busy}
|
||||||
? ' (geplant)'
|
>
|
||||||
: ''}
|
{planableSprints.map((cycle) => (
|
||||||
</p>
|
<option key={cycle.id} value={cycle.id}>
|
||||||
) : (
|
{cycle.title}
|
||||||
<label className="backlog-sprint-planning__field">
|
{cycle.status === 'active' ? ' (aktiv)' : cycle.status === 'planned' ? ' (geplant)' : ''}
|
||||||
<span className="muted">Ziel-Sprint</span>
|
</option>
|
||||||
<select
|
))}
|
||||||
value={sprintTargetId}
|
</select>
|
||||||
onChange={(e) => setSprintTargetId(e.target.value)}
|
</label>
|
||||||
disabled={busy}
|
|
||||||
>
|
|
||||||
{planableSprints.map((cycle) => (
|
|
||||||
<option key={cycle.id} value={cycle.id}>
|
|
||||||
{cycle.title}
|
|
||||||
{cycle.status === 'active'
|
|
||||||
? ' (aktiv)'
|
|
||||||
: cycle.status === 'planned'
|
|
||||||
? ' (geplant)'
|
|
||||||
: ''}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
{selectedCommittable.length > 0 && (
|
{selectedCommittable.length > 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,29 @@
|
||||||
import { useState } from 'react'
|
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({
|
export function CollapsibleSection({
|
||||||
title,
|
title,
|
||||||
lead,
|
summary,
|
||||||
defaultCollapsed = true,
|
defaultOpen = false,
|
||||||
expandLabel = 'Alle anzeigen',
|
|
||||||
collapseLabel = 'Einklappen',
|
|
||||||
headerActions = null,
|
|
||||||
className = 'card',
|
|
||||||
children,
|
children,
|
||||||
|
className = '',
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(!defaultCollapsed)
|
const [open, setOpen] = useState(defaultOpen)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={className}>
|
<section className={`collapsible-section card ${className}`.trim()}>
|
||||||
<div className="section-header">
|
<button
|
||||||
<div>
|
type="button"
|
||||||
{title && <h2>{title}</h2>}
|
className="collapsible-section-trigger"
|
||||||
{lead && <p className="section-lead muted">{lead}</p>}
|
onClick={() => setOpen((v) => !v)}
|
||||||
</div>
|
aria-expanded={open}
|
||||||
{headerActions}
|
>
|
||||||
</div>
|
<span className="collapsible-section-title">{title}</span>
|
||||||
|
{summary && <span className="collapsible-section-summary muted">{summary}</span>}
|
||||||
{expanded ? (
|
<span className="collapsible-section-chevron" aria-hidden="true">
|
||||||
<>
|
{open ? '▾' : '▸'}
|
||||||
{children}
|
</span>
|
||||||
<p className="collapsible-section__toggle">
|
</button>
|
||||||
<button
|
{open && <div className="collapsible-section-body">{children}</div>}
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost btn-sm"
|
|
||||||
onClick={() => setExpanded(false)}
|
|
||||||
>
|
|
||||||
{collapseLabel}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="collapsible-section__toggle">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-sm"
|
|
||||||
onClick={() => setExpanded(true)}
|
|
||||||
>
|
|
||||||
{expandLabel}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { ActionHubCard } from './ActionHubCard.jsx'
|
import { ActionHubCard } from './ActionHubCard.jsx'
|
||||||
import { ActionForm } from './ActionForm.jsx'
|
import { ActionForm } from './ActionForm.jsx'
|
||||||
import { CollapsibleSection } from './CollapsibleSection.jsx'
|
|
||||||
import { EmptyState } from './EmptyState.jsx'
|
import { EmptyState } from './EmptyState.jsx'
|
||||||
|
|
||||||
export function InitiativeActionsHub({
|
export function InitiativeActionsHub({
|
||||||
|
|
@ -35,35 +34,35 @@ export function InitiativeActionsHub({
|
||||||
sectionTitle = 'Arbeitspakete',
|
sectionTitle = 'Arbeitspakete',
|
||||||
sectionLead = 'Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews hängen am Arbeitspaket.',
|
sectionLead = 'Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews hängen am Arbeitspaket.',
|
||||||
allActions = [],
|
allActions = [],
|
||||||
collapsible = false,
|
|
||||||
defaultCollapsed = true,
|
|
||||||
expandLabel = 'Alle anzeigen',
|
|
||||||
collapseLabel = 'Liste einklappen',
|
|
||||||
}) {
|
}) {
|
||||||
const headerActions = (
|
return (
|
||||||
<div className="section-actions">
|
<section className="card initiative-actions-hub">
|
||||||
<label className="checkbox-label inline-filter">
|
<div className="section-header">
|
||||||
<input
|
<div>
|
||||||
type="checkbox"
|
<h2>{sectionTitle}</h2>
|
||||||
checked={hideDone}
|
<p className="section-lead muted">{sectionLead}</p>
|
||||||
onChange={(e) => onHideDoneChange(e.target.checked)}
|
</div>
|
||||||
/>
|
<div className="section-actions">
|
||||||
Erledigte ausblenden
|
<label className="checkbox-label inline-filter">
|
||||||
</label>
|
<input
|
||||||
{canManage && (
|
type="checkbox"
|
||||||
<button
|
checked={hideDone}
|
||||||
type="button"
|
onChange={(e) => onHideDoneChange(e.target.checked)}
|
||||||
className="btn btn-primary btn-block-mobile"
|
/>
|
||||||
onClick={onToggleForm}
|
Erledigte ausblenden
|
||||||
>
|
</label>
|
||||||
{showForm ? 'Abbrechen' : 'Arbeitspaket'}
|
{canManage && (
|
||||||
</button>
|
<button
|
||||||
)}
|
type="button"
|
||||||
</div>
|
className="btn btn-primary btn-block-mobile"
|
||||||
)
|
onClick={onToggleForm}
|
||||||
|
>
|
||||||
|
{showForm ? 'Abbrechen' : 'Arbeitspaket'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
const body = (
|
|
||||||
<>
|
|
||||||
{showForm && canManage && (
|
{showForm && canManage && (
|
||||||
<div className="inline-form-block">
|
<div className="inline-form-block">
|
||||||
<ActionForm
|
<ActionForm
|
||||||
|
|
@ -119,35 +118,6 @@ export function InitiativeActionsHub({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,6 @@ export function InitiativeCompositionSurface({
|
||||||
handleMethodChange: ops.handleMethodChange,
|
handleMethodChange: ops.handleMethodChange,
|
||||||
methodBusy: ops.methodBusy,
|
methodBusy: ops.methodBusy,
|
||||||
formBusy: ops.formBusy,
|
formBusy: ops.formBusy,
|
||||||
activeWorkCycle: ops.activeWorkCycle,
|
|
||||||
uiFeatures: ops.uiFeatures,
|
|
||||||
dataSlices: ops.dataSlices,
|
|
||||||
onAcceptBacklogProposal,
|
onAcceptBacklogProposal,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
|
@ -49,9 +46,6 @@ export function InitiativeCompositionSurface({
|
||||||
ops.handleMethodChange,
|
ops.handleMethodChange,
|
||||||
ops.methodBusy,
|
ops.methodBusy,
|
||||||
ops.formBusy,
|
ops.formBusy,
|
||||||
ops.activeWorkCycle,
|
|
||||||
ops.uiFeatures,
|
|
||||||
ops.dataSlices,
|
|
||||||
onAcceptBacklogProposal,
|
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} [proposalKey]
|
||||||
* @property {string} [widgetKey]
|
* @property {string} [widgetKey]
|
||||||
* @property {string} [predicateKey]
|
* @property {string} [predicateKey]
|
||||||
* @property {string} [workActionsVariant]
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const WIDGET_PROVIDERS = WIDGETS.filter((w) => w.area === 'workspace').map((widget) => ({
|
const WIDGET_PROVIDERS = WIDGETS.filter((w) => w.area === 'workspace').map((widget) => ({
|
||||||
|
|
@ -82,7 +81,7 @@ export const COMPOSITION_PROVIDERS = [
|
||||||
key: 'steering.next_action',
|
key: 'steering.next_action',
|
||||||
kind: 'steering_element',
|
kind: 'steering_element',
|
||||||
steeringElement: 'next_action_primary',
|
steeringElement: 'next_action_primary',
|
||||||
slotKeys: ['control.status.steering', 'work.today.primary', 'work.sprint.primary'],
|
slotKeys: ['control.status.steering'],
|
||||||
componentKey: 'NextActionWidget',
|
componentKey: 'NextActionWidget',
|
||||||
requiresCapability: 'kairo.workspace.read',
|
requiresCapability: 'kairo.workspace.read',
|
||||||
scopeTypes: ['initiative'],
|
scopeTypes: ['initiative'],
|
||||||
|
|
@ -138,40 +137,6 @@ export const COMPOSITION_PROVIDERS = [
|
||||||
scopeTypes: ['initiative'],
|
scopeTypes: ['initiative'],
|
||||||
order: 0,
|
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 { AgentSlotsPanel } from '../components/AgentSlotsPanel.jsx'
|
||||||
import { SteeringProposalsPanel } from '../components/SteeringProposalsPanel.jsx'
|
import { SteeringProposalsPanel } from '../components/SteeringProposalsPanel.jsx'
|
||||||
import { NextActionWidget } from '../widgets/NextActionWidget.jsx'
|
import { NextActionWidget } from '../widgets/NextActionWidget.jsx'
|
||||||
import { WorkActionsPanel } from './WorkActionsPanel.jsx'
|
|
||||||
|
|
||||||
/** Roadblocker-Strip — extrahiert aus InitiativeOverviewPage für Composition. */
|
/** Roadblocker-Strip — extrahiert aus InitiativeOverviewPage für Composition. */
|
||||||
export function RoadblockersStrip({ initiativeId, actionsBlocked = 0, blockersOpen = 0 }) {
|
export function RoadblockersStrip({ initiativeId, actionsBlocked = 0, blockersOpen = 0 }) {
|
||||||
|
|
@ -44,7 +43,6 @@ export const PROVIDER_COMPONENTS = {
|
||||||
SteeringProposalsPanel,
|
SteeringProposalsPanel,
|
||||||
RoadblockersStrip,
|
RoadblockersStrip,
|
||||||
WidgetHost,
|
WidgetHost,
|
||||||
WorkActionsPanel,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -33,25 +33,6 @@ const CONDITIONAL_PREDICATES = {
|
||||||
const counts = ctx.steeringSnapshot?.counts || {}
|
const counts = ctx.steeringSnapshot?.counts || {}
|
||||||
return (counts.actions_blocked || 0) > 0 || (counts.blockers_open || 0) > 0
|
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)
|
const widget = getWidgetByKey(provider.widgetKey)
|
||||||
return { widget }
|
return { widget }
|
||||||
}
|
}
|
||||||
case 'WorkActionsPanel':
|
|
||||||
return {
|
|
||||||
variant: provider.workActionsVariant || 'today',
|
|
||||||
defaultCollapsed: true,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ describe('uiSlotRegistry', () => {
|
||||||
it('defines MVP surfaces', () => {
|
it('defines MVP surfaces', () => {
|
||||||
expect(COMPOSITION_SURFACES['control.status']).toBeTruthy()
|
expect(COMPOSITION_SURFACES['control.status']).toBeTruthy()
|
||||||
expect(COMPOSITION_SURFACES['plan.inbox']).toBeTruthy()
|
expect(COMPOSITION_SURFACES['plan.inbox']).toBeTruthy()
|
||||||
expect(COMPOSITION_SURFACES['work.today']).toBeTruthy()
|
|
||||||
expect(COMPOSITION_SURFACES['work.sprint']).toBeTruthy()
|
|
||||||
expect(COMPOSITION_SURFACES.cockpit).toBeTruthy()
|
expect(COMPOSITION_SURFACES.cockpit).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -260,67 +258,4 @@ describe('resolveSteeringComposition', () => {
|
||||||
expect(withGate.showGateHorizon).toBe(true)
|
expect(withGate.showGateHorizon).toBe(true)
|
||||||
expect(withoutGate.showGateHorizon).toBe(false)
|
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.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.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: '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',
|
routeKey: 'gates',
|
||||||
slotKeys: ['plan.gates.proposals'],
|
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 { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||||
import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
|
import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
|
||||||
import { BacklogSection } from '../../components/BacklogSection.jsx'
|
|
||||||
import { InitiativeCompositionSurface } from '../../composition/InitiativeCompositionSurface.jsx'
|
import { InitiativeCompositionSurface } from '../../composition/InitiativeCompositionSurface.jsx'
|
||||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||||
import { countActionsByWorkCycle } from '../../utils/workCycleActions.js'
|
import { countActionsByWorkCycle } from '../../utils/workCycleActions.js'
|
||||||
import { PRODUCT_TERMS } from '../../constants/productLanguage.js'
|
|
||||||
|
|
||||||
function PlanSprintInner() {
|
function PlanSprintInner() {
|
||||||
const ops = useInitiativeOperations()
|
const ops = useInitiativeOperations()
|
||||||
|
|
@ -21,10 +19,8 @@ function PlanSprintInner() {
|
||||||
selectedSprintActions,
|
selectedSprintActions,
|
||||||
workCycles,
|
workCycles,
|
||||||
actions,
|
actions,
|
||||||
backlogItems,
|
|
||||||
projects,
|
projects,
|
||||||
roadmapItems,
|
roadmapItems,
|
||||||
featureParentActions,
|
|
||||||
capabilities,
|
capabilities,
|
||||||
hideDone,
|
hideDone,
|
||||||
setHideDone,
|
setHideDone,
|
||||||
|
|
@ -53,50 +49,19 @@ function PlanSprintInner() {
|
||||||
handleMoveActionToSprint,
|
handleMoveActionToSprint,
|
||||||
handleUnplanAction,
|
handleUnplanAction,
|
||||||
handleConvertBacklog,
|
handleConvertBacklog,
|
||||||
handleBulkConvertBacklog,
|
|
||||||
handleCreateBacklog,
|
|
||||||
handleUpdateBacklog,
|
|
||||||
handleReorderBacklog,
|
|
||||||
handleDeleteBacklog,
|
|
||||||
reloadSlices,
|
reloadSlices,
|
||||||
} = ops
|
} = ops
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadSlices(['steering_snapshot', 'backlog'])
|
reloadSlices(['steering_snapshot'])
|
||||||
}, [reloadSlices])
|
|
||||||
|
|
||||||
const reloadAfterSprintCommit = useCallback(async () => {
|
|
||||||
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
|
||||||
}, [reloadSlices])
|
}, [reloadSlices])
|
||||||
|
|
||||||
const onAcceptBacklogProposal = useCallback(
|
const onAcceptBacklogProposal = useCallback(
|
||||||
async (itemId, options) => {
|
async (itemId, options) => {
|
||||||
await handleConvertBacklog(itemId, {
|
await handleConvertBacklog(itemId, options)
|
||||||
work_cycle_id: options?.work_cycle_id || selectedWorkCycleId,
|
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
||||||
})
|
|
||||||
await reloadAfterSprintCommit()
|
|
||||||
},
|
},
|
||||||
[handleConvertBacklog, selectedWorkCycleId, reloadAfterSprintCommit],
|
[handleConvertBacklog, reloadSlices],
|
||||||
)
|
|
||||||
|
|
||||||
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],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const proposalFilterContext = useMemo(
|
const proposalFilterContext = useMemo(
|
||||||
|
|
@ -148,33 +113,11 @@ function PlanSprintInner() {
|
||||||
busy={formBusy}
|
busy={formBusy}
|
||||||
/>
|
/>
|
||||||
{selectedWorkCycleId && (
|
{selectedWorkCycleId && (
|
||||||
<>
|
<InitiativeCompositionSurface
|
||||||
<InitiativeCompositionSurface
|
surfaceKey="plan.sprint"
|
||||||
surfaceKey="plan.sprint"
|
filterContext={proposalFilterContext}
|
||||||
filterContext={proposalFilterContext}
|
onAcceptBacklogProposal={onAcceptBacklogProposal}
|
||||||
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 && (
|
{selectedWorkCycleId && selectedWorkCycle && (
|
||||||
<InitiativeActionsHub
|
<InitiativeActionsHub
|
||||||
|
|
@ -207,7 +150,7 @@ function PlanSprintInner() {
|
||||||
workCycles={workCycles}
|
workCycles={workCycles}
|
||||||
activeWorkCycle={selectedWorkCycle}
|
activeWorkCycle={selectedWorkCycle}
|
||||||
allActions={actions}
|
allActions={actions}
|
||||||
sectionTitle={`${PRODUCT_TERMS.sprintBacklog} — ${selectedWorkCycle.title}`}
|
sectionTitle={`Sprint-Backlog — ${selectedWorkCycle.title}`}
|
||||||
sectionLead={sprintLead}
|
sectionLead={sprintLead}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,51 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
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 { LoadingState } from '../../components/LoadingState.jsx'
|
||||||
import { EmptyState } from '../../components/EmptyState.jsx'
|
import { EmptyState } from '../../components/EmptyState.jsx'
|
||||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||||
import { scopedPath } from '../../utils/routes.js'
|
import { scopedPath } from '../../utils/routes.js'
|
||||||
import { PRODUCT_TERMS } from '../../constants/productLanguage.js'
|
|
||||||
|
|
||||||
function WorkSprintInner() {
|
function WorkSprintInner() {
|
||||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||||
const ops = useInitiativeOperations()
|
const ops = useInitiativeOperations()
|
||||||
const {
|
const {
|
||||||
activeWorkCycle,
|
activeWorkCycle,
|
||||||
|
sprintActions,
|
||||||
|
projects,
|
||||||
|
roadmapItems,
|
||||||
|
capabilities,
|
||||||
|
hideDone,
|
||||||
|
setHideDone,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
formBusy,
|
||||||
|
showActionForm,
|
||||||
|
setShowActionForm,
|
||||||
|
editingActionId,
|
||||||
|
setEditingActionId,
|
||||||
|
actionContextById,
|
||||||
|
actors,
|
||||||
|
actorsLoading,
|
||||||
|
actorsError,
|
||||||
|
actorsUsedFallback,
|
||||||
|
reloadActors,
|
||||||
|
handleCreateAction,
|
||||||
|
handleUpdateAction,
|
||||||
|
handleQuickStatus,
|
||||||
|
handleCreateBlockerForAction,
|
||||||
|
handleMoveActionToSprint,
|
||||||
|
handleUnplanAction,
|
||||||
|
initiative,
|
||||||
operatingContext,
|
operatingContext,
|
||||||
dataSlices,
|
dataSlices,
|
||||||
uiFeatures,
|
uiFeatures,
|
||||||
steeringSnapshot,
|
steeringSnapshot,
|
||||||
steeringSnapshotLoading,
|
steeringSnapshotLoading,
|
||||||
|
visibleActions,
|
||||||
reloadSlices,
|
reloadSlices,
|
||||||
} = ops
|
} = ops
|
||||||
|
|
||||||
|
|
@ -40,63 +65,157 @@ function WorkSprintInner() {
|
||||||
reloadSlices,
|
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) {
|
if (loading) {
|
||||||
return <LoadingState message={`Lade ${PRODUCT_TERMS.sprintBacklog} …`} />
|
return <LoadingState message="Lade Sprint-Backlog …" />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!supportsWorkCycles) {
|
const isProductArchetype = isContinuousProductMode
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
message={`${PRODUCT_TERMS.sprintBacklog} ist für diesen Vorhabentyp nicht verfügbar.`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && <p className="error">{error}</p>}
|
{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>
|
<section className="card sprint-context-banner">
|
||||||
<p className="section-lead muted">
|
<h2>Product-Ist (ohne aktiven Sprint)</h2>
|
||||||
Kein aktiver {PRODUCT_TERMS.workCycle} — Kairo steuert nach{' '}
|
<p className="section-lead muted">
|
||||||
<strong>{operatingContext?.method_key || 'continuous_product'}</strong> (wirkungsvollster
|
Kein aktiver Sprint — Kairo steuert nach{' '}
|
||||||
Schritt). {PRODUCT_TERMS.workCycle}-Planung unter{' '}
|
<strong>{operatingContext?.method_key || 'continuous_product'}</strong> (wirkungsvollster Schritt). Sprint-Planung
|
||||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
unter{' '}
|
||||||
Plan → Sprint planen
|
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||||
</Link>
|
Plan → Sprint
|
||||||
.
|
</Link>
|
||||||
</p>
|
.
|
||||||
</section>
|
</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 && (
|
{activeWorkCycle && (
|
||||||
<section className="card sprint-context-banner">
|
<>
|
||||||
<h2>{PRODUCT_TERMS.sprintBacklog}</h2>
|
<section className="card sprint-context-banner">
|
||||||
<p className="section-lead muted">
|
<h2>Sprint-Backlog</h2>
|
||||||
<strong>{activeWorkCycle.title}</strong>
|
<p className="section-lead muted">
|
||||||
{activeWorkCycle.goal_description && ` — ${activeWorkCycle.goal_description}`}
|
<strong>{activeWorkCycle.title}</strong>
|
||||||
{' · '}
|
{activeWorkCycle.goal_description && ` — ${activeWorkCycle.goal_description}`}
|
||||||
{PRODUCT_TERMS.productBacklog} triagierst du unter{' '}
|
{' · '}
|
||||||
<Link to={scopedPath('/plan/inbox', { initiativeId })} className="link-inline">
|
Product Backlog triagierst du unter{' '}
|
||||||
Plan → {PRODUCT_TERMS.intake}
|
<Link to={scopedPath('/plan/inbox', { initiativeId })} className="link-inline">
|
||||||
</Link>
|
Plan → Eingang
|
||||||
.
|
</Link>
|
||||||
</p>
|
.
|
||||||
</section>
|
</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() {
|
export function WorkSprintPage() {
|
||||||
return (
|
return (
|
||||||
<RequireInitiativeScope
|
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein passendes Vorhaben im Scope.">
|
||||||
lead={`${PRODUCT_TERMS.sprintBacklog} ist vorhabenspezifisch — wähle ein passendes Vorhaben im Scope.`}
|
|
||||||
>
|
|
||||||
<WorkSprintInner />
|
<WorkSprintInner />
|
||||||
</RequireInitiativeScope>
|
</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 { 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() {
|
export function WorkTodayPage() {
|
||||||
const { initiativeId } = useProgramScope()
|
return (
|
||||||
|
<section className="card">
|
||||||
if (!initiativeId) {
|
<WorkspaceTodayWidget />
|
||||||
return <WorkTodayPortfolioView />
|
</section>
|
||||||
}
|
)
|
||||||
|
|
||||||
return <WorkTodayInitiativeView />
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const STEERING_ELEMENT_UI = {
|
||||||
|
|
||||||
/** Labels für Steuerungselemente auf Plan-Profil (Anzeige, nicht Verhalten). */
|
/** Labels für Steuerungselemente auf Plan-Profil (Anzeige, nicht Verhalten). */
|
||||||
export const STEERING_ELEMENT_PLAN_LABELS = {
|
export const STEERING_ELEMENT_PLAN_LABELS = {
|
||||||
work_cycle_scope: 'Sprint',
|
work_cycle_scope: 'Sprint-Zeitbox',
|
||||||
backlog_epic_hierarchy: 'Epic-Hierarchie im Eingang',
|
backlog_epic_hierarchy: 'Epic-Hierarchie im Eingang',
|
||||||
gate_fulfillment: 'Gate-Fortschritt',
|
gate_fulfillment: 'Gate-Fortschritt',
|
||||||
queue_inbox: 'Queue-Eingang',
|
queue_inbox: 'Queue-Eingang',
|
||||||
|
|
|
||||||
|
|
@ -1093,11 +1093,6 @@
|
||||||
padding: 0 1rem 1rem;
|
padding: 0 1rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-section__toggle {
|
|
||||||
margin: 0.75rem 0 0;
|
|
||||||
padding: 0 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.initiative-advanced-stack {
|
.initiative-advanced-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { ErrorState } from '../components/ErrorState.jsx'
|
||||||
import { LoadingState } from '../components/LoadingState.jsx'
|
import { LoadingState } from '../components/LoadingState.jsx'
|
||||||
import { WidgetCard } from '../components/WidgetCard.jsx'
|
import { WidgetCard } from '../components/WidgetCard.jsx'
|
||||||
import { actionPath, scopedPath } from '../utils/routes.js'
|
import { actionPath, scopedPath } from '../utils/routes.js'
|
||||||
import { PRODUCT_TERMS } from '../constants/productLanguage.js'
|
|
||||||
|
|
||||||
function formatDue(iso) {
|
function formatDue(iso) {
|
||||||
if (!iso) return null
|
if (!iso) return null
|
||||||
|
|
@ -24,7 +23,7 @@ function formatDue(iso) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceTodayWidget({ limit = 10 }) {
|
export function WorkspaceTodayWidget() {
|
||||||
const [actions, setActions] = useState([])
|
const [actions, setActions] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
|
@ -33,34 +32,32 @@ export function WorkspaceTodayWidget({ limit = 10 }) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const data = await listWorkspaceTodayActions(limit)
|
const data = await listWorkspaceTodayActions(10)
|
||||||
setActions(Array.isArray(data) ? data : [])
|
setActions(Array.isArray(data) ? data : [])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [limit])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
const hasMoreHint = limit < 10
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetCard
|
<WidgetCard
|
||||||
title="Heute"
|
title="Heute"
|
||||||
subtitle={`Deine ${PRODUCT_TERMS.actionPlural} — überfällig und dringend zuerst (max. ${limit})`}
|
subtitle="Deine Maßnahmen — überfällig und dringend zuerst"
|
||||||
actions={
|
actions={
|
||||||
<button type="button" className="btn btn-ghost" onClick={load} disabled={loading}>
|
<button type="button" className="btn btn-ghost" onClick={load} disabled={loading}>
|
||||||
Aktualisieren
|
Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
actions.length > 0 || hasMoreHint ? (
|
actions.length > 0 ? (
|
||||||
<Link to="/work/mine" className="link-inline">
|
<Link to="/work/mine" className="link-inline">
|
||||||
Alle {PRODUCT_TERMS.actionPossessive} →
|
Alle meine Maßnahmen →
|
||||||
</Link>
|
</Link>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +65,7 @@ export function WorkspaceTodayWidget({ limit = 10 }) {
|
||||||
{loading && <LoadingState />}
|
{loading && <LoadingState />}
|
||||||
{!loading && error && <ErrorState message={error} onRetry={load} />}
|
{!loading && error && <ErrorState message={error} onRetry={load} />}
|
||||||
{!loading && !error && actions.length === 0 && (
|
{!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 && (
|
{!loading && !error && actions.length > 0 && (
|
||||||
<ul className="item-list">
|
<ul className="item-list">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user