package notifications import ( "slices" "time" ) // GetScheduleForFrequency returns the reminder schedule (days before due date) // based on the task's frequency interval days. // Returns nil or 0 interval as Once schedule. func GetScheduleForFrequency(intervalDays *int) []int { if intervalDays == nil || *intervalDays == 0 { return FrequencySchedules[0] // Once schedule } days := *intervalDays // Check for exact match first if schedule, exists := FrequencySchedules[days]; exists { return schedule } // For Custom frequencies, find the nearest tier return GetScheduleForCustomInterval(days) } // GetScheduleForCustomInterval looks up schedule for Custom frequency // based on CustomIntervalDays value - finds nearest tier func GetScheduleForCustomInterval(days int) []int { // Ordered tiers from smallest to largest tiers := []int{1, 7, 14, 30, 90, 180, 365} for _, tier := range tiers { if days <= tier { return FrequencySchedules[tier] } } // Fallback to annual schedule for intervals > 365 days return FrequencySchedules[365] } // ShouldSendOverdueReminder checks if we should send an overdue reminder // for a task that is daysOverdue days past its due date. // // Returns true if: // - Days 1 to DailyReminderDays: every day // - Days DailyReminderDays+1 to MaxOverdueDays: every TaperIntervalDays // - After MaxOverdueDays: never func ShouldSendOverdueReminder(daysOverdue int) bool { if daysOverdue <= 0 { return false } // Phase 1: Daily reminders for first N days if daysOverdue <= OverdueConfig.DailyReminderDays { return true } // Phase 2: Tapered reminders (every N days) if daysOverdue <= OverdueConfig.MaxOverdueDays { // Calculate which day in the taper phase daysSinceDaily := daysOverdue - OverdueConfig.DailyReminderDays // Send on days that are multiples of TaperIntervalDays after daily phase // e.g., with DailyReminderDays=3, TaperIntervalDays=3: // Day 4 (daysSinceDaily=1): 1 % 3 = 1 (no) // Day 5 (daysSinceDaily=2): 2 % 3 = 2 (no) // Day 6 (daysSinceDaily=3): 3 % 3 = 0 (yes) -> but we want day 4, 7, 10, 13 // Correcting: we want to send on daysSinceDaily = 1, 4, 7, 10... // So (daysSinceDaily - 1) % TaperIntervalDays == 0 return (daysSinceDaily-1)%OverdueConfig.TaperIntervalDays == 0 } // Phase 3: No more reminders return false } // GetOverdueReminderDays returns all the days on which overdue reminders // should be sent, based on the current OverdueConfig. func GetOverdueReminderDays() []int { var days []int // Add daily reminder days for i := 1; i <= OverdueConfig.DailyReminderDays; i++ { days = append(days, i) } // Add tapered reminder days for i := OverdueConfig.DailyReminderDays + 1; i <= OverdueConfig.MaxOverdueDays; i++ { if ShouldSendOverdueReminder(i) { days = append(days, i) } } return days } // GetReminderStageForToday determines which reminder stage applies today // for a task with the given due date and frequency. // // Returns: // - "reminder_Nd" if task is N days away and in schedule // - "day_of" if task is due today // - "overdue_N" if task is N days overdue and should be reminded // - empty string if no reminder should be sent today func GetReminderStageForToday(dueDate time.Time, frequencyDays *int, today time.Time) string { // Normalize to date only (midnight) dueDate = time.Date(dueDate.Year(), dueDate.Month(), dueDate.Day(), 0, 0, 0, 0, time.UTC) today = time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC) // Calculate days difference diff := int(dueDate.Sub(today).Hours() / 24) if diff > 0 { // Task is in the future - check pre-due reminders schedule := GetScheduleForFrequency(frequencyDays) if slices.Contains(schedule, diff) { return formatDaysBeforeStage(diff) } return "" } else if diff == 0 { // Task is due today return "day_of" } else { // Task is overdue daysOverdue := -diff if ShouldSendOverdueReminder(daysOverdue) { return formatOverdueStage(daysOverdue) } return "" } } // formatDaysBeforeStage returns the stage string for days before due date func formatDaysBeforeStage(days int) string { if days == 0 { return "day_of" } return "reminder_" + itoa(days) + "d" } // formatOverdueStage returns the stage string for days overdue func formatOverdueStage(days int) string { return "overdue_" + itoa(days) } // itoa is a simple int to string helper func itoa(i int) string { if i == 0 { return "0" } if i < 0 { return "-" + itoa(-i) } s := "" for i > 0 { s = string(rune('0'+i%10)) + s i /= 10 } return s } // GetHumanReadableSchedule returns a human-readable description of the // notification schedule for a given frequency interval. func GetHumanReadableSchedule(intervalDays *int) string { days := 0 if intervalDays != nil { days = *intervalDays } if desc, exists := HumanReadableSchedule[days]; exists { return desc } // For custom intervals, find the nearest tier tiers := []int{1, 7, 14, 30, 90, 180, 365} for _, tier := range tiers { if days <= tier { return HumanReadableSchedule[tier] + " (custom)" } } return HumanReadableSchedule[365] + " (custom)" } // GetFrequencyName returns the human-readable name for a frequency interval func GetFrequencyName(intervalDays *int) string { days := 0 if intervalDays != nil { days = *intervalDays } if name, exists := FrequencyNames[days]; exists { return name } return "Custom" }