Fix recurring structure (#646)
* Refactor recurring * fixup! Refactor recurring * Add after completion tests * fixup! Add after completion tests * fixup! fixup! Add after completion tests
This commit is contained in:
parent
e75a6e290e
commit
cd6b810b08
32 changed files with 1957 additions and 3552 deletions
|
|
@ -1,234 +1,3 @@
|
|||
const { Task, sequelize } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const { logError } = require('./logService');
|
||||
const taskRepository = require('../repositories/TaskRepository');
|
||||
|
||||
const generationLocks = new Map();
|
||||
|
||||
const addDays = (date, days) => {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
};
|
||||
|
||||
const generateRecurringTasksWithLock = async (userId, lookAheadDays = 7) => {
|
||||
const lockKey = `user_${userId}`;
|
||||
|
||||
if (generationLocks.get(lockKey)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
generationLocks.set(lockKey, true);
|
||||
return await generateRecurringTasks(userId, lookAheadDays);
|
||||
} catch (error) {
|
||||
logError('Error generating recurring tasks with lock:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
generationLocks.delete(lockKey);
|
||||
}
|
||||
};
|
||||
|
||||
const generateRecurringTasks = async (userId = null, lookAheadDays = 7) => {
|
||||
try {
|
||||
const whereClause = {
|
||||
recurrence_type: { [Op.ne]: 'none' },
|
||||
status: { [Op.ne]: Task.STATUS.ARCHIVED },
|
||||
};
|
||||
|
||||
if (userId) {
|
||||
whereClause.user_id = userId;
|
||||
}
|
||||
|
||||
const recurringTasks = await Task.findAll({
|
||||
where: whereClause,
|
||||
order: [['last_generated_date', 'ASC']],
|
||||
});
|
||||
|
||||
const newTasks = [];
|
||||
const now = new Date();
|
||||
const lookAheadDate = addDays(now, lookAheadDays);
|
||||
|
||||
for (const task of recurringTasks) {
|
||||
const generatedTasks = await processRecurringTask(
|
||||
task,
|
||||
now,
|
||||
lookAheadDate
|
||||
);
|
||||
newTasks.push(...generatedTasks);
|
||||
}
|
||||
|
||||
return newTasks;
|
||||
} catch (error) {
|
||||
console.error('Error generating recurring tasks:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const processRecurringTask = async (task, now, lookAheadDate = null) => {
|
||||
const newTasks = [];
|
||||
const generateUpTo = lookAheadDate || now;
|
||||
|
||||
if (task.recurrence_end_date && now > task.recurrence_end_date) {
|
||||
return newTasks;
|
||||
}
|
||||
|
||||
if (!task.last_generated_date) {
|
||||
const originalDueDate = task.due_date
|
||||
? new Date(task.due_date.getTime())
|
||||
: new Date(now.getTime());
|
||||
|
||||
if (originalDueDate <= generateUpTo) {
|
||||
const startOfDay = new Date(originalDueDate);
|
||||
startOfDay.setUTCHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(originalDueDate);
|
||||
endOfDay.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
const whereClause = {
|
||||
user_id: task.user_id,
|
||||
recurring_parent_id: task.id,
|
||||
due_date: {
|
||||
[Op.between]: [startOfDay, endOfDay],
|
||||
},
|
||||
};
|
||||
|
||||
if (task.project_id !== null && task.project_id !== undefined) {
|
||||
whereClause.project_id = task.project_id;
|
||||
} else {
|
||||
whereClause.project_id = null;
|
||||
}
|
||||
|
||||
const existingTask = await Task.findOne({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (!existingTask) {
|
||||
const newTask = await createTaskInstance(task, originalDueDate);
|
||||
newTasks.push(newTask);
|
||||
}
|
||||
|
||||
if (originalDueDate <= now) {
|
||||
task.last_generated_date = originalDueDate;
|
||||
await task.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nextDueDate = calculateNextDueDate(
|
||||
task,
|
||||
task.last_generated_date || task.due_date || now
|
||||
);
|
||||
|
||||
while (nextDueDate && nextDueDate <= generateUpTo) {
|
||||
const startOfDay = new Date(nextDueDate);
|
||||
startOfDay.setUTCHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(nextDueDate);
|
||||
endOfDay.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
const whereClause = {
|
||||
user_id: task.user_id,
|
||||
recurring_parent_id: task.id,
|
||||
due_date: {
|
||||
[Op.between]: [startOfDay, endOfDay],
|
||||
},
|
||||
};
|
||||
|
||||
if (task.project_id !== null && task.project_id !== undefined) {
|
||||
whereClause.project_id = task.project_id;
|
||||
} else {
|
||||
whereClause.project_id = null;
|
||||
}
|
||||
|
||||
const result = await sequelize.transaction(async (transaction) => {
|
||||
const existingTask = await Task.findOne({
|
||||
where: whereClause,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (existingTask) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await createTaskInstance(task, nextDueDate, transaction);
|
||||
});
|
||||
|
||||
if (result) {
|
||||
newTasks.push(result);
|
||||
}
|
||||
|
||||
if (nextDueDate <= now) {
|
||||
task.last_generated_date = nextDueDate;
|
||||
await task.save();
|
||||
}
|
||||
|
||||
nextDueDate = calculateNextDueDate(task, nextDueDate);
|
||||
|
||||
if (newTasks.length > 100) {
|
||||
console.warn(
|
||||
`Generated 100+ tasks for recurring task ${task.id}, stopping to prevent overflow`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return newTasks;
|
||||
};
|
||||
|
||||
const createTaskInstance = async (template, dueDate, transaction = null) => {
|
||||
const taskData = {
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
due_date: dueDate,
|
||||
today: false,
|
||||
priority: template.priority,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
note: template.note,
|
||||
user_id: template.user_id,
|
||||
project_id: template.project_id,
|
||||
recurrence_type: 'none',
|
||||
recurring_parent_id: template.id,
|
||||
};
|
||||
|
||||
const options = {};
|
||||
if (transaction) {
|
||||
options.transaction = transaction;
|
||||
}
|
||||
|
||||
const newTask = await Task.create(taskData, options);
|
||||
|
||||
const subtasks = await taskRepository.findChildren(
|
||||
template.id,
|
||||
template.user_id
|
||||
);
|
||||
|
||||
if (subtasks && subtasks.length > 0) {
|
||||
const subtasksData = subtasks.map((subtask) => ({
|
||||
name: subtask.name,
|
||||
description: subtask.description,
|
||||
parent_task_id: newTask.id,
|
||||
user_id: template.user_id,
|
||||
priority: subtask.priority,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
note: subtask.note,
|
||||
today: false,
|
||||
recurrence_type: 'none',
|
||||
completion_based: false,
|
||||
}));
|
||||
|
||||
if (transaction) {
|
||||
await Promise.all(
|
||||
subtasksData.map((subtaskData) =>
|
||||
Task.create(subtaskData, { transaction })
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await taskRepository.createMany(subtasksData);
|
||||
}
|
||||
}
|
||||
|
||||
return newTask;
|
||||
};
|
||||
|
||||
const calculateNextDueDate = (task, fromDate) => {
|
||||
if (
|
||||
!task ||
|
||||
|
|
@ -417,67 +186,46 @@ const shouldGenerateNextTask = (task, nextDate) => {
|
|||
return nextDate < task.recurrence_end_date;
|
||||
};
|
||||
|
||||
const handleTaskCompletion = async (task) => {
|
||||
if (!task.recurrence_type || task.recurrence_type === 'none') {
|
||||
return null;
|
||||
const calculateVirtualOccurrences = (task, count = 7, startFrom = null) => {
|
||||
const occurrences = [];
|
||||
let currentDate = startFrom
|
||||
? new Date(startFrom)
|
||||
: task.due_date
|
||||
? new Date(task.due_date)
|
||||
: new Date();
|
||||
let iterationCount = 0;
|
||||
const MAX_ITERATIONS = 100;
|
||||
|
||||
while (occurrences.length < count && iterationCount < MAX_ITERATIONS) {
|
||||
if (
|
||||
task.recurrence_end_date &&
|
||||
currentDate > new Date(task.recurrence_end_date)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
occurrences.push({
|
||||
due_date: currentDate.toISOString().split('T')[0],
|
||||
is_virtual: true,
|
||||
});
|
||||
|
||||
currentDate = calculateNextDueDate(task, currentDate);
|
||||
if (!currentDate) break;
|
||||
|
||||
iterationCount++;
|
||||
}
|
||||
|
||||
if (!task.completion_based) {
|
||||
return null;
|
||||
}
|
||||
|
||||
task.last_generated_date = new Date();
|
||||
await task.save();
|
||||
|
||||
const nextDueDate = calculateNextDueDate(task, new Date());
|
||||
|
||||
if (!nextDueDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startOfDay = new Date(nextDueDate);
|
||||
startOfDay.setUTCHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(nextDueDate);
|
||||
endOfDay.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
const whereClause = {
|
||||
user_id: task.user_id,
|
||||
recurring_parent_id: task.id,
|
||||
due_date: {
|
||||
[Op.between]: [startOfDay, endOfDay],
|
||||
},
|
||||
};
|
||||
|
||||
if (task.project_id !== null && task.project_id !== undefined) {
|
||||
whereClause.project_id = task.project_id;
|
||||
} else {
|
||||
whereClause.project_id = null;
|
||||
}
|
||||
|
||||
const existingTask = await Task.findOne({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (existingTask) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextTask = await createTaskInstance(task, nextDueDate);
|
||||
return nextTask;
|
||||
return occurrences;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateRecurringTasks,
|
||||
generateRecurringTasksWithLock,
|
||||
processRecurringTask,
|
||||
createTaskInstance,
|
||||
calculateNextDueDate,
|
||||
calculateDailyRecurrence,
|
||||
calculateWeeklyRecurrence,
|
||||
calculateMonthlyRecurrence,
|
||||
calculateMonthlyWeekdayRecurrence,
|
||||
calculateMonthlyLastDayRecurrence,
|
||||
handleTaskCompletion,
|
||||
calculateVirtualOccurrences,
|
||||
shouldGenerateNextTask,
|
||||
getFirstWeekdayOfMonth,
|
||||
getLastWeekdayOfMonth,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue