* 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
211 lines
No EOL
5.8 KiB
JavaScript
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;
|
|
}; |