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 { 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; } }