Files
task-manager/server/services/recurrence.ts
T
2025-12-15 21:52:36 +01:00

113 lines
4.2 KiB
TypeScript

import { IStorage } from "../storage";
import { Task, InsertTask } from "@shared/schema";
import { addDays, addWeeks, addMonths, addYears } from "date-fns";
export class RecurrenceService {
private storage: IStorage;
constructor(storage: IStorage) {
this.storage = storage;
}
async handleTaskCompletion(task: Task): Promise<Task | undefined> {
if (!task.isRecurring || !task.recurrenceInterval) {
return;
}
// Determine the next due date
const nextDate = this.calculateNextDate(task);
if (!nextDate) return;
// Check if we passed the end date
if (task.recurrenceEnd && nextDate > task.recurrenceEnd) {
return;
}
// Create the next task
// Note: startDate is nullable in schema but required in InsertTask type if strict.
// We cast to any to avoid strict type issues with recent schema changes if types aren't perfectly synced.
const newTask: any = {
title: task.title,
description: task.description,
priority: task.priority,
status: "todo",
dueDate: nextDate,
estimatedDuration: task.estimatedDuration,
labelId: task.labelId,
projectId: task.projectId,
energyLevel: task.energyLevel,
userId: task.userId,
// Copy recurrence settings
isRecurring: true,
recurrenceInterval: task.recurrenceInterval,
recurrenceIntervalValue: task.recurrenceIntervalValue,
recurrenceDays: task.recurrenceDays,
recurrenceEnd: task.recurrenceEnd,
startDate: null,
};
const created = await this.storage.createTask(newTask);
// Log it
await this.storage.createAuditLog({
userId: task.userId!,
action: "CREATE",
entityType: "TASK",
entityId: created.id,
details: { message: `Recurring task created from ${task.id}`, recurrence: task.recurrenceInterval },
source: "SYSTEM"
});
return created;
}
private calculateNextDate(task: Task): Date | null {
// Base calculation on the original due date or today if missing (though recurring tasks should have due dates)
const baseDate = task.dueDate ? new Date(task.dueDate) : new Date();
const intervalValue = task.recurrenceIntervalValue || 1;
let nextDate: Date = new Date(); // Default init to satisfy typescript, overwritten below
switch (task.recurrenceInterval) {
case 'daily':
nextDate = addDays(baseDate, intervalValue);
break;
case 'weekly':
if (task.recurrenceDays && task.recurrenceDays.length > 0) {
// Complex logic for specific days (e.g., Mon, Wed)
// Simplified approach for MVP: Outlook style often just means "same day next week" if no days specified.
// If days ARE specified, find the next matching day.
let potentialDate = addDays(baseDate, 1);
// Search for next 14 days maximum to avoid infinite loops
let found = false;
for (let i = 0; i < 14; i++) {
const dayOfWeek = potentialDate.getDay(); // 0=Sun, 1=Mon
if (task.recurrenceDays.includes(dayOfWeek)) {
nextDate = potentialDate;
found = true;
break;
}
potentialDate = addDays(potentialDate, 1);
}
if (!found) nextDate = addWeeks(baseDate, intervalValue); // Fallback
} else {
nextDate = addWeeks(baseDate, intervalValue);
}
break;
case 'monthly':
nextDate = addMonths(baseDate, intervalValue);
break;
case 'yearly':
nextDate = addYears(baseDate, intervalValue);
break;
default:
return null;
}
return nextDate;
}
}