AP1.1b: Vorhaben-Detail als Steuerungsfläche — Action-Hub statt paralleler Listen
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 1m26s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 1m26s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
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.
This commit is contained in:
parent
fc956d4ceb
commit
a520d54bcf
|
|
@ -1,3 +1,3 @@
|
||||||
APP_VERSION = "0.11.1-ap1.1"
|
APP_VERSION = "0.11.2-ap1.1b"
|
||||||
DB_SCHEMA_VERSION = "009"
|
DB_SCHEMA_VERSION = "009"
|
||||||
APP_NAME = "jinkendo-kairo"
|
APP_NAME = "jinkendo-kairo"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "kairo-jinkendo-frontend",
|
"name": "kairo-jinkendo-frontend",
|
||||||
"version": "0.11.1-ap1.1",
|
"version": "0.11.2-ap1.1b",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
173
frontend/src/components/ActionHubCard.jsx
Normal file
173
frontend/src/components/ActionHubCard.jsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="action-hub-linked">
|
||||||
|
<h4 className="action-hub-linked-title">{title}</h4>
|
||||||
|
<ul className="action-hub-linked-list">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.id}>{renderItem(item)}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<article className="action-hub-card action-hub-card--editing">
|
||||||
|
<ActionForm
|
||||||
|
initial={action}
|
||||||
|
actors={actors}
|
||||||
|
actorsLoading={actorsLoading}
|
||||||
|
actorsError={actorsError}
|
||||||
|
actorsUsedFallback={actorsUsedFallback}
|
||||||
|
onReloadActors={onReloadActors}
|
||||||
|
onSubmit={onSubmitEdit}
|
||||||
|
onCancel={onCancelEdit}
|
||||||
|
busy={formBusy}
|
||||||
|
submitLabel="Speichern"
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={`action-hub-card${isBlocked ? ' action-hub-card--blocked' : ''}`}
|
||||||
|
>
|
||||||
|
<header className="action-hub-card-header">
|
||||||
|
<div className="action-hub-card-title-row">
|
||||||
|
<h3 className="action-hub-card-title">{action.title}</h3>
|
||||||
|
<div className="action-hub-card-badges">
|
||||||
|
<StatusBadge status={action.status} />
|
||||||
|
<PriorityBadge priority={action.priority} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{action.description && (
|
||||||
|
<p className="action-hub-card-desc">{action.description}</p>
|
||||||
|
)}
|
||||||
|
<div className="action-hub-card-meta muted">
|
||||||
|
{action.assigned_actor_ids?.length > 0 && (
|
||||||
|
<span>{action.assigned_actor_ids.length} zugewiesen</span>
|
||||||
|
)}
|
||||||
|
{due && <span>Fällig: {due}</span>}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{context && (
|
||||||
|
<>
|
||||||
|
<LinkedItems
|
||||||
|
title="Blocker"
|
||||||
|
items={context.blockers?.filter((b) =>
|
||||||
|
['open', 'in_progress'].includes(b.status)
|
||||||
|
)}
|
||||||
|
renderItem={(b) => (
|
||||||
|
<>
|
||||||
|
<StatusBadge kind="blocker" status={b.status} />
|
||||||
|
<strong>{b.title}</strong>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<LinkedItems
|
||||||
|
title="Nachweise"
|
||||||
|
items={context.evidence}
|
||||||
|
renderItem={(e) => (
|
||||||
|
<>
|
||||||
|
<StatusBadge kind="evidence" status={e.status} />
|
||||||
|
{e.title}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<LinkedItems
|
||||||
|
title="Reviews"
|
||||||
|
items={context.reviews}
|
||||||
|
renderItem={(r) => (
|
||||||
|
<>
|
||||||
|
<StatusBadge kind="review" status={r.status} />
|
||||||
|
{r.title}
|
||||||
|
{r.due_at && (
|
||||||
|
<span className="muted"> · fällig {formatDue(r.due_at)}</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canManage && (
|
||||||
|
<footer className="action-hub-card-actions">
|
||||||
|
<select
|
||||||
|
className="inline-select"
|
||||||
|
value={action.status}
|
||||||
|
onChange={(e) => onQuickStatus(e.target.value)}
|
||||||
|
aria-label="Status ändern"
|
||||||
|
>
|
||||||
|
{ACTION_STATUSES.map((s) => (
|
||||||
|
<option key={s} value={s}>
|
||||||
|
{ACTION_STATUS_LABELS[s]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{canManageBlocker && action.status !== 'done' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-sm"
|
||||||
|
onClick={onCreateBlocker}
|
||||||
|
disabled={formBusy}
|
||||||
|
>
|
||||||
|
Blocker melden
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
onClick={onEdit}
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -13,22 +13,28 @@ export function BlockersSection({
|
||||||
onUpdateStatus,
|
onUpdateStatus,
|
||||||
onDelete,
|
onDelete,
|
||||||
busy,
|
busy,
|
||||||
|
heading = 'Blocker',
|
||||||
|
lead = null,
|
||||||
|
emptyMessage = 'Keine Blocker.',
|
||||||
}) {
|
}) {
|
||||||
const [title, setTitle] = useState('')
|
const [formTitle, setFormTitle] = useState('')
|
||||||
const [showForm, setShowForm] = useState(false)
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!title.trim()) return
|
if (!formTitle.trim()) return
|
||||||
await onCreate({ title: title.trim() })
|
await onCreate({ title: formTitle.trim() })
|
||||||
setTitle('')
|
setFormTitle('')
|
||||||
setShowForm(false)
|
setShowForm(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h2>Blocker</h2>
|
<div>
|
||||||
|
<h2>{heading}</h2>
|
||||||
|
{lead && <p className="section-lead muted">{lead}</p>}
|
||||||
|
</div>
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -45,8 +51,8 @@ export function BlockersSection({
|
||||||
<label>
|
<label>
|
||||||
Titel
|
Titel
|
||||||
<input
|
<input
|
||||||
value={title}
|
value={formTitle}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setFormTitle(e.target.value)}
|
||||||
maxLength={255}
|
maxLength={255}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -57,7 +63,7 @@ export function BlockersSection({
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{blockers.length === 0 && <EmptyState message="Keine Blocker." />}
|
{blockers.length === 0 && <EmptyState message={emptyMessage} />}
|
||||||
|
|
||||||
<ul className="item-list">
|
<ul className="item-list">
|
||||||
{blockers.map((blocker) => (
|
{blockers.map((blocker) => (
|
||||||
|
|
|
||||||
29
frontend/src/components/CollapsibleSection.jsx
Normal file
29
frontend/src/components/CollapsibleSection.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export function CollapsibleSection({
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
defaultOpen = false,
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(defaultOpen)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`collapsible-section card ${className}`.trim()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="collapsible-section-trigger"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{open && <div className="collapsible-section-body">{children}</div>}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
103
frontend/src/components/InitiativeActionsHub.jsx
Normal file
103
frontend/src/components/InitiativeActionsHub.jsx
Normal file
|
|
@ -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 (
|
||||||
|
<section className="card initiative-actions-hub">
|
||||||
|
<div className="section-header">
|
||||||
|
<div>
|
||||||
|
<h2>Maßnahmen — operative Arbeit</h2>
|
||||||
|
<p className="section-lead muted">
|
||||||
|
Blocker, Nachweise und Reviews hängen an der Maßnahme, nicht lose im Vorhaben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="section-actions">
|
||||||
|
<label className="checkbox-label inline-filter">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={hideDone}
|
||||||
|
onChange={(e) => onHideDoneChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Erledigte ausblenden
|
||||||
|
</label>
|
||||||
|
{canManage && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-block-mobile"
|
||||||
|
onClick={onToggleForm}
|
||||||
|
>
|
||||||
|
{showForm ? 'Abbrechen' : 'Maßnahme anlegen'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showForm && canManage && (
|
||||||
|
<div className="inline-form-block">
|
||||||
|
<ActionForm
|
||||||
|
actors={actors}
|
||||||
|
actorsLoading={actorsLoading}
|
||||||
|
actorsError={actorsError}
|
||||||
|
actorsUsedFallback={actorsUsedFallback}
|
||||||
|
onReloadActors={onReloadActors}
|
||||||
|
onSubmit={onCreateAction}
|
||||||
|
onCancel={onToggleForm}
|
||||||
|
busy={formBusy}
|
||||||
|
submitLabel="Maßnahme anlegen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{actions.length === 0 && (
|
||||||
|
<EmptyState message="Noch keine Maßnahmen — lege die erste operative Maßnahme an oder wandle Backlog um." />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="action-hub-grid">
|
||||||
|
{actions.map((action) => (
|
||||||
|
<ActionHubCard
|
||||||
|
key={action.id}
|
||||||
|
action={action}
|
||||||
|
context={actionContextById[action.id]}
|
||||||
|
editing={editingActionId === action.id}
|
||||||
|
onEdit={() => 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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,17 @@ import {
|
||||||
import { StatusBadge } from './StatusBadge.jsx'
|
import { StatusBadge } from './StatusBadge.jsx'
|
||||||
import { EmptyState } from './EmptyState.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({
|
export function MilestonesSection({
|
||||||
milestones,
|
milestones,
|
||||||
canManage,
|
canManage,
|
||||||
|
|
@ -13,22 +24,36 @@ export function MilestonesSection({
|
||||||
onUpdateStatus,
|
onUpdateStatus,
|
||||||
onDelete,
|
onDelete,
|
||||||
busy,
|
busy,
|
||||||
|
compact = false,
|
||||||
}) {
|
}) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
|
const [goalDescription, setGoalDescription] = useState('')
|
||||||
|
const [targetDate, setTargetDate] = useState('')
|
||||||
const [showForm, setShowForm] = useState(false)
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!title.trim()) return
|
if (!title.trim()) return
|
||||||
await onCreate({ title: title.trim() })
|
await onCreate({
|
||||||
|
title: title.trim(),
|
||||||
|
goal_description: goalDescription.trim(),
|
||||||
|
target_date: targetDate || undefined,
|
||||||
|
})
|
||||||
setTitle('')
|
setTitle('')
|
||||||
|
setGoalDescription('')
|
||||||
|
setTargetDate('')
|
||||||
setShowForm(false)
|
setShowForm(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className={`card${compact ? ' card--secondary' : ''}`}>
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
|
<div>
|
||||||
<h2>Meilensteine</h2>
|
<h2>Meilensteine</h2>
|
||||||
|
<p className="section-lead muted">
|
||||||
|
Überprüfbare Zielpunkte — kein Task. Ziel und Termin machen den Horizont steuerbar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -41,7 +66,7 @@ export function MilestonesSection({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showForm && canManage && (
|
{showForm && canManage && (
|
||||||
<form className="inline-form-block" onSubmit={handleSubmit}>
|
<form className="inline-form-block milestone-form" onSubmit={handleSubmit}>
|
||||||
<label>
|
<label>
|
||||||
Titel
|
Titel
|
||||||
<input
|
<input
|
||||||
|
|
@ -49,6 +74,24 @@ export function MilestonesSection({
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
maxLength={255}
|
maxLength={255}
|
||||||
required
|
required
|
||||||
|
placeholder="z. B. MVP nutzbar im Alltag"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Ziel / Definition of Done
|
||||||
|
<textarea
|
||||||
|
value={goalDescription}
|
||||||
|
onChange={(e) => setGoalDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Woran erkennst du, dass dieser Meilenstein erreicht ist?"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Zieltermin
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={targetDate}
|
||||||
|
onChange={(e) => setTargetDate(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" className="btn btn-primary" disabled={busy}>
|
<button type="submit" className="btn btn-primary" disabled={busy}>
|
||||||
|
|
@ -57,18 +100,22 @@ export function MilestonesSection({
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{milestones.length === 0 && <EmptyState message="Keine Meilensteine." />}
|
{milestones.length === 0 && (
|
||||||
|
<EmptyState message="Noch keine Meilensteine — strukturiere das Vorhaben in überprüfbare Zielpunkte." />
|
||||||
|
)}
|
||||||
|
|
||||||
<ul className="item-list">
|
<ul className="item-list milestone-list">
|
||||||
{milestones.map((milestone) => (
|
{milestones.map((milestone) => (
|
||||||
<li key={milestone.id} className="list-item card-list-item">
|
<li key={milestone.id} className="list-item card-list-item milestone-list-item">
|
||||||
<div className="list-item-main">
|
<div className="list-item-main">
|
||||||
<strong>{milestone.title}</strong>
|
<strong>{milestone.title}</strong>
|
||||||
{milestone.goal_description && (
|
{milestone.goal_description && (
|
||||||
<p className="list-item-desc">{milestone.goal_description}</p>
|
<p className="list-item-desc">{milestone.goal_description}</p>
|
||||||
)}
|
)}
|
||||||
{milestone.target_date && (
|
{milestone.target_date && (
|
||||||
<p className="muted list-item-sub">Ziel: {milestone.target_date}</p>
|
<p className="muted list-item-sub">
|
||||||
|
Zieltermin: {formatDate(milestone.target_date)}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="list-item-meta action-controls">
|
<div className="list-item-meta action-controls">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import {
|
import {
|
||||||
LIFECYCLE_LABELS,
|
LIFECYCLE_LABELS,
|
||||||
PHASE_LABELS,
|
PHASE_LABELS,
|
||||||
|
|
@ -69,13 +68,28 @@ export function SteeringSnapshotPanel({
|
||||||
? null
|
? null
|
||||||
: PHASE_DESCRIPTIONS[operating_phase] || ''
|
: PHASE_DESCRIPTIONS[operating_phase] || ''
|
||||||
|
|
||||||
return (
|
const statPills = [
|
||||||
<section className="card steering-snapshot">
|
{ label: 'Offen', value: counts.actions_open, warn: false },
|
||||||
<div className="section-header">
|
{ label: 'Blockiert', value: counts.actions_blocked, warn: counts.actions_blocked > 0 },
|
||||||
<h2>Steuerungszustand</h2>
|
{ label: 'Review', value: counts.actions_review_required, warn: counts.actions_review_required > 0 },
|
||||||
<span className="badge status-badge status-active">{displayLabel}</span>
|
{ label: 'Blocker', value: counts.blockers_open, warn: counts.blockers_open > 0 },
|
||||||
</div>
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card steering-snapshot steering-snapshot--hero">
|
||||||
|
<header className="steering-hero-header">
|
||||||
|
<div>
|
||||||
|
<p className="steering-hero-kicker">Leitfrage</p>
|
||||||
|
<h2 className="steering-hero-title">
|
||||||
|
Wo stehe ich — und was ist als Nächstes dran?
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span className="badge status-badge status-active steering-lifecycle-badge">
|
||||||
|
{displayLabel}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="steering-hero-toolbar">
|
||||||
{(method_label || canManageMethod) && (
|
{(method_label || canManageMethod) && (
|
||||||
<p className="steering-method-row">
|
<p className="steering-method-row">
|
||||||
<span className="muted">Methode: </span>
|
<span className="muted">Methode: </span>
|
||||||
|
|
@ -99,6 +113,19 @@ export function SteeringSnapshotPanel({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ul className="steering-stat-pills" aria-label="Kurzüberblick">
|
||||||
|
{statPills.map((pill) => (
|
||||||
|
<li
|
||||||
|
key={pill.label}
|
||||||
|
className={`steering-stat-pill${pill.warn ? ' steering-stat-pill--warn' : ''}`}
|
||||||
|
>
|
||||||
|
<span className="steering-stat-pill-value">{pill.value}</span>
|
||||||
|
<span className="steering-stat-pill-label">{pill.label}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{phaseDesc && <p className="steering-phase-desc">{phaseDesc}</p>}
|
{phaseDesc && <p className="steering-phase-desc">{phaseDesc}</p>}
|
||||||
|
|
||||||
{phase_signals?.length > 0 && (
|
{phase_signals?.length > 0 && (
|
||||||
|
|
@ -107,9 +134,10 @@ export function SteeringSnapshotPanel({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="steering-hero-columns">
|
||||||
{next_actions.length > 0 && (
|
{next_actions.length > 0 && (
|
||||||
<div className="steering-next-actions">
|
<div className="steering-next-actions">
|
||||||
<h3>Nächste Schritte für dieses Vorhaben</h3>
|
<h3>Nächste Schritte</h3>
|
||||||
<ol className="item-list compact-list">
|
<ol className="item-list compact-list">
|
||||||
{next_actions.map((item, i) => (
|
{next_actions.map((item, i) => (
|
||||||
<li key={`${item.kind}-${i}`} className="list-item card-list-item compact">
|
<li key={`${item.kind}-${i}`} className="list-item card-list-item compact">
|
||||||
|
|
@ -145,38 +173,16 @@ export function SteeringSnapshotPanel({
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<dl className="snapshot-counts">
|
|
||||||
<div>
|
|
||||||
<dt>Offene Maßnahmen</dt>
|
|
||||||
<dd>{counts.actions_open}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Blockiert</dt>
|
|
||||||
<dd>{counts.actions_blocked}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Review nötig</dt>
|
|
||||||
<dd>{counts.actions_review_required}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Offene Blocker</dt>
|
|
||||||
<dd>{counts.blockers_open}</dd>
|
|
||||||
</div>
|
|
||||||
{(counts.unlinked_blockers > 0 || counts.unlinked_evidence > 0) && (
|
{(counts.unlinked_blockers > 0 || counts.unlinked_evidence > 0) && (
|
||||||
<div className="snapshot-warn">
|
<p className="steering-unlinked-hint muted">
|
||||||
<dt>Unverknüpft</dt>
|
{counts.unlinked_blockers > 0 && `${counts.unlinked_blockers} Blocker ohne Maßnahme`}
|
||||||
<dd>
|
{counts.unlinked_blockers > 0 && counts.unlinked_evidence > 0 && ' · '}
|
||||||
{counts.unlinked_blockers} Blocker, {counts.unlinked_evidence} Evidence
|
{counts.unlinked_evidence > 0 && `${counts.unlinked_evidence} Nachweise ohne Maßnahme`}
|
||||||
</dd>
|
{' — unter „Erweitert“ verknüpfen oder aufräumen.'}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<p className="muted snapshot-hint">
|
|
||||||
Steuerungszustand über Standard Lifecycle
|
|
||||||
{lifecycle_state ? ` (${lifecycle_state})` : ''}.
|
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,8 @@ import {
|
||||||
updateRecurring,
|
updateRecurring,
|
||||||
deleteRecurring,
|
deleteRecurring,
|
||||||
} from '../api/recurring.js'
|
} from '../api/recurring.js'
|
||||||
import { ACTION_STATUSES, ACTION_STATUS_LABELS } from '../constants/status.js'
|
|
||||||
import { StatusBadge } from '../components/StatusBadge.jsx'
|
import { StatusBadge } from '../components/StatusBadge.jsx'
|
||||||
import { PriorityBadge } from '../components/PriorityBadge.jsx'
|
import { PriorityBadge } from '../components/PriorityBadge.jsx'
|
||||||
import { ActionForm } from '../components/ActionForm.jsx'
|
|
||||||
import { BlockersSection } from '../components/BlockersSection.jsx'
|
import { BlockersSection } from '../components/BlockersSection.jsx'
|
||||||
import { BacklogSection } from '../components/BacklogSection.jsx'
|
import { BacklogSection } from '../components/BacklogSection.jsx'
|
||||||
import { MilestonesSection } from '../components/MilestonesSection.jsx'
|
import { MilestonesSection } from '../components/MilestonesSection.jsx'
|
||||||
|
|
@ -62,8 +60,9 @@ import { DecisionsSection } from '../components/DecisionsSection.jsx'
|
||||||
import { ReviewsSection } from '../components/ReviewsSection.jsx'
|
import { ReviewsSection } from '../components/ReviewsSection.jsx'
|
||||||
import { RecurringSection } from '../components/RecurringSection.jsx'
|
import { RecurringSection } from '../components/RecurringSection.jsx'
|
||||||
import { SteeringSnapshotPanel } from '../components/SteeringSnapshotPanel.jsx'
|
import { SteeringSnapshotPanel } from '../components/SteeringSnapshotPanel.jsx'
|
||||||
|
import { InitiativeActionsHub } from '../components/InitiativeActionsHub.jsx'
|
||||||
|
import { CollapsibleSection } from '../components/CollapsibleSection.jsx'
|
||||||
import { listSteeringMethods, updateInitiativeSteeringMethod } from '../api/steering.js'
|
import { listSteeringMethods, updateInitiativeSteeringMethod } from '../api/steering.js'
|
||||||
import { EmptyState } from '../components/EmptyState.jsx'
|
|
||||||
import { ErrorState } from '../components/ErrorState.jsx'
|
import { ErrorState } from '../components/ErrorState.jsx'
|
||||||
import { LoadingState } from '../components/LoadingState.jsx'
|
import { LoadingState } from '../components/LoadingState.jsx'
|
||||||
import { useCapabilities } from '../hooks/useCapabilities.js'
|
import { useCapabilities } from '../hooks/useCapabilities.js'
|
||||||
|
|
@ -165,6 +164,20 @@ export function InitiativeDetailPage() {
|
||||||
(steeringSnapshot?.actions || []).map((a) => [a.id, a])
|
(steeringSnapshot?.actions || []).map((a) => [a.id, a])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const unlinkedBlockers = blockers.filter((b) => !b.action_id)
|
||||||
|
|
||||||
|
const advancedSummary = (() => {
|
||||||
|
const c = steeringSnapshot?.counts
|
||||||
|
const parts = []
|
||||||
|
if (backlogItems.length) parts.push(`${backlogItems.length} Backlog`)
|
||||||
|
if (decisions.length) parts.push(`${decisions.length} Entscheidungen`)
|
||||||
|
if (recurringItems.length) parts.push(`${recurringItems.length} Wiederkehrend`)
|
||||||
|
if (unlinkedBlockers.length) parts.push(`${unlinkedBlockers.length} lose Blocker`)
|
||||||
|
if (c?.unlinked_evidence) parts.push(`${c.unlinked_evidence} lose Nachweise`)
|
||||||
|
if (reviews.length) parts.push(`${reviews.length} Reviews`)
|
||||||
|
return parts.length ? parts.join(' · ') : 'Backlog, Entscheidungen, Nachweise …'
|
||||||
|
})()
|
||||||
|
|
||||||
async function handleMethodChange(methodKey) {
|
async function handleMethodChange(methodKey) {
|
||||||
setMethodBusy(true)
|
setMethodBusy(true)
|
||||||
try {
|
try {
|
||||||
|
|
@ -523,140 +536,32 @@ export function InitiativeDetailPage() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="card">
|
<InitiativeActionsHub
|
||||||
<div className="section-header">
|
actions={visibleActions}
|
||||||
<h2>Maßnahmen</h2>
|
actionContextById={actionContextById}
|
||||||
<div className="section-actions">
|
hideDone={hideDone}
|
||||||
<label className="checkbox-label inline-filter">
|
onHideDoneChange={setHideDone}
|
||||||
<input
|
showForm={showActionForm}
|
||||||
type="checkbox"
|
onToggleForm={() => {
|
||||||
checked={hideDone}
|
setShowActionForm((v) => !v)
|
||||||
onChange={(e) => setHideDone(e.target.checked)}
|
setEditingAction(null)
|
||||||
/>
|
|
||||||
Erledigte ausblenden
|
|
||||||
</label>
|
|
||||||
{capabilities.has('kairo.action.manage') && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary btn-block-mobile"
|
|
||||||
onClick={() => { setShowActionForm((v) => !v); setEditingAction(null) }}
|
|
||||||
>
|
|
||||||
{showActionForm ? 'Abbrechen' : 'Maßnahme anlegen'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showActionForm && (
|
|
||||||
<div className="inline-form-block">
|
|
||||||
<ActionForm
|
|
||||||
actors={actors}
|
|
||||||
actorsLoading={actorsLoading}
|
|
||||||
actorsError={actorsError}
|
|
||||||
actorsUsedFallback={actorsUsedFallback}
|
|
||||||
onReloadActors={reloadActors}
|
|
||||||
onSubmit={handleCreateAction}
|
|
||||||
onCancel={() => setShowActionForm(false)}
|
|
||||||
busy={formBusy}
|
|
||||||
submitLabel="Maßnahme anlegen"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{visibleActions.length === 0 && (
|
|
||||||
<EmptyState message="Noch keine Maßnahmen in diesem Vorhaben." />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul className="item-list actions-detail-list">
|
|
||||||
{visibleActions.map((action) => {
|
|
||||||
const ctx = actionContextById[action.id]
|
|
||||||
return (
|
|
||||||
<li key={action.id} className="list-item card-list-item">
|
|
||||||
{editingAction === action.id ? (
|
|
||||||
<ActionForm
|
|
||||||
initial={action}
|
|
||||||
actors={actors}
|
|
||||||
actorsLoading={actorsLoading}
|
|
||||||
actorsError={actorsError}
|
|
||||||
actorsUsedFallback={actorsUsedFallback}
|
|
||||||
onReloadActors={reloadActors}
|
|
||||||
onSubmit={(payload) => handleUpdateAction(action.id, payload)}
|
|
||||||
onCancel={() => setEditingAction(null)}
|
|
||||||
busy={formBusy}
|
|
||||||
submitLabel="Speichern"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="list-item-main">
|
|
||||||
<strong>{action.title}</strong>
|
|
||||||
{action.description && (
|
|
||||||
<p className="list-item-desc">{action.description}</p>
|
|
||||||
)}
|
|
||||||
{action.assigned_actor_ids?.length > 0 && (
|
|
||||||
<p className="muted list-item-sub">
|
|
||||||
Zugewiesen: {action.assigned_actor_ids.length} Actor(s)
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{action.due_at && (
|
|
||||||
<p className="muted list-item-sub">
|
|
||||||
Fällig: {new Date(action.due_at).toLocaleString('de-DE')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{ctx && (ctx.open_blocker_count > 0 || ctx.evidence?.length > 0 || ctx.reviews?.length > 0) && (
|
|
||||||
<p className="muted list-item-sub action-links">
|
|
||||||
{ctx.open_blocker_count > 0 && `${ctx.open_blocker_count} Blocker · `}
|
|
||||||
{ctx.evidence?.length > 0 && `${ctx.evidence.length} Evidence · `}
|
|
||||||
{ctx.reviews?.length > 0 && `${ctx.reviews.length} Review(s)`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="list-item-meta action-controls">
|
|
||||||
<StatusBadge status={action.status} />
|
|
||||||
<PriorityBadge priority={action.priority} />
|
|
||||||
{capabilities.has('kairo.action.manage') && (
|
|
||||||
<>
|
|
||||||
<select
|
|
||||||
className="inline-select"
|
|
||||||
value={action.status}
|
|
||||||
onChange={(e) => handleQuickStatus(action, e.target.value)}
|
|
||||||
aria-label="Status ändern"
|
|
||||||
>
|
|
||||||
{ACTION_STATUSES.map((s) => (
|
|
||||||
<option key={s} value={s}>
|
|
||||||
{ACTION_STATUS_LABELS[s]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{capabilities.has('kairo.blocker.manage') && action.status !== 'done' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() => handleCreateBlockerForAction(action.id)}
|
|
||||||
disabled={formBusy}
|
|
||||||
>
|
|
||||||
Blocker
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingAction(action.id)
|
|
||||||
setShowActionForm(false)
|
|
||||||
}}
|
}}
|
||||||
>
|
editingActionId={editingAction}
|
||||||
Bearbeiten
|
onEditAction={setEditingAction}
|
||||||
</button>
|
onCancelEdit={() => setEditingAction(null)}
|
||||||
</>
|
onCreateAction={handleCreateAction}
|
||||||
)}
|
onUpdateAction={handleUpdateAction}
|
||||||
</div>
|
onQuickStatus={handleQuickStatus}
|
||||||
</>
|
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||||
)}
|
canManage={capabilities.has('kairo.action.manage')}
|
||||||
</li>
|
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||||
)
|
formBusy={formBusy}
|
||||||
})}
|
actors={actors}
|
||||||
</ul>
|
actorsLoading={actorsLoading}
|
||||||
</section>
|
actorsError={actorsError}
|
||||||
|
actorsUsedFallback={actorsUsedFallback}
|
||||||
|
onReloadActors={reloadActors}
|
||||||
|
/>
|
||||||
|
|
||||||
{capabilities.has('kairo.initiative.read') && (
|
{capabilities.has('kairo.initiative.read') && (
|
||||||
<MilestonesSection
|
<MilestonesSection
|
||||||
|
|
@ -670,6 +575,8 @@ export function InitiativeDetailPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{capabilities.has('kairo.initiative.read') && (
|
{capabilities.has('kairo.initiative.read') && (
|
||||||
|
<CollapsibleSection title="Erweitert" summary={advancedSummary}>
|
||||||
|
<div className="initiative-advanced-stack">
|
||||||
<BacklogSection
|
<BacklogSection
|
||||||
items={backlogItems}
|
items={backlogItems}
|
||||||
canManage={capabilities.has('kairo.backlog.manage')}
|
canManage={capabilities.has('kairo.backlog.manage')}
|
||||||
|
|
@ -679,31 +586,6 @@ export function InitiativeDetailPage() {
|
||||||
onDelete={handleDeleteBacklog}
|
onDelete={handleDeleteBacklog}
|
||||||
busy={formBusy}
|
busy={formBusy}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{capabilities.has('kairo.initiative.read') && (
|
|
||||||
<BlockersSection
|
|
||||||
blockers={blockers}
|
|
||||||
canManage={capabilities.has('kairo.blocker.manage')}
|
|
||||||
onCreate={handleCreateBlocker}
|
|
||||||
onUpdateStatus={handleBlockerStatus}
|
|
||||||
onDelete={handleDeleteBlocker}
|
|
||||||
busy={formBusy}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{capabilities.has('kairo.initiative.read') && (
|
|
||||||
<EvidenceSection
|
|
||||||
items={evidenceItems}
|
|
||||||
canManage={capabilities.has('kairo.evidence.manage')}
|
|
||||||
onCreate={handleCreateEvidence}
|
|
||||||
onUpdateStatus={handleEvidenceStatus}
|
|
||||||
onDelete={handleDeleteEvidence}
|
|
||||||
busy={formBusy}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{capabilities.has('kairo.initiative.read') && (
|
|
||||||
<DecisionsSection
|
<DecisionsSection
|
||||||
items={decisions}
|
items={decisions}
|
||||||
canManage={capabilities.has('kairo.decision.manage')}
|
canManage={capabilities.has('kairo.decision.manage')}
|
||||||
|
|
@ -712,20 +594,6 @@ export function InitiativeDetailPage() {
|
||||||
onDelete={handleDeleteDecision}
|
onDelete={handleDeleteDecision}
|
||||||
busy={formBusy}
|
busy={formBusy}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{capabilities.has('kairo.initiative.read') && (
|
|
||||||
<ReviewsSection
|
|
||||||
items={reviews}
|
|
||||||
canManage={capabilities.has('kairo.review.manage')}
|
|
||||||
onCreate={handleCreateReview}
|
|
||||||
onUpdateStatus={handleReviewStatus}
|
|
||||||
onDelete={handleDeleteReview}
|
|
||||||
busy={formBusy}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{capabilities.has('kairo.initiative.read') && (
|
|
||||||
<RecurringSection
|
<RecurringSection
|
||||||
items={recurringItems}
|
items={recurringItems}
|
||||||
canManage={capabilities.has('kairo.recurring.manage')}
|
canManage={capabilities.has('kairo.recurring.manage')}
|
||||||
|
|
@ -734,6 +602,35 @@ export function InitiativeDetailPage() {
|
||||||
onDelete={handleDeleteRecurring}
|
onDelete={handleDeleteRecurring}
|
||||||
busy={formBusy}
|
busy={formBusy}
|
||||||
/>
|
/>
|
||||||
|
<BlockersSection
|
||||||
|
blockers={unlinkedBlockers}
|
||||||
|
heading="Vorhaben-Blocker"
|
||||||
|
lead="Ohne Maßnahmenbezug — verknüpfte Blocker erscheinen am Maßnahmen-Hub."
|
||||||
|
emptyMessage="Keine vorhabenweiten Blocker."
|
||||||
|
canManage={capabilities.has('kairo.blocker.manage')}
|
||||||
|
onCreate={handleCreateBlocker}
|
||||||
|
onUpdateStatus={handleBlockerStatus}
|
||||||
|
onDelete={handleDeleteBlocker}
|
||||||
|
busy={formBusy}
|
||||||
|
/>
|
||||||
|
<EvidenceSection
|
||||||
|
items={evidenceItems}
|
||||||
|
canManage={capabilities.has('kairo.evidence.manage')}
|
||||||
|
onCreate={handleCreateEvidence}
|
||||||
|
onUpdateStatus={handleEvidenceStatus}
|
||||||
|
onDelete={handleDeleteEvidence}
|
||||||
|
busy={formBusy}
|
||||||
|
/>
|
||||||
|
<ReviewsSection
|
||||||
|
items={reviews}
|
||||||
|
canManage={capabilities.has('kairo.review.manage')}
|
||||||
|
onCreate={handleCreateReview}
|
||||||
|
onUpdateStatus={handleReviewStatus}
|
||||||
|
onDelete={handleDeleteReview}
|
||||||
|
busy={formBusy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -690,3 +690,280 @@
|
||||||
.action-links {
|
.action-links {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AP1.1b — Initiative detail steering surface */
|
||||||
|
|
||||||
|
.section-lead {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-snapshot--hero {
|
||||||
|
border-color: var(--jk-accent-muted, #dbeafe);
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--jk-surface-raised, #f8fafc) 0%,
|
||||||
|
var(--jk-surface, #fff) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-hero-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-hero-kicker {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--jk-accent, #2563eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-hero-title {
|
||||||
|
margin: 0.15rem 0 0;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-lifecycle-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-hero-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem 1.25rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-stat-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-stat-pill {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 3.25rem;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--jk-surface, #fff);
|
||||||
|
border: 1px solid var(--jk-border, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-stat-pill--warn {
|
||||||
|
border-color: var(--jk-warning-border, #fcd34d);
|
||||||
|
background: var(--jk-warning-bg, #fffbeb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-stat-pill-value {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-stat-pill-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--jk-text-muted, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-hero-columns {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.steering-hero-columns {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-unlinked-hint {
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initiative-actions-hub {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card {
|
||||||
|
border: 1px solid var(--jk-border, #e5e7eb);
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
background: var(--jk-surface, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card--blocked {
|
||||||
|
border-color: var(--jk-warning-border, #fbbf24);
|
||||||
|
background: var(--jk-warning-bg, #fffbeb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card--editing {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card-header {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card-title-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card-desc {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--jk-text, #111);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-linked {
|
||||||
|
margin-top: 0.65rem;
|
||||||
|
padding-top: 0.65rem;
|
||||||
|
border-top: 1px dashed var(--jk-border, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-linked-title {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: var(--jk-text-muted, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-linked-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-linked-list li {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hub-card-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding-top: 0.65rem;
|
||||||
|
border-top: 1px solid var(--jk-border, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.55rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card--secondary {
|
||||||
|
background: var(--jk-surface-raised, #f9fafb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-form textarea {
|
||||||
|
min-height: 4.5rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-section {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-section-trigger {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 1.15rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-section-trigger:hover {
|
||||||
|
background: var(--jk-surface-raised, #f9fafb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-section-title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-section-summary {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
min-width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-section-chevron {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--jk-text-muted, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-section-body {
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initiative-advanced-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initiative-advanced-stack .card {
|
||||||
|
box-shadow: none;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user