113 lines
4.2 KiB
TypeScript
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;
|
|
}
|
|
}
|