tududi/backend/modules/tasks/taskEventService.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

489 lines
12 KiB
JavaScript

const { TaskEvent, sequelize } = require('../../models');
// Helper function to create value object
const createValueObject = (fieldName, value) =>
value ? { [fieldName || 'value']: value } : null;
/**
* Log a task event
* @param {Object} eventData - Event data
* @param {number} eventData.taskId - Task ID
* @param {number} eventData.userId - User ID
* @param {string} eventData.eventType - Type of event
* @param {string} eventData.fieldName - Field that changed (optional)
* @param {any} eventData.oldValue - Old value (optional)
* @param {any} eventData.newValue - New value (optional)
* @param {Object} eventData.metadata - Additional metadata (optional)
*/
const logEvent = async ({
taskId,
userId,
eventType,
fieldName = null,
oldValue = null,
newValue = null,
metadata = {},
}) => {
try {
const finalMetadata = {
source: 'web',
...metadata,
};
const event = await TaskEvent.create({
task_id: taskId,
user_id: userId,
event_type: eventType,
field_name: fieldName,
old_value: createValueObject(fieldName, oldValue),
new_value: createValueObject(fieldName, newValue),
metadata: finalMetadata,
});
return event;
} catch (error) {
console.error('Error logging task event:', error);
throw error;
}
};
/**
* Log task creation event
*/
const logTaskCreated = async (taskId, userId, taskData, metadata = {}) => {
return await logEvent({
taskId,
userId,
eventType: 'created',
newValue: taskData,
metadata: { ...metadata, action: 'task_created' },
});
};
/**
* Log status change event
*/
const logStatusChange = async (
taskId,
userId,
oldStatus,
newStatus,
metadata = {}
) => {
const eventType =
newStatus === 2
? 'completed'
: newStatus === 3
? 'archived'
: 'status_changed';
return await logEvent({
taskId,
userId,
eventType,
fieldName: 'status',
oldValue: oldStatus,
newValue: newStatus,
metadata: { ...metadata, action: 'status_change' },
});
};
/**
* Log priority change event
*/
const logPriorityChange = async (
taskId,
userId,
oldPriority,
newPriority,
metadata = {}
) => {
return await logEvent({
taskId,
userId,
eventType: 'priority_changed',
fieldName: 'priority',
oldValue: oldPriority,
newValue: newPriority,
metadata: { ...metadata, action: 'priority_change' },
});
};
/**
* Log due date change event
*/
const logDueDateChange = async (
taskId,
userId,
oldDueDate,
newDueDate,
metadata = {}
) => {
return await logEvent({
taskId,
userId,
eventType: 'due_date_changed',
fieldName: 'due_date',
oldValue: oldDueDate,
newValue: newDueDate,
metadata: { ...metadata, action: 'due_date_change' },
});
};
/**
* Log project change event
*/
const logProjectChange = async (
taskId,
userId,
oldProjectId,
newProjectId,
metadata = {}
) => {
return await logEvent({
taskId,
userId,
eventType: 'project_changed',
fieldName: 'project_id',
oldValue: oldProjectId,
newValue: newProjectId,
metadata: { ...metadata, action: 'project_change' },
});
};
/**
* Log task name change event
*/
const logNameChange = async (
taskId,
userId,
oldName,
newName,
metadata = {}
) => {
return await logEvent({
taskId,
userId,
eventType: 'name_changed',
fieldName: 'name',
oldValue: oldName,
newValue: newName,
metadata: { ...metadata, action: 'name_change' },
});
};
/**
* Log description change event
*/
const logDescriptionChange = async (
taskId,
userId,
oldDescription,
newDescription,
metadata = {}
) => {
return await logEvent({
taskId,
userId,
eventType: 'description_changed',
fieldName: 'description',
oldValue: oldDescription,
newValue: newDescription,
metadata: { ...metadata, action: 'description_change' },
});
};
// Helper function to determine event type based on field name and value
const getEventType = (fieldName, newValue) => {
switch (fieldName) {
case 'status':
return newValue === 2
? 'completed'
: newValue === 3
? 'archived'
: 'status_changed';
default:
return `${fieldName}_changed`;
}
};
/**
* Log multiple field changes at once
*/
const logTaskUpdate = async (taskId, userId, changes, metadata = {}) => {
const events = [];
for (const [fieldName, { oldValue, newValue }] of Object.entries(changes)) {
// Skip if values are the same
if (oldValue === newValue) continue;
const eventType = getEventType(fieldName, newValue);
const event = await logEvent({
taskId,
userId,
eventType,
fieldName,
oldValue,
newValue,
metadata: { ...metadata, action: 'bulk_update' },
});
events.push(event);
}
return events;
};
/**
* Get task timeline (all events for a task)
*/
const getTaskTimeline = async (taskId) => {
return await TaskEvent.findAll({
where: { task_id: taskId },
order: [['created_at', 'ASC']],
include: [
{
model: require('../../models').User,
as: 'User',
attributes: ['id', 'name', 'email'],
},
],
});
};
// Helper function to find start event
const findStartEvent = (events) =>
events.find(
(e) =>
e.event_type === 'created' ||
(e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress
);
// Helper function to find completed event
const findCompletedEvent = (events) =>
events.find(
(e) =>
e.event_type === 'completed' ||
(e.event_type === 'status_changed' && e.new_value?.status === 2) // done
);
// Helper function to calculate duration metrics
const calculateDurationMetrics = (taskId, startTime, endTime) => ({
task_id: taskId,
started_at: startTime,
completed_at: endTime,
duration_ms: endTime - startTime,
duration_hours: (endTime - startTime) / (1000 * 60 * 60),
duration_days: (endTime - startTime) / (1000 * 60 * 60 * 24),
});
/**
* Get task completion metrics
*/
const getTaskCompletionTime = async (taskId) => {
const events = await TaskEvent.findAll({
where: {
task_id: taskId,
event_type: ['status_changed', 'created', 'completed'],
},
order: [['created_at', 'ASC']],
});
if (events.length === 0) return null;
const startEvent = findStartEvent(events);
const completedEvent = findCompletedEvent(events);
if (!startEvent || !completedEvent) return null;
const startTime = new Date(startEvent.created_at);
const endTime = new Date(completedEvent.created_at);
return calculateDurationMetrics(taskId, startTime, endTime);
};
// Helper function to build where clause for date range
const buildDateWhereClause = (userId, startDate, endDate) => {
const whereClause = { user_id: userId };
if (startDate && endDate) {
whereClause.created_at = {
[require('sequelize').Op.between]: [startDate, endDate],
};
}
return whereClause;
};
// Helper function to calculate basic metrics from events
const calculateBasicMetrics = (events) => ({
total_events: events.length,
tasks_created: events.filter((e) => e.event_type === 'created').length,
tasks_completed: events.filter((e) => e.event_type === 'completed').length,
status_changes: events.filter((e) => e.event_type === 'status_changed')
.length,
average_completion_time: null,
completion_times: [],
});
// Helper function to calculate average completion time
const calculateAverageCompletionTime = (completionTimes) => {
if (completionTimes.length === 0) return null;
const totalHours = completionTimes.reduce(
(sum, ct) => sum + ct.duration_hours,
0
);
return totalHours / completionTimes.length;
};
/**
* Get user productivity metrics
*/
const getUserProductivityMetrics = async (
userId,
startDate = null,
endDate = null
) => {
const whereClause = buildDateWhereClause(userId, startDate, endDate);
const events = await TaskEvent.findAll({
where: whereClause,
order: [['created_at', 'ASC']],
});
const metrics = calculateBasicMetrics(events);
// Calculate completion times for all completed tasks
const completedTasks = events.filter((e) => e.event_type === 'completed');
const completionTimes = [];
for (const completedEvent of completedTasks) {
const taskCompletion = await getTaskCompletionTime(
completedEvent.task_id
);
if (taskCompletion) {
completionTimes.push(taskCompletion);
}
}
if (completionTimes.length > 0) {
metrics.average_completion_time =
calculateAverageCompletionTime(completionTimes);
metrics.completion_times = completionTimes;
}
return metrics;
};
/**
* Get task activity summary for a date range
*/
const getTaskActivitySummary = async (userId, startDate, endDate) => {
const events = await TaskEvent.findAll({
where: {
user_id: userId,
created_at: {
[require('sequelize').Op.between]: [startDate, endDate],
},
},
attributes: [
'event_type',
[
require('sequelize').fn(
'COUNT',
require('sequelize').col('id')
),
'count',
],
[
require('sequelize').fn(
'DATE',
require('sequelize').col('created_at')
),
'date',
],
],
group: ['event_type', 'date'],
order: [['date', 'ASC']],
});
return events;
};
/**
* Get count of how many times a task has been moved to today
*/
const getTaskTodayMoveCount = async (taskId) => {
const { Op } = require('sequelize');
const count = await TaskEvent.count({
where: {
task_id: taskId,
event_type: 'today_changed',
new_value: {
[Op.like]: '%"today":true%',
},
},
});
return count;
};
/**
* Get today move counts for multiple tasks in a single query (bulk operation)
* @param {Array<number>} taskIds - Array of task IDs
* @returns {Promise<Object>} Map of task_id -> count
*/
const getTaskTodayMoveCounts = async (taskIds) => {
const { Op } = require('sequelize');
if (!taskIds || taskIds.length === 0) {
return {};
}
const results = await TaskEvent.findAll({
attributes: [
'task_id',
[sequelize.fn('COUNT', sequelize.col('task_id')), 'move_count'],
],
where: {
task_id: {
[Op.in]: taskIds,
},
event_type: 'today_changed',
new_value: {
[Op.like]: '%"today":true%',
},
},
group: ['task_id'],
raw: true,
});
// Convert array to map for O(1) lookup
const countMap = {};
results.forEach((result) => {
countMap[result.task_id] = parseInt(result.move_count, 10);
});
return countMap;
};
module.exports = {
logEvent,
logTaskCreated,
logStatusChange,
logPriorityChange,
logDueDateChange,
logProjectChange,
logNameChange,
logDescriptionChange,
logTaskUpdate,
getTaskTimeline,
getTaskCompletionTime,
getUserProductivityMetrics,
getTaskActivitySummary,
getTaskTodayMoveCount,
getTaskTodayMoveCounts,
};