tududi/backend/models/task_event.js
Chris 03f38f05dc
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
2025-06-27 14:02:18 +03:00

211 lines
No EOL
5.8 KiB
JavaScript

const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const TaskEvent = sequelize.define('TaskEvent', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
task_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'tasks',
key: 'id'
}
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
event_type: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isIn: [['created', 'status_changed', 'priority_changed', 'due_date_changed',
'project_changed', 'name_changed', 'description_changed', 'note_changed',
'completed', 'archived', 'deleted', 'restored', 'today_changed',
'tags_changed', 'recurrence_changed', 'recurrence_type_changed',
'completion_based_changed', 'recurrence_end_date_changed']]
}
},
old_value: {
type: DataTypes.TEXT,
allowNull: true,
get() {
const rawValue = this.getDataValue('old_value');
return rawValue ? JSON.parse(rawValue) : null;
},
set(value) {
this.setDataValue('old_value', value ? JSON.stringify(value) : null);
}
},
new_value: {
type: DataTypes.TEXT,
allowNull: true,
get() {
const rawValue = this.getDataValue('new_value');
return rawValue ? JSON.parse(rawValue) : null;
},
set(value) {
this.setDataValue('new_value', value ? JSON.stringify(value) : null);
}
},
field_name: {
type: DataTypes.STRING,
allowNull: true,
validate: {
isIn: [['status', 'priority', 'due_date', 'project_id', 'name', 'description',
'note', 'today', 'tags', 'recurrence_type', 'recurrence_interval',
'recurrence_end_date', 'recurrence_weekday', 'recurrence_month_day',
'recurrence_week_of_month', 'completion_based']]
}
},
metadata: {
type: DataTypes.TEXT,
allowNull: true,
get() {
const rawValue = this.getDataValue('metadata');
return rawValue ? JSON.parse(rawValue) : null;
},
set(value) {
this.setDataValue('metadata', value ? JSON.stringify(value) : null);
}
}
}, {
tableName: 'task_events',
timestamps: true,
createdAt: 'created_at',
updatedAt: false, // We don't need updated_at for events (they're immutable)
indexes: [
{
fields: ['task_id']
},
{
fields: ['user_id']
},
{
fields: ['event_type']
},
{
fields: ['created_at']
},
{
fields: ['task_id', 'event_type']
},
{
fields: ['task_id', 'created_at']
}
]
});
// Define associations
TaskEvent.associate = function(models) {
// TaskEvent belongs to Task
TaskEvent.belongsTo(models.Task, {
foreignKey: 'task_id',
as: 'Task'
});
// TaskEvent belongs to User
TaskEvent.belongsTo(models.User, {
foreignKey: 'user_id',
as: 'User'
});
};
// Helper methods for common event types
TaskEvent.createStatusChangeEvent = async function(taskId, userId, oldStatus, newStatus, metadata = {}) {
return await TaskEvent.create({
task_id: taskId,
user_id: userId,
event_type: 'status_changed',
field_name: 'status',
old_value: { status: oldStatus },
new_value: { status: newStatus },
metadata: metadata
});
};
TaskEvent.createTaskCreatedEvent = async function(taskId, userId, taskData, metadata = {}) {
return await TaskEvent.create({
task_id: taskId,
user_id: userId,
event_type: 'created',
field_name: null,
old_value: null,
new_value: taskData,
metadata: metadata
});
};
TaskEvent.createFieldChangeEvent = async function(taskId, userId, fieldName, oldValue, newValue, metadata = {}) {
const eventType = fieldName === 'status' && newValue === 2 ? 'completed' :
fieldName === 'status' && newValue === 3 ? 'archived' :
`${fieldName}_changed`;
return await TaskEvent.create({
task_id: taskId,
user_id: userId,
event_type: eventType,
field_name: fieldName,
old_value: { [fieldName]: oldValue },
new_value: { [fieldName]: newValue },
metadata: metadata
});
};
// Query helpers
TaskEvent.getTaskTimeline = async function(taskId) {
return await TaskEvent.findAll({
where: { task_id: taskId },
order: [['created_at', 'ASC']],
include: [{
model: sequelize.models.User,
as: 'User',
attributes: ['id', 'name', 'email']
}]
});
};
TaskEvent.getCompletionTime = async function(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;
const startEvent = events.find(e =>
e.event_type === 'created' ||
(e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress
);
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 {
started_at: startTime,
completed_at: endTime,
duration_ms: endTime - startTime,
duration_hours: (endTime - startTime) / (1000 * 60 * 60)
};
};
return TaskEvent;
};