fix(tasks): overdue always first; sort by due date, priority as tiebreaker
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.
This commit is contained in:
+37
-4
@@ -24,6 +24,8 @@ const PRIORITIES = () => [
|
|||||||
{ value: 'none', label: t('tasks.priorityNone'), color: 'var(--color-priority-none)' },
|
{ 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 = () => [
|
const STATUSES = () => [
|
||||||
{ value: 'open', label: t('tasks.statusOpen') },
|
{ value: 'open', label: t('tasks.statusOpen') },
|
||||||
{ value: 'in_progress', label: t('tasks.statusInProgress') },
|
{ value: 'in_progress', label: t('tasks.statusInProgress') },
|
||||||
@@ -228,6 +230,28 @@ function renderTaskCard(task, opts = {}) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
function renderTaskGroups(tasks, groupMode) {
|
||||||
if (!tasks.length) {
|
if (!tasks.length) {
|
||||||
return `<div class="empty-state">
|
return `<div class="empty-state">
|
||||||
@@ -240,16 +264,20 @@ function renderTaskGroups(tasks, groupMode) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = groupBy(tasks, groupMode);
|
const now = new Date();
|
||||||
const catLabelsMap = CATEGORY_LABELS();
|
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 `
|
||||||
<div class="task-group">
|
<div class="task-group">
|
||||||
<div class="task-group__header">
|
<div class="task-group__header">
|
||||||
<span class="task-group__title">${catLabelsMap[name] ?? name}</span>
|
<span class="task-group__title">${catLabelsMap[name] ?? name}</span>
|
||||||
<span class="task-group__count">${groupTasks.length}</span>
|
<span class="task-group__count">${groupTasks.length}</span>
|
||||||
</div>
|
</div>
|
||||||
${groupTasks.map((t) => renderSwipeRow(t, renderTaskCard(t))).join('')}
|
${sorted.map((t) => renderSwipeRow(t, renderTaskCard(t))).join('')}
|
||||||
</div>`).join('');
|
</div>`;
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -638,6 +666,11 @@ function renderKanban(container) {
|
|||||||
else grouped['open'].push(t);
|
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 = `
|
listEl.innerHTML = `
|
||||||
<div class="kanban-board">
|
<div class="kanban-board">
|
||||||
${cols.map((col) => `
|
${cols.map((col) => `
|
||||||
|
|||||||
+32
-17
@@ -42,9 +42,12 @@ router.get('/', (req, res) => {
|
|||||||
SELECT
|
SELECT
|
||||||
ce.*,
|
ce.*,
|
||||||
u.display_name AS assigned_name,
|
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
|
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 >= ?
|
WHERE ce.start_datetime >= ?
|
||||||
ORDER BY ce.start_datetime ASC
|
ORDER BY ce.start_datetime ASC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
@@ -54,27 +57,39 @@ router.get('/', (req, res) => {
|
|||||||
result.upcomingEvents = [];
|
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 {
|
try {
|
||||||
result.urgentTasks = d.prepare(`
|
const allOpen = d.prepare(`
|
||||||
SELECT
|
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color
|
||||||
t.*,
|
|
||||||
u.display_name AS assigned_name,
|
|
||||||
u.avatar_color AS assigned_color
|
|
||||||
FROM tasks t
|
FROM tasks t
|
||||||
LEFT JOIN users u ON t.assigned_to = u.id
|
LEFT JOIN users u ON t.assigned_to = u.id
|
||||||
WHERE t.status != 'done'
|
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();
|
`).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) {
|
} catch (err) {
|
||||||
log.error('urgentTasks-Fehler:', err.message);
|
log.error('urgentTasks-Fehler:', err.message);
|
||||||
result.urgentTasks = [];
|
result.urgentTasks = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user