From 583d2543fbb9bf55506dbd734c372b366bba037e Mon Sep 17 00:00:00 2001 From: "Konrad M." Date: Tue, 21 Apr 2026 21:52:58 +0200 Subject: [PATCH] fix(tasks): overdue always first; sort by due date, priority as tiebreaker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit effectiveDue() and sortTasks() added — same logic on client (tasks.js) and server (dashboard.js urgentTasks moved from SQL to JS sort). Applies in list-group, Kanban, and dashboard widget views. SQLite DATE('now') replaced with new Date() for timezone-safe due_time. --- public/pages/tasks.js | 41 +++++++++++++++++++++++++++--- server/routes/dashboard.js | 51 ++++++++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/public/pages/tasks.js b/public/pages/tasks.js index 47eb400..0cab2b0 100644 --- a/public/pages/tasks.js +++ b/public/pages/tasks.js @@ -24,6 +24,8 @@ const PRIORITIES = () => [ { value: 'none', label: t('tasks.priorityNone'), color: 'var(--color-priority-none)' }, ]; +const PRIO_ORDER = { urgent: 0, high: 1, medium: 2, low: 3, none: 4 }; + const STATUSES = () => [ { value: 'open', label: t('tasks.statusOpen') }, { value: 'in_progress', label: t('tasks.statusInProgress') }, @@ -228,6 +230,28 @@ function renderTaskCard(task, opts = {}) { `; } +// Effektive Fälligkeit: mit due_time wenn vorhanden, sonst 23:59:59 des Tages +function effectiveDue(task) { + if (!task.due_date) return null; + return task.due_time + ? new Date(`${task.due_date}T${task.due_time}`) + : new Date(`${task.due_date}T23:59:59`); +} + +// Einheitliche Sortierung: überfällig zuerst → Datum/Zeit ASC → Prio als Tiebreaker +function sortTasks(a, b, now) { + const aDate = effectiveDue(a); + const bDate = effectiveDue(b); + const aOver = aDate && aDate < now ? 1 : 0; + const bOver = bDate && bDate < now ? 1 : 0; + if (bOver !== aOver) return bOver - aOver; + if (!aDate && !bDate) return (PRIO_ORDER[a.priority] ?? 4) - (PRIO_ORDER[b.priority] ?? 4); + if (!aDate) return 1; + if (!bDate) return -1; + if (aDate.getTime() !== bDate.getTime()) return aDate < bDate ? -1 : 1; + return (PRIO_ORDER[a.priority] ?? 4) - (PRIO_ORDER[b.priority] ?? 4); +} + function renderTaskGroups(tasks, groupMode) { if (!tasks.length) { return `
@@ -240,16 +264,20 @@ function renderTaskGroups(tasks, groupMode) {
`; } - const groups = groupBy(tasks, groupMode); + const now = new Date(); const catLabelsMap = CATEGORY_LABELS(); - return groups.map(([name, groupTasks]) => ` + const groups = groupBy(tasks, groupMode); + return groups.map(([name, groupTasks]) => { + const sorted = [...groupTasks].sort((a, b) => sortTasks(a, b, now)); + return `
${catLabelsMap[name] ?? name} ${groupTasks.length}
- ${groupTasks.map((t) => renderSwipeRow(t, renderTaskCard(t))).join('')} -
`).join(''); + ${sorted.map((t) => renderSwipeRow(t, renderTaskCard(t))).join('')} + `; + }).join(''); } // -------------------------------------------------------- @@ -638,6 +666,11 @@ function renderKanban(container) { else grouped['open'].push(t); } + const now = new Date(); + for (const col of cols) { + grouped[col.status].sort((a, b) => sortTasks(a, b, now)); + } + listEl.innerHTML = `
${cols.map((col) => ` diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 8101c9a..7e36483 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -42,9 +42,12 @@ router.get('/', (req, res) => { SELECT ce.*, u.display_name AS assigned_name, - u.avatar_color AS assigned_color + u.avatar_color AS assigned_color, + ec.name AS cal_name, + ec.color AS cal_color FROM calendar_events ce - LEFT JOIN users u ON ce.assigned_to = u.id + LEFT JOIN users u ON ce.assigned_to = u.id + LEFT JOIN external_calendars ec ON ec.id = ce.calendar_ref_id WHERE ce.start_datetime >= ? ORDER BY ce.start_datetime ASC LIMIT 5 @@ -54,27 +57,39 @@ router.get('/', (req, res) => { result.upcomingEvents = []; } - // Offene Aufgaben: alle nicht-erledigten, sortiert nach Priorität und Fälligkeit + // Offene Aufgaben: in JS sortiert damit due_time korrekt gegen lokale Zeit geprüft wird try { - result.urgentTasks = d.prepare(` - SELECT - t.*, - u.display_name AS assigned_name, - u.avatar_color AS assigned_color + const allOpen = d.prepare(` + SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color FROM tasks t LEFT JOIN users u ON t.assigned_to = u.id WHERE t.status != 'done' - ORDER BY - CASE t.priority - WHEN 'urgent' THEN 0 - WHEN 'high' THEN 1 - WHEN 'medium' THEN 2 - WHEN 'low' THEN 3 - ELSE 4 - END, - t.due_date ASC NULLS LAST - LIMIT 5 `).all(); + + const PRIO = { urgent: 0, high: 1, medium: 2, low: 3, none: 4 }; + const now = new Date(); + + function effectiveDue(task) { + if (!task.due_date) return null; + return task.due_time + ? new Date(`${task.due_date}T${task.due_time}`) + : new Date(`${task.due_date}T23:59:59`); + } + + allOpen.sort((a, b) => { + const aDate = effectiveDue(a); + const bDate = effectiveDue(b); + const aOver = aDate && aDate < now ? 1 : 0; + const bOver = bDate && bDate < now ? 1 : 0; + if (bOver !== aOver) return bOver - aOver; + if (!aDate && !bDate) return (PRIO[a.priority] ?? 4) - (PRIO[b.priority] ?? 4); + if (!aDate) return 1; + if (!bDate) return -1; + if (aDate.getTime() !== bDate.getTime()) return aDate < bDate ? -1 : 1; + return (PRIO[a.priority] ?? 4) - (PRIO[b.priority] ?? 4); + }); + + result.urgentTasks = allOpen.slice(0, 5); } catch (err) { log.error('urgentTasks-Fehler:', err.message); result.urgentTasks = [];