282 lines
8.6 KiB
JavaScript
282 lines
8.6 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;
|
|
};
|