From a520d54bcfab5d051f22b49becd8064cb51cc65f Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 5 Jul 2026 17:39:18 +0200 Subject: [PATCH] =?UTF-8?q?AP1.1b:=20Vorhaben-Detail=20als=20Steuerungsfl?= =?UTF-8?q?=C3=A4che=20=E2=80=94=20Action-Hub=20statt=20paralleler=20Liste?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leitfrage oben, Maßnahmen mit verknüpftem Kontext aus dem Snapshot, vollständiges Meilenstein-Formular, Backlog/Entscheidungen/Nachweise unter Erweitert. Version 0.11.2-ap1.1b. --- backend/version.py | 2 +- frontend/package.json | 2 +- frontend/src/components/ActionHubCard.jsx | 173 ++++++++++ frontend/src/components/BlockersSection.jsx | 22 +- .../src/components/CollapsibleSection.jsx | 29 ++ .../src/components/InitiativeActionsHub.jsx | 103 ++++++ frontend/src/components/MilestonesSection.jsx | 63 +++- .../src/components/SteeringSnapshotPanel.jsx | 196 ++++++------ frontend/src/pages/InitiativeDetailPage.jsx | 299 ++++++------------ frontend/src/styles/components.css | 277 ++++++++++++++++ 10 files changed, 852 insertions(+), 314 deletions(-) create mode 100644 frontend/src/components/ActionHubCard.jsx create mode 100644 frontend/src/components/CollapsibleSection.jsx create mode 100644 frontend/src/components/InitiativeActionsHub.jsx diff --git a/backend/version.py b/backend/version.py index f7db55d..e5b558f 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,3 +1,3 @@ -APP_VERSION = "0.11.1-ap1.1" +APP_VERSION = "0.11.2-ap1.1b" DB_SCHEMA_VERSION = "009" APP_NAME = "jinkendo-kairo" diff --git a/frontend/package.json b/frontend/package.json index d052422..f37e4a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "kairo-jinkendo-frontend", - "version": "0.11.1-ap1.1", + "version": "0.11.2-ap1.1b", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/components/ActionHubCard.jsx b/frontend/src/components/ActionHubCard.jsx new file mode 100644 index 0000000..ebb1be7 --- /dev/null +++ b/frontend/src/components/ActionHubCard.jsx @@ -0,0 +1,173 @@ +import { ACTION_STATUSES, ACTION_STATUS_LABELS } from '../constants/status.js' +import { StatusBadge } from './StatusBadge.jsx' +import { PriorityBadge } from './PriorityBadge.jsx' +import { ActionForm } from './ActionForm.jsx' + +function formatDue(iso) { + if (!iso) return null + try { + return new Date(iso).toLocaleString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } catch { + return iso + } +} + +function LinkedItems({ title, items, renderItem }) { + if (!items?.length) return null + return ( +
+

{title}

+ +
+ ) +} + +export function ActionHubCard({ + action, + context, + editing, + onEdit, + onCancelEdit, + onSubmitEdit, + onQuickStatus, + onCreateBlocker, + canManage, + canManageBlocker, + formBusy, + actors, + actorsLoading, + actorsError, + actorsUsedFallback, + onReloadActors, +}) { + const due = formatDue(action.due_at) + const isBlocked = action.status === 'blocked' || context?.has_open_blocker + + if (editing) { + return ( +
+ +
+ ) + } + + return ( +
+
+
+

{action.title}

+
+ + +
+
+ {action.description && ( +

{action.description}

+ )} +
+ {action.assigned_actor_ids?.length > 0 && ( + {action.assigned_actor_ids.length} zugewiesen + )} + {due && Fällig: {due}} +
+
+ + {context && ( + <> + + ['open', 'in_progress'].includes(b.status) + )} + renderItem={(b) => ( + <> + + {b.title} + + )} + /> + ( + <> + + {e.title} + + )} + /> + ( + <> + + {r.title} + {r.due_at && ( + · fällig {formatDue(r.due_at)} + )} + + )} + /> + + )} + + {canManage && ( +
+ + {canManageBlocker && action.status !== 'done' && ( + + )} + +
+ )} +
+ ) +} diff --git a/frontend/src/components/BlockersSection.jsx b/frontend/src/components/BlockersSection.jsx index e25d852..ec0c0c4 100644 --- a/frontend/src/components/BlockersSection.jsx +++ b/frontend/src/components/BlockersSection.jsx @@ -13,22 +13,28 @@ export function BlockersSection({ onUpdateStatus, onDelete, busy, + heading = 'Blocker', + lead = null, + emptyMessage = 'Keine Blocker.', }) { - const [title, setTitle] = useState('') + const [formTitle, setFormTitle] = useState('') const [showForm, setShowForm] = useState(false) async function handleSubmit(e) { e.preventDefault() - if (!title.trim()) return - await onCreate({ title: title.trim() }) - setTitle('') + if (!formTitle.trim()) return + await onCreate({ title: formTitle.trim() }) + setFormTitle('') setShowForm(false) } return (
-

Blocker

+
+

{heading}

+ {lead &&

{lead}

} +
{canManage && ( + {open &&
{children}
} +
+ ) +} diff --git a/frontend/src/components/InitiativeActionsHub.jsx b/frontend/src/components/InitiativeActionsHub.jsx new file mode 100644 index 0000000..69f2363 --- /dev/null +++ b/frontend/src/components/InitiativeActionsHub.jsx @@ -0,0 +1,103 @@ +import { ActionHubCard } from './ActionHubCard.jsx' +import { ActionForm } from './ActionForm.jsx' +import { EmptyState } from './EmptyState.jsx' + +export function InitiativeActionsHub({ + actions, + actionContextById, + hideDone, + onHideDoneChange, + showForm, + onToggleForm, + editingActionId, + onEditAction, + onCancelEdit, + onCreateAction, + onUpdateAction, + onQuickStatus, + onCreateBlockerForAction, + canManage, + canManageBlocker, + formBusy, + actors, + actorsLoading, + actorsError, + actorsUsedFallback, + onReloadActors, +}) { + return ( +
+
+
+

Maßnahmen — operative Arbeit

+

+ Blocker, Nachweise und Reviews hängen an der Maßnahme, nicht lose im Vorhaben. +

+
+
+ + {canManage && ( + + )} +
+
+ + {showForm && canManage && ( +
+ +
+ )} + + {actions.length === 0 && ( + + )} + +
+ {actions.map((action) => ( + onEditAction(action.id)} + onCancelEdit={onCancelEdit} + onSubmitEdit={(payload) => onUpdateAction(action.id, payload)} + onQuickStatus={(status) => onQuickStatus(action, status)} + onCreateBlocker={() => onCreateBlockerForAction(action.id)} + canManage={canManage} + canManageBlocker={canManageBlocker} + formBusy={formBusy} + actors={actors} + actorsLoading={actorsLoading} + actorsError={actorsError} + actorsUsedFallback={actorsUsedFallback} + onReloadActors={onReloadActors} + /> + ))} +
+
+ ) +} diff --git a/frontend/src/components/MilestonesSection.jsx b/frontend/src/components/MilestonesSection.jsx index 918bfbe..26b8960 100644 --- a/frontend/src/components/MilestonesSection.jsx +++ b/frontend/src/components/MilestonesSection.jsx @@ -6,6 +6,17 @@ import { import { StatusBadge } from './StatusBadge.jsx' import { EmptyState } from './EmptyState.jsx' +function formatDate(value) { + if (!value) return null + try { + return new Date(value.includes('T') ? value : `${value}T12:00:00`).toLocaleDateString( + 'de-DE' + ) + } catch { + return value + } +} + export function MilestonesSection({ milestones, canManage, @@ -13,22 +24,36 @@ export function MilestonesSection({ onUpdateStatus, onDelete, busy, + compact = false, }) { const [title, setTitle] = useState('') + const [goalDescription, setGoalDescription] = useState('') + const [targetDate, setTargetDate] = useState('') const [showForm, setShowForm] = useState(false) async function handleSubmit(e) { e.preventDefault() if (!title.trim()) return - await onCreate({ title: title.trim() }) + await onCreate({ + title: title.trim(), + goal_description: goalDescription.trim(), + target_date: targetDate || undefined, + }) setTitle('') + setGoalDescription('') + setTargetDate('') setShowForm(false) } return ( -
+
-

Meilensteine

+
+

Meilensteine

+

+ Überprüfbare Zielpunkte — kein Task. Ziel und Termin machen den Horizont steuerbar. +

+
{canManage && (