tududi/backend/services/taskEventService.js
Antonis Anastasiadis e594d1075b
Linting cleanup (#99)
* Add eslint and prettier dependencies and configs

* Lint project.
2025-07-01 11:40:09 +03:00

417 lines
11 KiB
JavaScript

const { TaskEvent } = require('../models');
class TaskEventService {
/**
* 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)
*/
static async logEvent({
taskId,
userId,
eventType,
fieldName = null,
oldValue = null,
newValue = null,
metadata = {},
}) {
try {
// Add source to metadata if not provided
if (!metadata.source) {
metadata.source = 'web';
}
const event = await TaskEvent.create({
task_id: taskId,
user_id: userId,
event_type: eventType,
field_name: fieldName,
old_value: oldValue
? { [fieldName || 'value']: oldValue }
: null,
new_value: newValue
? { [fieldName || 'value']: newValue }
: null,
metadata: metadata,
});
return event;
} catch (error) {
console.error('Error logging task event:', error);
throw error;
}
}
/**
* Log task creation event
*/
static async logTaskCreated(taskId, userId, taskData, metadata = {}) {
return await this.logEvent({
taskId,
userId,
eventType: 'created',
newValue: taskData,
metadata: { ...metadata, action: 'task_created' },
});
}
/**
* Log status change event
*/
static async logStatusChange(
taskId,
userId,
oldStatus,
newStatus,
metadata = {}
) {
const eventType =
newStatus === 2
? 'completed'
: newStatus === 3
? 'archived'
: 'status_changed';
return await this.logEvent({
taskId,
userId,
eventType,
fieldName: 'status',
oldValue: oldStatus,
newValue: newStatus,
metadata: { ...metadata, action: 'status_change' },
});
}
/**
* Log priority change event
*/
static async logPriorityChange(
taskId,
userId,
oldPriority,
newPriority,
metadata = {}
) {
return await this.logEvent({
taskId,
userId,
eventType: 'priority_changed',
fieldName: 'priority',
oldValue: oldPriority,
newValue: newPriority,
metadata: { ...metadata, action: 'priority_change' },
});
}
/**
* Log due date change event
*/
static async logDueDateChange(
taskId,
userId,
oldDueDate,
newDueDate,
metadata = {}
) {
return await this.logEvent({
taskId,
userId,
eventType: 'due_date_changed',
fieldName: 'due_date',
oldValue: oldDueDate,
newValue: newDueDate,
metadata: { ...metadata, action: 'due_date_change' },
});
}
/**
* Log project change event
*/
static async logProjectChange(
taskId,
userId,
oldProjectId,
newProjectId,
metadata = {}
) {
return await this.logEvent({
taskId,
userId,
eventType: 'project_changed',
fieldName: 'project_id',
oldValue: oldProjectId,
newValue: newProjectId,
metadata: { ...metadata, action: 'project_change' },
});
}
/**
* Log task name change event
*/
static async logNameChange(
taskId,
userId,
oldName,
newName,
metadata = {}
) {
return await this.logEvent({
taskId,
userId,
eventType: 'name_changed',
fieldName: 'name',
oldValue: oldName,
newValue: newName,
metadata: { ...metadata, action: 'name_change' },
});
}
/**
* Log description change event
*/
static async logDescriptionChange(
taskId,
userId,
oldDescription,
newDescription,
metadata = {}
) {
return await this.logEvent({
taskId,
userId,
eventType: 'description_changed',
fieldName: 'description',
oldValue: oldDescription,
newValue: newDescription,
metadata: { ...metadata, action: 'description_change' },
});
}
/**
* Log multiple field changes at once
*/
static async logTaskUpdate(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;
let eventType;
switch (fieldName) {
case 'status':
eventType =
newValue === 2
? 'completed'
: newValue === 3
? 'archived'
: 'status_changed';
break;
default:
eventType = `${fieldName}_changed`;
}
const event = await this.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)
*/
static async getTaskTimeline(taskId) {
return await TaskEvent.findAll({
where: { task_id: taskId },
order: [['created_at', 'ASC']],
include: [
{
model: require('../models').User,
as: 'User',
attributes: ['id', 'name', 'email'],
},
],
});
}
/**
* Get task completion metrics
*/
static async getTaskCompletionTime(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;
// Find when task was started (moved to in_progress or created)
const startEvent = events.find(
(e) =>
e.event_type === 'created' ||
(e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress
);
// Find when task was completed
const completedEvent = events.find(
(e) =>
e.event_type === 'completed' ||
(e.event_type === 'status_changed' && e.new_value?.status === 2) // done
);
if (!startEvent || !completedEvent) return null;
const startTime = new Date(startEvent.created_at);
const endTime = new Date(completedEvent.created_at);
return {
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 user productivity metrics
*/
static async getUserProductivityMetrics(
userId,
startDate = null,
endDate = null
) {
const whereClause = { user_id: userId };
if (startDate && endDate) {
whereClause.created_at = {
[require('sequelize').Op.between]: [startDate, endDate],
};
}
const events = await TaskEvent.findAll({
where: whereClause,
order: [['created_at', 'ASC']],
});
// Calculate metrics
const metrics = {
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: [],
};
// 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 this.getTaskCompletionTime(
completedEvent.task_id
);
if (taskCompletion) {
completionTimes.push(taskCompletion);
}
}
if (completionTimes.length > 0) {
const totalHours = completionTimes.reduce(
(sum, ct) => sum + ct.duration_hours,
0
);
metrics.average_completion_time =
totalHours / completionTimes.length;
metrics.completion_times = completionTimes;
}
return metrics;
}
/**
* Get task activity summary for a date range
*/
static async getTaskActivitySummary(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
*/
static async getTaskTodayMoveCount(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;
}
}
module.exports = TaskEventService;