diff --git a/app/dashboard/backend/mission_service.py b/app/dashboard/backend/mission_service.py index d1fe874..82ea4a3 100644 --- a/app/dashboard/backend/mission_service.py +++ b/app/dashboard/backend/mission_service.py @@ -595,6 +595,37 @@ class MissionService: (project_id,), ) 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. # 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"): @@ -667,6 +698,8 @@ class MissionService: "milestones": [dict(row) for row in milestones], "blockers": [dict(row) for row in blockers], "tasks": [dict(row) for row in tasks], + "project_open_todo_count": project_open_todo_count, + "project_open_todo_titles": project_open_todo_titles, } @staticmethod diff --git a/app/dashboard/frontend/mission_control_v2.html b/app/dashboard/frontend/mission_control_v2.html index 8ec16a2..7376eb9 100644 --- a/app/dashboard/frontend/mission_control_v2.html +++ b/app/dashboard/frontend/mission_control_v2.html @@ -1328,6 +1328,10 @@ const tasks = Array.isArray(detail?.tasks) ? detail.tasks : []; const milestones = Array.isArray(detail?.milestones) ? detail.milestones : []; 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: [] }; tasks.forEach((task) => { @@ -1339,6 +1343,7 @@ kpis.innerHTML = [ { label: 'Opgaver', value: tasks.length }, + { label: 'Projekt todo', value: projectOpenTodoCount }, { label: 'Milepæle', value: milestones.length }, { label: 'Blockers', value: blockers.length }, { label: 'Deadline', value: formatShortDate(detail?.ended_at || detail?.deadline) }, @@ -1355,11 +1360,29 @@ { key: 'done', label: 'Lukket' }, ]; + const projectTodoPreview = projectOpenTodoTitles.slice(0, 5) + .map((title) => `
• ${escapeHtml(title)}
`) + .join(''); + const projectTodoMore = Math.max(projectOpenTodoCount - Math.min(projectOpenTodoTitles.length, 5), 0); + const projectTodoCard = projectOpenTodoCount > 0 + ? ` +
+
Projekt todo (${projectOpenTodoCount})
+
+ ${projectTodoPreview || '
-
'} + ${projectTodoMore > 0 ? `
+${projectTodoMore} flere
` : ''} +
+
+ ` + : ''; + board.innerHTML = laneMeta.map((lane) => { const laneTasks = grouped[lane.key] || []; + const extraCard = lane.key === 'todo' ? projectTodoCard : ''; return `
-
${escapeHtml(lane.label)} (${laneTasks.length})
+
${escapeHtml(lane.label)} (${laneTasks.length + (lane.key === 'todo' && projectOpenTodoCount > 0 ? 1 : 0)})
+ ${extraCard} ${laneTasks.length ? laneTasks.map((task) => `
#${Number(task.id || 0)} ${escapeHtml(task.titel || task.title || 'Uden titel')}