Add custom interval days support for task frequency
Backend changes: - Add "Custom" frequency option to database seeds - Add custom_interval_days field to Task model - Update task DTOs to accept custom_interval_days - Update task service to use custom_interval_days for next due date calculation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ type CreateTaskRequest struct {
|
|||||||
CategoryID *uint `json:"category_id"`
|
CategoryID *uint `json:"category_id"`
|
||||||
PriorityID *uint `json:"priority_id"`
|
PriorityID *uint `json:"priority_id"`
|
||||||
FrequencyID *uint `json:"frequency_id"`
|
FrequencyID *uint `json:"frequency_id"`
|
||||||
|
CustomIntervalDays *int `json:"custom_interval_days"` // For "Custom" frequency, user-specified days
|
||||||
InProgress bool `json:"in_progress"`
|
InProgress bool `json:"in_progress"`
|
||||||
AssignedToID *uint `json:"assigned_to_id"`
|
AssignedToID *uint `json:"assigned_to_id"`
|
||||||
DueDate *FlexibleDate `json:"due_date"`
|
DueDate *FlexibleDate `json:"due_date"`
|
||||||
@@ -74,6 +75,7 @@ type UpdateTaskRequest struct {
|
|||||||
CategoryID *uint `json:"category_id"`
|
CategoryID *uint `json:"category_id"`
|
||||||
PriorityID *uint `json:"priority_id"`
|
PriorityID *uint `json:"priority_id"`
|
||||||
FrequencyID *uint `json:"frequency_id"`
|
FrequencyID *uint `json:"frequency_id"`
|
||||||
|
CustomIntervalDays *int `json:"custom_interval_days"` // For "Custom" frequency, user-specified days
|
||||||
InProgress *bool `json:"in_progress"`
|
InProgress *bool `json:"in_progress"`
|
||||||
AssignedToID *uint `json:"assigned_to_id"`
|
AssignedToID *uint `json:"assigned_to_id"`
|
||||||
DueDate *FlexibleDate `json:"due_date"`
|
DueDate *FlexibleDate `json:"due_date"`
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ type Task struct {
|
|||||||
Priority *TaskPriority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"`
|
Priority *TaskPriority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"`
|
||||||
FrequencyID *uint `gorm:"column:frequency_id;index" json:"frequency_id"`
|
FrequencyID *uint `gorm:"column:frequency_id;index" json:"frequency_id"`
|
||||||
Frequency *TaskFrequency `gorm:"foreignKey:FrequencyID" json:"frequency,omitempty"`
|
Frequency *TaskFrequency `gorm:"foreignKey:FrequencyID" json:"frequency,omitempty"`
|
||||||
|
CustomIntervalDays *int `gorm:"column:custom_interval_days" json:"custom_interval_days"` // For "Custom" frequency, user-specified days
|
||||||
|
|
||||||
// In Progress flag - replaces status lookup
|
// In Progress flag - replaces status lookup
|
||||||
InProgress bool `gorm:"column:in_progress;default:false;index" json:"in_progress"`
|
InProgress bool `gorm:"column:in_progress;default:false;index" json:"in_progress"`
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
|
|||||||
CategoryID: req.CategoryID,
|
CategoryID: req.CategoryID,
|
||||||
PriorityID: req.PriorityID,
|
PriorityID: req.PriorityID,
|
||||||
FrequencyID: req.FrequencyID,
|
FrequencyID: req.FrequencyID,
|
||||||
|
CustomIntervalDays: req.CustomIntervalDays,
|
||||||
InProgress: req.InProgress,
|
InProgress: req.InProgress,
|
||||||
AssignedToID: req.AssignedToID,
|
AssignedToID: req.AssignedToID,
|
||||||
DueDate: dueDate,
|
DueDate: dueDate,
|
||||||
@@ -237,6 +238,9 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
|
|||||||
if req.FrequencyID != nil {
|
if req.FrequencyID != nil {
|
||||||
task.FrequencyID = req.FrequencyID
|
task.FrequencyID = req.FrequencyID
|
||||||
}
|
}
|
||||||
|
if req.CustomIntervalDays != nil {
|
||||||
|
task.CustomIntervalDays = req.CustomIntervalDays
|
||||||
|
}
|
||||||
if req.InProgress != nil {
|
if req.InProgress != nil {
|
||||||
task.InProgress = *req.InProgress
|
task.InProgress = *req.InProgress
|
||||||
}
|
}
|
||||||
@@ -534,15 +538,25 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
|||||||
|
|
||||||
// Update next_due_date and in_progress based on frequency
|
// Update next_due_date and in_progress based on frequency
|
||||||
// - If frequency is "Once" (days = nil or 0), set next_due_date to nil (marks as completed)
|
// - If frequency is "Once" (days = nil or 0), set next_due_date to nil (marks as completed)
|
||||||
|
// - If frequency is "Custom", use task.CustomIntervalDays for recurrence
|
||||||
// - If frequency is recurring, calculate next_due_date = completion_date + frequency_days
|
// - If frequency is recurring, calculate next_due_date = completion_date + frequency_days
|
||||||
// and reset in_progress to false so task shows in correct kanban column
|
// and reset in_progress to false so task shows in correct kanban column
|
||||||
if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
|
var intervalDays *int
|
||||||
|
if task.Frequency != nil && task.Frequency.Name == "Custom" {
|
||||||
|
// Custom frequency - use task's custom_interval_days
|
||||||
|
intervalDays = task.CustomIntervalDays
|
||||||
|
} else if task.Frequency != nil {
|
||||||
|
// Standard frequency - use frequency's days
|
||||||
|
intervalDays = task.Frequency.Days
|
||||||
|
}
|
||||||
|
|
||||||
|
if intervalDays == nil || *intervalDays == 0 {
|
||||||
// One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
|
// One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
|
||||||
task.NextDueDate = nil
|
task.NextDueDate = nil
|
||||||
task.InProgress = false
|
task.InProgress = false
|
||||||
} else {
|
} else {
|
||||||
// Recurring task - calculate next due date from completion date + frequency
|
// Recurring task - calculate next due date from completion date + interval
|
||||||
nextDue := completedAt.AddDate(0, 0, *task.Frequency.Days)
|
nextDue := completedAt.AddDate(0, 0, *intervalDays)
|
||||||
task.NextDueDate = &nextDue
|
task.NextDueDate = &nextDue
|
||||||
|
|
||||||
// Reset in_progress to false so task appears in upcoming/due_soon
|
// Reset in_progress to false so task appears in upcoming/due_soon
|
||||||
@@ -630,7 +644,15 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update next_due_date and in_progress based on frequency
|
// Update next_due_date and in_progress based on frequency
|
||||||
if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
|
// Determine interval days: Custom frequency uses task.CustomIntervalDays, otherwise use frequency.Days
|
||||||
|
var quickIntervalDays *int
|
||||||
|
if task.Frequency != nil && task.Frequency.Name == "Custom" {
|
||||||
|
quickIntervalDays = task.CustomIntervalDays
|
||||||
|
} else if task.Frequency != nil {
|
||||||
|
quickIntervalDays = task.Frequency.Days
|
||||||
|
}
|
||||||
|
|
||||||
|
if quickIntervalDays == nil || *quickIntervalDays == 0 {
|
||||||
// One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
|
// One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
|
||||||
log.Info().
|
log.Info().
|
||||||
Uint("task_id", task.ID).
|
Uint("task_id", task.ID).
|
||||||
@@ -639,12 +661,16 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
|||||||
task.NextDueDate = nil
|
task.NextDueDate = nil
|
||||||
task.InProgress = false
|
task.InProgress = false
|
||||||
} else {
|
} else {
|
||||||
// Recurring task - calculate next due date from completion date + frequency
|
// Recurring task - calculate next due date from completion date + interval
|
||||||
nextDue := completedAt.AddDate(0, 0, *task.Frequency.Days)
|
nextDue := completedAt.AddDate(0, 0, *quickIntervalDays)
|
||||||
|
frequencyName := "unknown"
|
||||||
|
if task.Frequency != nil {
|
||||||
|
frequencyName = task.Frequency.Name
|
||||||
|
}
|
||||||
log.Info().
|
log.Info().
|
||||||
Uint("task_id", task.ID).
|
Uint("task_id", task.ID).
|
||||||
Str("frequency_name", task.Frequency.Name).
|
Str("frequency_name", frequencyName).
|
||||||
Int("frequency_days", *task.Frequency.Days).
|
Int("interval_days", *quickIntervalDays).
|
||||||
Time("completed_at", completedAt).
|
Time("completed_at", completedAt).
|
||||||
Time("next_due_date", nextDue).
|
Time("next_due_date", nextDue).
|
||||||
Msg("QuickComplete: Recurring task, setting next_due_date")
|
Msg("QuickComplete: Recurring task, setting next_due_date")
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ VALUES
|
|||||||
(5, NOW(), NOW(), 'Monthly', 30, 5),
|
(5, NOW(), NOW(), 'Monthly', 30, 5),
|
||||||
(6, NOW(), NOW(), 'Quarterly', 90, 6),
|
(6, NOW(), NOW(), 'Quarterly', 90, 6),
|
||||||
(7, NOW(), NOW(), 'Semi-Annually', 180, 7),
|
(7, NOW(), NOW(), 'Semi-Annually', 180, 7),
|
||||||
(8, NOW(), NOW(), 'Annually', 365, 8)
|
(8, NOW(), NOW(), 'Annually', 365, 8),
|
||||||
|
(9, NOW(), NOW(), 'Custom', NULL, 9)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
days = EXCLUDED.days,
|
days = EXCLUDED.days,
|
||||||
|
|||||||
Reference in New Issue
Block a user