Kairo-Jinkendo/frontend/src/pages/InitiativeDetailPage.jsx

257 lines
8.2 KiB
React
Raw Normal View History

import { useCallback, useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import {
getInitiative,
listInitiativeActions,
createInitiativeAction,
} from '../api/initiatives.js'
import { updateAction, setActionAssignments } from '../api/actions.js'
import { actorsFromContext } from '../api/actors.js'
import { ACTION_STATUS_LABELS } from '../constants/status.js'
import { StatusBadge } from '../components/StatusBadge.jsx'
import { PriorityBadge } from '../components/PriorityBadge.jsx'
import { ActionForm } from '../components/ActionForm.jsx'
import { EmptyState } from '../components/EmptyState.jsx'
import { ErrorState } from '../components/ErrorState.jsx'
import { LoadingState } from '../components/LoadingState.jsx'
import { useCapabilities } from '../hooks/useCapabilities.js'
import { useSession } from '../context/SessionContext.jsx'
export function InitiativeDetailPage() {
const { id } = useParams()
const { context } = useSession()
const { hasCapability } = useCapabilities()
const actors = actorsFromContext(context)
const [initiative, setInitiative] = useState(null)
const [actions, setActions] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showActionForm, setShowActionForm] = useState(false)
const [editingAction, setEditingAction] = useState(null)
const [formBusy, setFormBusy] = useState(false)
const [hideDone, setHideDone] = useState(true)
const load = useCallback(async () => {
setLoading(true)
setError(null)
try {
const [initData, actionData] = await Promise.all([
getInitiative(id),
listInitiativeActions(id),
])
setInitiative(initData)
setActions(actionData)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [id])
useEffect(() => {
load()
}, [load])
const visibleActions = hideDone
? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded')
: actions
async function handleCreateAction(payload) {
setFormBusy(true)
try {
const assigned =
payload.assigned_actor_ids?.length > 0
? payload.assigned_actor_ids
: context?.actor?.id
? [context.actor.id]
: []
await createInitiativeAction(id, { ...payload, assigned_actor_ids: assigned })
setShowActionForm(false)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleUpdateAction(actionId, payload) {
setFormBusy(true)
try {
await updateAction(actionId, {
title: payload.title,
description: payload.description,
status: payload.status,
priority: payload.priority,
})
if (hasCapability('kairo.action.manage')) {
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
}
setEditingAction(null)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleQuickStatus(action, status) {
if (!hasCapability('kairo.action.manage')) return
try {
await updateAction(action.id, { status })
await load()
} catch (err) {
setError(err.message)
}
}
if (loading) {
return (
<div className="app-page">
<LoadingState message="Lade Vorhaben …" />
</div>
)
}
if (error && !initiative) {
return (
<div className="app-page">
<ErrorState message={error} onRetry={load} />
</div>
)
}
if (!initiative) {
return (
<div className="app-page">
<ErrorState message="Vorhaben nicht gefunden." />
</div>
)
}
return (
<div className="app-page">
<nav className="breadcrumb">
<Link to="/initiatives">Vorhaben</Link>
<span aria-hidden="true"> / </span>
<span>{initiative.title}</span>
</nav>
<header className="page-header detail-header">
<div>
<h1>{initiative.title}</h1>
{initiative.goal && <p className="detail-goal">{initiative.goal}</p>}
<div className="detail-badges">
<StatusBadge kind="initiative" status={initiative.status} />
<PriorityBadge priority={initiative.priority} />
</div>
</div>
</header>
{error && <p className="error">{error}</p>}
<section className="card">
<div className="section-header">
<h2>Maßnahmen</h2>
<div className="section-actions">
<label className="checkbox-label inline-filter">
<input
type="checkbox"
checked={hideDone}
onChange={(e) => setHideDone(e.target.checked)}
/>
Erledigte ausblenden
</label>
{hasCapability('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}
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) => (
<li key={action.id} className="list-item card-list-item">
{editingAction === action.id ? (
<ActionForm
initial={action}
actors={actors}
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>
)}
</div>
<div className="list-item-meta action-controls">
<StatusBadge status={action.status} />
<PriorityBadge priority={action.priority} />
{hasCapability('kairo.action.manage') && (
<>
<select
className="inline-select"
value={action.status}
onChange={(e) => handleQuickStatus(action, e.target.value)}
aria-label="Status ändern"
>
{['open', 'in_progress', 'blocked', 'done', 'discarded'].map((s) => (
<option key={s} value={s}>
{ACTION_STATUS_LABELS[s]}
</option>
))}
</select>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setEditingAction(action.id)
setShowActionForm(false)
}}
>
Bearbeiten
</button>
</>
)}
</div>
</>
)}
</li>
))}
</ul>
</section>
</div>
)
}