fix: Gate-Verify — Evidence an Plan-Element verknüpfen
Some checks failed
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Failing after 1m31s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Some checks failed
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Failing after 1m31s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Verify prüft akzeptiertes Evidence am RoadmapItem. Plan-UI schließt Gates mit Nachweis; Journey erlaubt Plan-Zuordnung. Migration 011 roadmap_item_id auf evidence/reviews. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
09433b5d28
commit
fdda8a9fae
29
backend/migrations/011_roadmap_item_links.sql
Normal file
29
backend/migrations/011_roadmap_item_links.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- AP1.4b: Plan-Ist-Verknüpfung für Evidence und Reviews
|
||||
|
||||
ALTER TABLE evidence
|
||||
ADD COLUMN roadmap_item_id UUID NULL REFERENCES roadmap_items(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE reviews
|
||||
ADD COLUMN roadmap_item_id UUID NULL REFERENCES roadmap_items(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX idx_evidence_roadmap_item ON evidence(tenant_id, roadmap_item_id)
|
||||
WHERE roadmap_item_id IS NOT NULL;
|
||||
CREATE INDEX idx_reviews_roadmap_item ON reviews(tenant_id, roadmap_item_id)
|
||||
WHERE roadmap_item_id IS NOT NULL;
|
||||
|
||||
-- Bestehende milestone_id-Verknüpfungen spiegeln (RoadmapItem behält oft dieselbe UUID)
|
||||
UPDATE evidence e
|
||||
SET roadmap_item_id = e.milestone_id
|
||||
WHERE e.milestone_id IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM roadmap_items ri
|
||||
WHERE ri.id = e.milestone_id AND ri.tenant_id = e.tenant_id
|
||||
);
|
||||
|
||||
UPDATE reviews r
|
||||
SET roadmap_item_id = r.milestone_id
|
||||
WHERE r.milestone_id IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM roadmap_items ri
|
||||
WHERE ri.id = r.milestone_id AND ri.tenant_id = r.tenant_id
|
||||
);
|
||||
|
|
@ -19,8 +19,10 @@ class EvidenceUpdateRequest(BaseModel):
|
|||
status: Optional[Literal["submitted", "accepted", "rejected"]] = None
|
||||
action_id: Optional[str] = None
|
||||
milestone_id: Optional[str] = None
|
||||
roadmap_item_id: Optional[str] = None
|
||||
clear_action_id: bool = False
|
||||
clear_milestone_id: bool = False
|
||||
clear_roadmap_item_id: bool = False
|
||||
|
||||
|
||||
@router.get("/{evidence_id}")
|
||||
|
|
@ -50,8 +52,10 @@ def update_evidence(
|
|||
status=body.status,
|
||||
action_id=body.action_id,
|
||||
milestone_id=body.milestone_id,
|
||||
roadmap_item_id=body.roadmap_item_id,
|
||||
clear_action_id=body.clear_action_id,
|
||||
clear_milestone_id=body.clear_milestone_id,
|
||||
clear_roadmap_item_id=body.clear_roadmap_item_id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class EvidenceCreateRequest(BaseModel):
|
|||
status: Literal["submitted", "accepted", "rejected"] = "submitted"
|
||||
action_id: Optional[str] = None
|
||||
milestone_id: Optional[str] = None
|
||||
roadmap_item_id: Optional[str] = None
|
||||
|
||||
|
||||
class DecisionCreateRequest(BaseModel):
|
||||
|
|
@ -412,6 +413,7 @@ def create_initiative_evidence(
|
|||
status=body.status,
|
||||
action_id=body.action_id,
|
||||
milestone_id=body.milestone_id,
|
||||
roadmap_item_id=body.roadmap_item_id,
|
||||
submitted_by_actor_id=ctx.actor_id,
|
||||
user_id=ctx.user_id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ EvidenceStatus = Literal["submitted", "accepted", "rejected"]
|
|||
EVIDENCE_STATUSES = frozenset({"submitted", "accepted", "rejected"})
|
||||
|
||||
_EVIDENCE_COLUMNS = """
|
||||
id, tenant_id, initiative_id, action_id, milestone_id, title, description,
|
||||
status, submitted_by_actor_id, created_at, updated_at
|
||||
id, tenant_id, initiative_id, action_id, milestone_id, roadmap_item_id,
|
||||
title, description, status, submitted_by_actor_id, created_at, updated_at
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -28,6 +28,7 @@ def _serialize_row(row: dict[str, Any]) -> dict[str, Any]:
|
|||
"initiative_id",
|
||||
"action_id",
|
||||
"milestone_id",
|
||||
"roadmap_item_id",
|
||||
"submitted_by_actor_id",
|
||||
):
|
||||
if result.get(key):
|
||||
|
|
@ -72,6 +73,29 @@ def _action_in_initiative(*, tenant_id: str, initiative_id: str, action_id: str)
|
|||
conn.close()
|
||||
|
||||
|
||||
def _roadmap_item_in_initiative(
|
||||
*, tenant_id: str, initiative_id: str, roadmap_item_id: str
|
||||
) -> Optional[dict[str, str]]:
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT ri.id, ri.item_type
|
||||
FROM roadmap_items ri
|
||||
JOIN roadmaps r ON r.id = ri.roadmap_id AND r.tenant_id = ri.tenant_id
|
||||
WHERE ri.id = %s AND ri.tenant_id = %s AND r.initiative_id = %s
|
||||
""",
|
||||
(roadmap_item_id, tenant_id, initiative_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {"id": str(row["id"]), "item_type": row["item_type"]}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _milestone_in_initiative(
|
||||
*, tenant_id: str, initiative_id: str, milestone_id: str
|
||||
) -> bool:
|
||||
|
|
@ -93,7 +117,6 @@ def _milestone_in_initiative(
|
|||
FROM roadmap_items ri
|
||||
JOIN roadmaps r ON r.id = ri.roadmap_id AND r.tenant_id = ri.tenant_id
|
||||
WHERE ri.id = %s AND ri.tenant_id = %s AND r.initiative_id = %s
|
||||
AND ri.item_type = 'milestone'
|
||||
""",
|
||||
(milestone_id, tenant_id, initiative_id),
|
||||
)
|
||||
|
|
@ -102,6 +125,40 @@ def _milestone_in_initiative(
|
|||
conn.close()
|
||||
|
||||
|
||||
def _resolve_gate_links(
|
||||
*,
|
||||
tenant_id: str,
|
||||
initiative_id: str,
|
||||
milestone_id: Optional[str] = None,
|
||||
roadmap_item_id: Optional[str] = None,
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Gibt (milestone_id, roadmap_item_id) für Evidence-Insert zurück."""
|
||||
resolved_milestone = milestone_id
|
||||
resolved_roadmap = roadmap_item_id
|
||||
|
||||
if roadmap_item_id:
|
||||
item = _roadmap_item_in_initiative(
|
||||
tenant_id=tenant_id,
|
||||
initiative_id=initiative_id,
|
||||
roadmap_item_id=roadmap_item_id,
|
||||
)
|
||||
if not item:
|
||||
raise ValueError("Plan-Element gehört nicht zum Vorhaben")
|
||||
resolved_roadmap = item["id"]
|
||||
if item["item_type"] == "milestone":
|
||||
resolved_milestone = item["id"]
|
||||
elif milestone_id:
|
||||
if not _milestone_in_initiative(
|
||||
tenant_id=tenant_id,
|
||||
initiative_id=initiative_id,
|
||||
milestone_id=milestone_id,
|
||||
):
|
||||
raise ValueError("Meilenstein gehört nicht zum Vorhaben")
|
||||
resolved_roadmap = milestone_id
|
||||
|
||||
return resolved_milestone, resolved_roadmap
|
||||
|
||||
|
||||
def create_evidence(
|
||||
*,
|
||||
tenant_id: str,
|
||||
|
|
@ -111,6 +168,7 @@ def create_evidence(
|
|||
status: EvidenceStatus = "submitted",
|
||||
action_id: Optional[str] = None,
|
||||
milestone_id: Optional[str] = None,
|
||||
roadmap_item_id: Optional[str] = None,
|
||||
submitted_by_actor_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
|
|
@ -124,10 +182,12 @@ def create_evidence(
|
|||
tenant_id=tenant_id, initiative_id=initiative_id, action_id=action_id
|
||||
):
|
||||
raise ValueError("Maßnahme gehört nicht zum Vorhaben")
|
||||
if milestone_id and not _milestone_in_initiative(
|
||||
tenant_id=tenant_id, initiative_id=initiative_id, milestone_id=milestone_id
|
||||
):
|
||||
raise ValueError("Meilenstein gehört nicht zum Vorhaben")
|
||||
milestone_id, roadmap_item_id = _resolve_gate_links(
|
||||
tenant_id=tenant_id,
|
||||
initiative_id=initiative_id,
|
||||
milestone_id=milestone_id,
|
||||
roadmap_item_id=roadmap_item_id,
|
||||
)
|
||||
if submitted_by_actor_id and not _actor_in_tenant(
|
||||
tenant_id=tenant_id, actor_id=submitted_by_actor_id
|
||||
):
|
||||
|
|
@ -139,10 +199,10 @@ def create_evidence(
|
|||
cur.execute(
|
||||
f"""
|
||||
INSERT INTO evidence (
|
||||
tenant_id, initiative_id, action_id, milestone_id,
|
||||
tenant_id, initiative_id, action_id, milestone_id, roadmap_item_id,
|
||||
title, description, status, submitted_by_actor_id
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING {_EVIDENCE_COLUMNS}
|
||||
""",
|
||||
(
|
||||
|
|
@ -150,6 +210,7 @@ def create_evidence(
|
|||
initiative_id,
|
||||
action_id,
|
||||
milestone_id,
|
||||
roadmap_item_id,
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
|
|
@ -221,8 +282,10 @@ def update_evidence(
|
|||
status: Optional[EvidenceStatus] = None,
|
||||
action_id: Optional[str] = None,
|
||||
milestone_id: Optional[str] = None,
|
||||
roadmap_item_id: Optional[str] = None,
|
||||
clear_action_id: bool = False,
|
||||
clear_milestone_id: bool = False,
|
||||
clear_roadmap_item_id: bool = False,
|
||||
) -> Optional[dict[str, Any]]:
|
||||
existing = get_evidence(tenant_id=tenant_id, evidence_id=evidence_id)
|
||||
if not existing:
|
||||
|
|
@ -255,17 +318,20 @@ def update_evidence(
|
|||
raise ValueError("Maßnahme gehört nicht zum Vorhaben")
|
||||
updates.append("action_id = %s")
|
||||
params.append(action_id)
|
||||
if clear_milestone_id:
|
||||
if clear_milestone_id or clear_roadmap_item_id:
|
||||
updates.append("milestone_id = NULL")
|
||||
elif milestone_id is not None:
|
||||
if not _milestone_in_initiative(
|
||||
updates.append("roadmap_item_id = NULL")
|
||||
elif roadmap_item_id is not None or milestone_id is not None:
|
||||
link_milestone, link_roadmap = _resolve_gate_links(
|
||||
tenant_id=tenant_id,
|
||||
initiative_id=existing["initiative_id"],
|
||||
milestone_id=milestone_id,
|
||||
):
|
||||
raise ValueError("Meilenstein gehört nicht zum Vorhaben")
|
||||
roadmap_item_id=roadmap_item_id,
|
||||
)
|
||||
updates.append("milestone_id = %s")
|
||||
params.append(milestone_id)
|
||||
params.append(link_milestone)
|
||||
updates.append("roadmap_item_id = %s")
|
||||
params.append(link_roadmap)
|
||||
|
||||
if not updates:
|
||||
return existing
|
||||
|
|
|
|||
|
|
@ -549,10 +549,10 @@ def verify_reached(
|
|||
SELECT 1 FROM evidence
|
||||
WHERE tenant_id = %s AND initiative_id = %s
|
||||
AND status = 'accepted'
|
||||
AND milestone_id = %s
|
||||
AND (roadmap_item_id = %s OR milestone_id = %s)
|
||||
LIMIT 1
|
||||
""",
|
||||
(tenant_id, initiative_id, item_id),
|
||||
(tenant_id, initiative_id, item_id, item_id),
|
||||
)
|
||||
if cur.fetchone():
|
||||
verify_reason = "evidence_accepted"
|
||||
|
|
@ -562,10 +562,10 @@ def verify_reached(
|
|||
SELECT 1 FROM reviews
|
||||
WHERE tenant_id = %s AND initiative_id = %s
|
||||
AND status = 'completed'
|
||||
AND (milestone_id = %s)
|
||||
AND (roadmap_item_id = %s OR milestone_id = %s)
|
||||
LIMIT 1
|
||||
""",
|
||||
(tenant_id, initiative_id, item_id),
|
||||
(tenant_id, initiative_id, item_id, item_id),
|
||||
)
|
||||
if cur.fetchone():
|
||||
verify_reason = "review_completed"
|
||||
|
|
@ -584,8 +584,10 @@ def verify_reached(
|
|||
verify_reason = "gate_override_decision"
|
||||
else:
|
||||
raise ValueError(
|
||||
"Verify fehlgeschlagen — accepted Evidence, completed Review "
|
||||
"oder gate_override Decision erforderlich"
|
||||
"Verify fehlgeschlagen — am Plan-Element fehlt ein akzeptierter "
|
||||
"Nachweis (Evidence), ein abgeschlossenes Review oder eine "
|
||||
"gate_override-Decision. Nachweis unter Plan einreichen oder auf "
|
||||
"der Journey-Seite mit Plan-Element verknüpfen und auf Akzeptiert setzen."
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ def test_verify_reached_requires_evidence(client):
|
|||
f"/api/initiatives/{initiative_id}/evidence",
|
||||
json={
|
||||
"title": "Nachweis",
|
||||
"milestone_id": item["id"],
|
||||
"roadmap_item_id": item["id"],
|
||||
"status": "accepted",
|
||||
},
|
||||
headers=_auth(token),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
APP_VERSION = "0.13.0-ap1.4"
|
||||
DB_SCHEMA_VERSION = "010"
|
||||
DB_SCHEMA_VERSION = "011"
|
||||
APP_NAME = "jinkendo-kairo"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,50 @@
|
|||
import { useState } from 'react'
|
||||
import { EVIDENCE_STATUSES, EVIDENCE_STATUS_LABELS } from '../constants/status.js'
|
||||
import { EVIDENCE_STATUSES, EVIDENCE_STATUS_LABELS, ROADMAP_ITEM_TYPE_LABELS } from '../constants/status.js'
|
||||
import { StatusBadge } from './StatusBadge.jsx'
|
||||
import { EmptyState } from './EmptyState.jsx'
|
||||
|
||||
export function EvidenceSection({ items, canManage, onCreate, onUpdateStatus, onDelete, busy }) {
|
||||
function planElementLabel(items, roadmapItemId) {
|
||||
if (!roadmapItemId) return null
|
||||
const item = items.find((i) => i.id === roadmapItemId)
|
||||
if (!item) return 'Plan-Element'
|
||||
const type = ROADMAP_ITEM_TYPE_LABELS[item.item_type] || item.item_type
|
||||
return `${type}: ${item.title}`
|
||||
}
|
||||
|
||||
export function EvidenceSection({
|
||||
items,
|
||||
roadmapItems = [],
|
||||
canManage,
|
||||
onCreate,
|
||||
onUpdateStatus,
|
||||
onDelete,
|
||||
busy,
|
||||
}) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [roadmapItemId, setRoadmapItemId] = 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(),
|
||||
roadmap_item_id: roadmapItemId || undefined,
|
||||
})
|
||||
setTitle('')
|
||||
setRoadmapItemId('')
|
||||
setShowForm(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Evidence</h2>
|
||||
<p className="section-lead muted">
|
||||
Für Gate-Verify am Plan-Element verknüpfen und Status Akzeptiert setzen.
|
||||
</p>
|
||||
</div>
|
||||
{canManage && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -41,6 +67,22 @@ export function EvidenceSection({ items, canManage, onCreate, onUpdateStatus, on
|
|||
required
|
||||
/>
|
||||
</label>
|
||||
{roadmapItems.length > 0 && (
|
||||
<label>
|
||||
Plan-Element (optional)
|
||||
<select
|
||||
value={roadmapItemId}
|
||||
onChange={(e) => setRoadmapItemId(e.target.value)}
|
||||
>
|
||||
<option value="">— nicht zugeordnet —</option>
|
||||
{roadmapItems.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{ROADMAP_ITEM_TYPE_LABELS[item.item_type] || item.item_type}: {item.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<button type="submit" className="btn btn-primary" disabled={busy}>
|
||||
Anlegen
|
||||
</button>
|
||||
|
|
@ -55,6 +97,15 @@ export function EvidenceSection({ items, canManage, onCreate, onUpdateStatus, on
|
|||
<div className="list-item-main">
|
||||
<strong>{item.title}</strong>
|
||||
{item.description && <p className="list-item-desc">{item.description}</p>}
|
||||
{(item.roadmap_item_id || item.milestone_id) && (
|
||||
<p className="muted list-item-sub">
|
||||
Plan:{' '}
|
||||
{planElementLabel(
|
||||
roadmapItems,
|
||||
item.roadmap_item_id || item.milestone_id
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="list-item-meta action-controls">
|
||||
<StatusBadge kind="evidence" status={item.status} />
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function RoadmapPlanSection({
|
|||
onCreate,
|
||||
onUpdateStatus,
|
||||
onUpdateItem,
|
||||
onVerifyReached,
|
||||
onVerifyWithEvidence,
|
||||
onDelete,
|
||||
busy,
|
||||
}) {
|
||||
|
|
@ -41,6 +41,7 @@ export function RoadmapPlanSection({
|
|||
const [itemType, setItemType] = useState('milestone')
|
||||
const [sequencingMode, setSequencingMode] = useState('sequential')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [verifyTitles, setVerifyTitles] = useState({})
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
|
|
@ -193,14 +194,41 @@ export function RoadmapPlanSection({
|
|||
))}
|
||||
</select>
|
||||
{['planned', 'active', 'at_risk'].includes(item.status) && (
|
||||
<div className="gate-verify-block">
|
||||
<label className="gate-verify-label">
|
||||
Nachweis für Verify
|
||||
<input
|
||||
type="text"
|
||||
value={verifyTitles[item.id] ?? ''}
|
||||
placeholder={`Nachweis: ${item.title}`}
|
||||
onChange={(e) =>
|
||||
setVerifyTitles((prev) => ({
|
||||
...prev,
|
||||
[item.id]: e.target.value,
|
||||
}))
|
||||
}
|
||||
maxLength={255}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={busy}
|
||||
onClick={() => onVerifyReached(item.id)}
|
||||
onClick={() =>
|
||||
onVerifyWithEvidence(
|
||||
item.id,
|
||||
verifyTitles[item.id]?.trim() || `Nachweis: ${item.title}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Verify → erreicht
|
||||
Gate schließen
|
||||
</button>
|
||||
<p className="muted gate-verify-hint">
|
||||
Legt akzeptiertes Evidence am Plan-Element an und setzt Status auf erreicht.
|
||||
Bereits vorhandener Nachweis auf Journey: Plan-Element zuweisen und auf
|
||||
Akzeptiert setzen, dann erneut Verify.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -347,6 +347,24 @@ export function InitiativeOperationsProvider({ children }) {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleVerifyWithEvidence(itemId, evidenceTitle) {
|
||||
setFormBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
await createInitiativeEvidence(id, {
|
||||
title: evidenceTitle,
|
||||
roadmap_item_id: itemId,
|
||||
status: 'accepted',
|
||||
})
|
||||
await verifyRoadmapItemReached(itemId)
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setFormBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVerifyRoadmapItem(itemId) {
|
||||
setFormBusy(true)
|
||||
try {
|
||||
|
|
@ -545,6 +563,7 @@ export function InitiativeOperationsProvider({ children }) {
|
|||
handleRoadmapItemStatus,
|
||||
handleUpdateRoadmapItem,
|
||||
handleVerifyRoadmapItem,
|
||||
handleVerifyWithEvidence,
|
||||
handleDeleteRoadmapItem,
|
||||
handleCreateMilestone: handleCreateRoadmapItem,
|
||||
handleMilestoneStatus: handleRoadmapItemStatus,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export function InitiativeJourneyPage() {
|
|||
/>
|
||||
<EvidenceSection
|
||||
items={ops.evidenceItems}
|
||||
roadmapItems={ops.roadmapItems}
|
||||
canManage={capabilities.has('kairo.evidence.manage')}
|
||||
onCreate={ops.handleCreateEvidence}
|
||||
onUpdateStatus={ops.handleEvidenceStatus}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export function InitiativePlanPage() {
|
|||
handleCreateRoadmapItem,
|
||||
handleRoadmapItemStatus,
|
||||
handleUpdateRoadmapItem,
|
||||
handleVerifyRoadmapItem,
|
||||
handleVerifyWithEvidence,
|
||||
handleDeleteRoadmapItem,
|
||||
error,
|
||||
} = useInitiativeOperations()
|
||||
|
|
@ -27,7 +27,7 @@ export function InitiativePlanPage() {
|
|||
onCreate={handleCreateRoadmapItem}
|
||||
onUpdateStatus={handleRoadmapItemStatus}
|
||||
onUpdateItem={handleUpdateRoadmapItem}
|
||||
onVerifyReached={handleVerifyRoadmapItem}
|
||||
onVerifyWithEvidence={handleVerifyWithEvidence}
|
||||
onDelete={handleDeleteRoadmapItem}
|
||||
busy={formBusy}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1123,4 +1123,26 @@
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.gate-verify-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border-subtle, #e5e7eb);
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.gate-verify-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.gate-verify-hint {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user