417 lines
11 KiB
JavaScript
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;
|