tududi/backend/modules/tasks/queries/metrics-computation.js
Chris 542be2c1e9
Fix bug 366 (#764)
* Optimize DB

* Clean up names

* fixup! Clean up names

* fixup! fixup! Clean up names
2026-01-07 18:18:07 +02:00

292 lines
8.2 KiB
JavaScript

const { Task } = require('../../../models');
const { Op } = require('sequelize');
const moment = require('moment-timezone');
const permissionsService = require('../../../services/permissionsService');
const {
countTotalOpenTasks,
countTasksPendingOverMonth,
fetchTasksInProgress,
fetchTodayPlanTasks,
fetchTasksDueToday,
fetchOverdueTasks,
fetchSomedayTaskIds,
fetchNonProjectTasks,
fetchProjectTasks,
fetchSomedayFallbackTasks,
fetchTasksCompletedToday,
} = require('./metrics-queries');
const MAX_SUGGESTED_TASKS = 10;
const getPriorityValue = (priority) => {
const priorityOrder = {
high: 3,
medium: 2,
low: 1,
};
if (priority === null || priority === undefined) {
return 0;
}
if (typeof priority === 'number') {
// Normalize numeric priority (assuming 2=high,1=medium,0=low)
if (priority >= 2) return priorityOrder.high;
if (priority === 1) return priorityOrder.medium;
if (priority === 0) return priorityOrder.low;
return 0;
}
const normalized = String(priority).toLowerCase();
return priorityOrder[normalized] || 0;
};
const multiCriteriaTaskSort = (a, b) => {
// 1. Priority
const priorityDiff =
getPriorityValue(b.priority) - getPriorityValue(a.priority);
if (priorityDiff !== 0) {
return priorityDiff;
}
// 2. Due date (earlier first, null/undefined last)
const getDueDateValue = (task) => {
if (!task.due_date) return Infinity;
const due =
task.due_date instanceof Date
? task.due_date
: new Date(task.due_date);
const time = due.getTime();
return Number.isNaN(time) ? Infinity : time;
};
const dueDiff = getDueDateValue(a) - getDueDateValue(b);
if (dueDiff !== 0) {
return dueDiff;
}
// 3. Project (group similar project tasks)
const projectA = (a.project_id || '').toString();
const projectB = (b.project_id || '').toString();
return projectA.localeCompare(projectB);
};
async function computeSuggestedTasks(
visibleTasksWhere,
userId,
totalOpenTasks,
tasksInProgress,
tasksDueToday,
tasksOverdue,
todayPlanTasks
) {
if (
totalOpenTasks < 3 &&
tasksInProgress.length === 0 &&
tasksDueToday.length === 0
) {
return [];
}
const excludedTaskIds = [
...tasksInProgress.map((t) => t.id),
...tasksDueToday.map((t) => t.id),
...tasksOverdue.map((t) => t.id),
...todayPlanTasks.map((t) => t.id),
];
const somedayTaskIds = await fetchSomedayTaskIds(userId);
const [nonProjectTasks, projectTasks] = await Promise.all([
fetchNonProjectTasks(
visibleTasksWhere,
excludedTaskIds,
somedayTaskIds
),
fetchProjectTasks(visibleTasksWhere, excludedTaskIds, somedayTaskIds),
]);
let combinedTasks = [...nonProjectTasks, ...projectTasks];
if (combinedTasks.length < 6) {
const usedTaskIds = [
...excludedTaskIds,
...combinedTasks.map((t) => t.id),
];
const somedayFallbackTasks = await fetchSomedayFallbackTasks(
userId,
usedTaskIds,
somedayTaskIds
);
combinedTasks = [...combinedTasks, ...somedayFallbackTasks];
}
const now = Date.now();
const filteredTasks = combinedTasks.filter((task) => {
if (!task.defer_until) return true;
const deferUntil = new Date(task.defer_until).getTime();
if (Number.isNaN(deferUntil)) return true;
return deferUntil <= now;
});
filteredTasks.sort(multiCriteriaTaskSort);
return filteredTasks.slice(0, MAX_SUGGESTED_TASKS);
}
async function computeWeeklyCompletions(userId, userTimezone) {
const todayInUserTz = moment.tz(userTimezone);
const weekStartInUserTz = moment.tz(userTimezone).subtract(6, 'days');
const weekStart = weekStartInUserTz.clone().startOf('day').utc().toDate();
const weekEnd = todayInUserTz.clone().endOf('day').utc().toDate();
const weeklyCompletionsRaw = await Task.findAll({
where: {
user_id: userId,
status: Task.STATUS.DONE,
completed_at: {
[Op.between]: [weekStart, weekEnd],
},
},
attributes: ['completed_at'],
raw: true,
});
const dateCountMap = {};
weeklyCompletionsRaw.forEach((task) => {
const completedDate = new Date(task.completed_at);
const dateInUserTz = moment(completedDate)
.tz(userTimezone)
.format('YYYY-MM-DD');
dateCountMap[dateInUserTz] = (dateCountMap[dateInUserTz] || 0) + 1;
});
const weeklyCompletions = Object.entries(dateCountMap).map(
([date, count]) => ({
date,
count: count.toString(),
})
);
const weeklyData = [];
for (let i = 6; i >= 0; i--) {
const dateInUserTz = moment.tz(userTimezone).subtract(i, 'days');
const dateString = dateInUserTz.format('YYYY-MM-DD');
const found = weeklyCompletions.find(
(item) => item.date === dateString
);
const dayData = {
date: dateString,
count: found ? parseInt(found.count) : 0,
dayName: dateInUserTz.format('ddd'),
};
weeklyData.push(dayData);
}
return weeklyData;
}
async function computeTaskMetrics(
userId,
userTimezone = 'UTC',
permissionCache = null
) {
const visibleTasksWhere =
await permissionsService.ownershipOrPermissionWhere(
'task',
userId,
permissionCache
);
const [
totalOpenTasks,
tasksPendingOverMonth,
tasksInProgress,
todayPlanTasks,
tasksDueToday,
tasksOverdue,
tasksCompletedToday,
weeklyCompletions,
] = await Promise.all([
countTotalOpenTasks(visibleTasksWhere),
countTasksPendingOverMonth(visibleTasksWhere),
fetchTasksInProgress(visibleTasksWhere),
fetchTodayPlanTasks(visibleTasksWhere),
fetchTasksDueToday(visibleTasksWhere, userTimezone),
fetchOverdueTasks(visibleTasksWhere, userTimezone),
fetchTasksCompletedToday(userId, userTimezone),
computeWeeklyCompletions(userId, userTimezone),
]);
const suggestedTasks = await computeSuggestedTasks(
visibleTasksWhere,
userId,
totalOpenTasks,
tasksInProgress,
tasksDueToday,
tasksOverdue,
todayPlanTasks
);
return {
total_open_tasks: totalOpenTasks,
tasks_pending_over_month: tasksPendingOverMonth,
tasks_in_progress_count: tasksInProgress.length,
tasks_in_progress: tasksInProgress,
tasks_due_today: tasksDueToday,
tasks_overdue: tasksOverdue,
today_plan_tasks: todayPlanTasks,
suggested_tasks: suggestedTasks,
tasks_completed_today: tasksCompletedToday,
weekly_completions: weeklyCompletions,
};
}
async function getTaskMetrics(userId, timezone) {
const metrics = await computeTaskMetrics(userId, timezone);
const {
buildMetricsResponse,
serializeTasks,
} = require('../core/serializers');
const response = await buildMetricsResponse(metrics);
const serializedLists = {
tasks_in_progress: await serializeTasks(
metrics.tasks_in_progress,
timezone
),
tasks_today_plan: await serializeTasks(
metrics.today_plan_tasks,
timezone
),
tasks_due_today: await serializeTasks(
metrics.tasks_due_today,
timezone
),
tasks_overdue: await serializeTasks(metrics.tasks_overdue, timezone),
suggested_tasks: await serializeTasks(
metrics.suggested_tasks,
timezone
),
tasks_completed_today: await serializeTasks(
metrics.tasks_completed_today,
timezone
),
};
Object.assign(response, serializedLists);
response.dashboard_lists = serializedLists;
return response;
}
module.exports = {
computeSuggestedTasks,
computeWeeklyCompletions,
computeTaskMetrics,
getTaskMetrics,
};