* Add next suggestions and remove console logs * Add pomodoro timer * Add pomodoro switch in settings * Fix pomodoro setting * Add timezones to settings * Fix an issue with password reset * Cleanup * Sort tags alphabetically * Clean up today's view * Add an indicator for repeatedly added to today * Refactor tags * Add due date today item * Move recurrence to the subtitle area * Fix today layout * Add a badge to Inbox items * Move inbox badge to sidebar * Add quotes and progress bar * Add translations for quotes * Fix test issues * Add helper script for docker local * Set up overdue tasks * Add linux/arm/v7 build to deploy script * Add linux/arm/v7 build to deploy script pt2 * Fix an issue with helmet and SSL * Add volume db persistence * Fix cog icon issues
329 lines
No EOL
8.9 KiB
JavaScript
329 lines
No EOL
8.9 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; |