fix(mission): show project-level todo tasks in mission control detail

This commit is contained in:
Christian 2026-05-18 07:26:09 +02:00
parent 071d926781
commit c019a0367b
2 changed files with 57 additions and 1 deletions

View File

@ -595,6 +595,37 @@ class MissionService:
(project_id,), (project_id,),
) or [] ) or []
project_open_todo_count = 0
project_open_todo_titles: list[str] = []
if MissionService._table_exists("sag_todo_steps"):
project_todo_row = execute_query_single(
"""
SELECT
COUNT(*) FILTER (
WHERE t.deleted_at IS NULL
AND COALESCE(t.is_done, FALSE) = FALSE
) AS open_todo_count,
ARRAY_REMOVE(
ARRAY_AGG(
CASE
WHEN t.deleted_at IS NULL
AND COALESCE(t.is_done, FALSE) = FALSE
THEN t.title
END
ORDER BY COALESCE(t.due_date, DATE '9999-12-31') ASC, t.id ASC
),
NULL
) AS open_todo_titles
FROM sag_todo_steps t
WHERE t.sag_id = %s
""",
(project_id,),
) or {}
project_open_todo_count = int(project_todo_row.get("open_todo_count") or 0)
titles_raw = project_todo_row.get("open_todo_titles") or []
if isinstance(titles_raw, list):
project_open_todo_titles = [str(item).strip() for item in titles_raw if str(item or "").strip()]
# Fallback for case-backed projects: fetch directly related/under cases from relation table. # Fallback for case-backed projects: fetch directly related/under cases from relation table.
# This is used when a project is a case of type project/projekt and tasks are linked as case relations. # This is used when a project is a case of type project/projekt and tasks are linked as case relations.
if not tasks and MissionService._table_exists("sag_relationer"): if not tasks and MissionService._table_exists("sag_relationer"):
@ -667,6 +698,8 @@ class MissionService:
"milestones": [dict(row) for row in milestones], "milestones": [dict(row) for row in milestones],
"blockers": [dict(row) for row in blockers], "blockers": [dict(row) for row in blockers],
"tasks": [dict(row) for row in tasks], "tasks": [dict(row) for row in tasks],
"project_open_todo_count": project_open_todo_count,
"project_open_todo_titles": project_open_todo_titles,
} }
@staticmethod @staticmethod

View File

@ -1328,6 +1328,10 @@
const tasks = Array.isArray(detail?.tasks) ? detail.tasks : []; const tasks = Array.isArray(detail?.tasks) ? detail.tasks : [];
const milestones = Array.isArray(detail?.milestones) ? detail.milestones : []; const milestones = Array.isArray(detail?.milestones) ? detail.milestones : [];
const blockers = Array.isArray(detail?.blockers) ? detail.blockers : []; const blockers = Array.isArray(detail?.blockers) ? detail.blockers : [];
const projectOpenTodoCount = Number(detail?.project_open_todo_count || 0);
const projectOpenTodoTitles = Array.isArray(detail?.project_open_todo_titles)
? detail.project_open_todo_titles.map((item) => String(item || '').trim()).filter(Boolean)
: [];
const grouped = { todo: [], doing: [], done: [] }; const grouped = { todo: [], doing: [], done: [] };
tasks.forEach((task) => { tasks.forEach((task) => {
@ -1339,6 +1343,7 @@
kpis.innerHTML = [ kpis.innerHTML = [
{ label: 'Opgaver', value: tasks.length }, { label: 'Opgaver', value: tasks.length },
{ label: 'Projekt todo', value: projectOpenTodoCount },
{ label: 'Milepæle', value: milestones.length }, { label: 'Milepæle', value: milestones.length },
{ label: 'Blockers', value: blockers.length }, { label: 'Blockers', value: blockers.length },
{ label: 'Deadline', value: formatShortDate(detail?.ended_at || detail?.deadline) }, { label: 'Deadline', value: formatShortDate(detail?.ended_at || detail?.deadline) },
@ -1355,11 +1360,29 @@
{ key: 'done', label: 'Lukket' }, { key: 'done', label: 'Lukket' },
]; ];
const projectTodoPreview = projectOpenTodoTitles.slice(0, 5)
.map((title) => `<div>• ${escapeHtml(title)}</div>`)
.join('');
const projectTodoMore = Math.max(projectOpenTodoCount - Math.min(projectOpenTodoTitles.length, 5), 0);
const projectTodoCard = projectOpenTodoCount > 0
? `
<div class="mc-kanban-card" style="border-left:3px solid #0f4c75;">
<div class="mc-kanban-title">Projekt todo (${projectOpenTodoCount})</div>
<div class="mc-kanban-meta">
${projectTodoPreview || '<div>-</div>'}
${projectTodoMore > 0 ? `<div>+${projectTodoMore} flere</div>` : ''}
</div>
</div>
`
: '';
board.innerHTML = laneMeta.map((lane) => { board.innerHTML = laneMeta.map((lane) => {
const laneTasks = grouped[lane.key] || []; const laneTasks = grouped[lane.key] || [];
const extraCard = lane.key === 'todo' ? projectTodoCard : '';
return ` return `
<div class="mc-kanban-col"> <div class="mc-kanban-col">
<h6>${escapeHtml(lane.label)} (${laneTasks.length})</h6> <h6>${escapeHtml(lane.label)} (${laneTasks.length + (lane.key === 'todo' && projectOpenTodoCount > 0 ? 1 : 0)})</h6>
${extraCard}
${laneTasks.length ? laneTasks.map((task) => ` ${laneTasks.length ? laneTasks.map((task) => `
<div class="mc-kanban-card"> <div class="mc-kanban-card">
<div class="mc-kanban-title">#${Number(task.id || 0)} ${escapeHtml(task.titel || task.title || 'Uden titel')}</div> <div class="mc-kanban-title">#${Number(task.id || 0)} ${escapeHtml(task.titel || task.title || 'Uden titel')}</div>