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:
Chris 2025-06-27 14:02:18 +03:00 committed by GitHub
parent 3affbe9baf
commit 03f38f05dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
143 changed files with 80005 additions and 21674 deletions

View 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;