Setup intelligence (#84)
* 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
This commit is contained in:
parent
3affbe9baf
commit
03f38f05dc
143 changed files with 80005 additions and 21674 deletions
329
backend/services/taskEventService.js
Normal file
329
backend/services/taskEventService.js
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue