Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
Expand Down
31 changes: 13 additions & 18 deletions internal/api/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ type LabelReq struct {
}

type TaskReq struct {
ID string `json:"id"`
Title string `json:"title" binding:"required"`
FrequencyType tModel.FrequencyType `json:"frequency_type"`
NextDueDate int64 `json:"next_due_date"`
IsRolling bool `json:"is_rolling"`
Frequency int `json:"frequency"`
// FrequencyMetadata *tModel.FrequencyMetadata `json:"frequency_metadata"`
Notification bool `json:"notification"`
ID string `json:"id"`
Title string `json:"title" binding:"required"`
NextDueDate int64 `json:"next_due_date"`
IsRolling bool `json:"is_rolling"`
Frequency tModel.Frequency `json:"frequency"`
Notification bool `json:"notification"`
// NotificationMetadata *tModel.NotificationMetadata `json:"notification_metadata"`
}

Expand Down Expand Up @@ -143,10 +141,8 @@ func (h *Handler) createTask(c *gin.Context) {
}

createdTask := &tModel.Task{
Title: TaskReq.Title,
FrequencyType: TaskReq.FrequencyType,
Frequency: TaskReq.Frequency,
// TODO: Serialize utility FrequencyMetadata: TaskReq.FrequencyMetadata,
Title: TaskReq.Title,
Frequency: TaskReq.Frequency,
NextDueDate: dueDate,
CreatedBy: currentUser.ID,
IsRolling: TaskReq.IsRolling,
Expand Down Expand Up @@ -241,17 +237,16 @@ func (h *Handler) editTask(c *gin.Context) {
}*/

updatedTask := &tModel.Task{
ID: taskId,
Title: TaskReq.Title,
FrequencyType: TaskReq.FrequencyType,
Frequency: TaskReq.Frequency,
// TODO: Serialize utility FrequencyMetadata: TaskReq.FrequencyMetadata,
ID: taskId,
Title: TaskReq.Title,
Frequency: TaskReq.Frequency,
NextDueDate: dueDate,
CreatedBy: currentUser.ID,
IsRolling: TaskReq.IsRolling,
Notification: TaskReq.Notification,
IsActive: oldTask.IsActive,
CreatedAt: oldTask.CreatedAt,
// TODO: Serialize utility NotificationMetadata: TaskReq.NotificationMetadata,
CreatedAt: oldTask.CreatedAt,
}
if err := h.tRepo.UpsertTask(c, updatedTask); err != nil {
c.JSON(500, gin.H{
Expand Down
76 changes: 46 additions & 30 deletions internal/models/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,61 @@ package task

import (
"time"

"github.com/lib/pq"
)

type FrequencyType string

const (
FrequancyTypeOnce FrequencyType = "once"
FrequancyTypeDaily FrequencyType = "daily"
FrequancyTypeWeekly FrequencyType = "weekly"
FrequancyTypeMonthly FrequencyType = "monthly"
FrequancyTypeYearly FrequencyType = "yearly"
FrequancyTypeInterval FrequencyType = "interval"
FrequancyTypeDayOfTheWeek FrequencyType = "days_of_the_week"
FrequancyTypeDayOfTheMonth FrequencyType = "day_of_the_month"
FrequancyTypeNoRepeat FrequencyType = "no_repeat"
RepeatOnce = "once"
RepeatDaily = "daily"
RepeatWeekly = "weekly"
RepeatMonthly = "monthly"
RepeatYearly = "yearly"
RepeatCustom = "custom"
)

type IntervalUnit string

const (
Hours IntervalUnit = "hours"
Days IntervalUnit = "days"
Weeks IntervalUnit = "weeks"
Months IntervalUnit = "months"
Years IntervalUnit = "years"
)

type RepeatOn string

const (
Interval RepeatOn = "interval"
DaysOfTheWeek RepeatOn = "days_of_the_week"
DayOfTheMonths RepeatOn = "day_of_the_months"
)

type Frequency struct {
Type FrequencyType `json:"type" validate:"required" gorm:"type:varchar(9)"`
On RepeatOn `json:"on" validate:"required_if=Type interval custom" gorm:"type:varchar(18);default:null"`
Every int `json:"every" validate:"required_if=On interval" gorm:"type:int;default:null"`
Unit IntervalUnit `json:"unit" validate:"required_if=On interval" gorm:"type:varchar(9);default:null"`
Days pq.Int32Array `json:"days" validate:"required_if=Type custom On days_of_the_week,dive,gte=0,lte=6" gorm:"type:integer[];default:null"`
Months pq.Int32Array `json:"months" validate:"required_if=Type custom On day_of_the_months,dive,gte=0,lte=11" gorm:"type:integer[];default:null"`
}

type Task struct {
// TODO: Frequency metadata should either be a different set of columns or be deleted
// TODO: Notification metadata should be separate columns
ID int `json:"id" gorm:"primary_key"`
Title string `json:"title" gorm:"column:title"`
FrequencyType FrequencyType `json:"frequency_type" gorm:"column:frequency_type"`
Frequency int `json:"frequency" gorm:"column:frequency"`
FrequencyMetadata *string `json:"frequency_metadata" gorm:"column:frequency_metadata"`
NextDueDate *time.Time `json:"next_due_date" gorm:"column:next_due_date;index"`
IsRolling bool `json:"is_rolling" gorm:"column:is_rolling"`
CreatedBy int `json:"created_by" gorm:"column:created_by"`
IsActive bool `json:"is_active" gorm:"column:is_active"`
Notification bool `json:"notification" gorm:"column:notification"`
NotificationMetadata *string `json:"notification_metadata" gorm:"column:notification_metadata"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
ID int `json:"id" gorm:"primary_key"`
Title string `json:"title" gorm:"column:title"`
Frequency Frequency `json:"frequency" gorm:"embedded;embeddedPrefix:frequency_"`
NextDueDate *time.Time `json:"next_due_date" gorm:"column:next_due_date;index"`
IsRolling bool `json:"is_rolling" gorm:"column:is_rolling"`
CreatedBy int `json:"created_by" gorm:"column:created_by"`
IsActive bool `json:"is_active" gorm:"column:is_active"`
Notification bool `json:"notification" gorm:"column:notification"`
NotificationMetadata *string `json:"notification_metadata" gorm:"column:notification_metadata"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
}

type TaskHistory struct {
Expand All @@ -43,13 +66,6 @@ type TaskHistory struct {
DueDate *time.Time `json:"due_date" gorm:"column:due_date"`
}

type FrequencyMetadata struct {
Days []*string `json:"days,omitempty"`
Months []*string `json:"months,omitempty"`
Unit *string `json:"unit,omitempty"`
Time string `json:"time,omitempty"`
}

type NotificationMetadata struct {
DueDate bool `json:"due_date,omitempty"`
Completion bool `json:"completion,omitempty"`
Expand Down
153 changes: 68 additions & 85 deletions internal/repos/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package repos

import (
"context"
"fmt"
"strings"
"errors"
"time"

config "dkhalife.com/tasks/core/config"
Expand Down Expand Up @@ -76,13 +75,14 @@ func (r *TaskRepository) CompleteTask(c context.Context, task *tModel.Task, user
if dueDate == nil {
updates["is_active"] = false
}
// Perform the update operation once, using the prepared updates map.

if err := tx.Model(&tModel.Task{}).Where("id = ?", task.ID).Updates(updates).Error; err != nil {
return err
}

return nil
})

return err
}

Expand All @@ -95,103 +95,86 @@ func (r *TaskRepository) GetTaskHistory(c context.Context, taskID int) ([]*tMode
}

func ScheduleNextDueDate(task *tModel.Task, completedDate time.Time) (*time.Time, error) {
// if task is rolling then the next due date calculated from the completed date, otherwise it's calculated from the due date
var nextDueDate time.Time
var baseDate time.Time
// TODO: Utility to deserialize from task.FrequencyMetadata
var frequencyMetadata *tModel.FrequencyMetadata

if task.FrequencyType == "once" {
var freq = task.Frequency
if freq.Type == "once" {
return nil, nil
}

if task.NextDueDate != nil {
// no due date set, use the current date
baseDate = task.NextDueDate.UTC()
} else {
baseDate = completedDate.UTC()
var baseDate time.Time = *task.NextDueDate
if task.IsRolling {
baseDate = completedDate
}

if task.FrequencyType == "day_of_the_month" || task.FrequencyType == "days_of_the_week" || task.FrequencyType == "interval" {
// time in frequency metadata stored as RFC3339 format like `2024-07-07T13:27:00-04:00`
// parse it to time.Time:
t, err := time.Parse(time.RFC3339, frequencyMetadata.Time)
if err != nil {
return nil, fmt.Errorf("error parsing time in frequency metadata")
}
// set the time to the time in the frequency metadata:
baseDate = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())

if baseDate.IsZero() {
return nil, errors.New("unable to calculate next due date")
}
if task.IsRolling {
baseDate = completedDate.UTC()
}
if task.FrequencyType == "daily" {

var nextDueDate time.Time
if freq.Type == "daily" {
nextDueDate = baseDate.AddDate(0, 0, 1)
} else if task.FrequencyType == "weekly" {
} else if freq.Type == "weekly" {
nextDueDate = baseDate.AddDate(0, 0, 7)
} else if task.FrequencyType == "monthly" {
} else if freq.Type == "monthly" {
nextDueDate = baseDate.AddDate(0, 1, 0)
} else if task.FrequencyType == "yearly" {
} else if freq.Type == "yearly" {
nextDueDate = baseDate.AddDate(1, 0, 0)
} else if task.FrequencyType == "once" {
// if the task is a one-time task, then the next due date is nil
} else if task.FrequencyType == "interval" {
// calculate the difference between the due date and now in days:
if *frequencyMetadata.Unit == "hours" {
nextDueDate = baseDate.UTC().Add(time.Hour * time.Duration(task.Frequency))
} else if *frequencyMetadata.Unit == "days" {
nextDueDate = baseDate.UTC().AddDate(0, 0, task.Frequency)
} else if *frequencyMetadata.Unit == "weeks" {
nextDueDate = baseDate.UTC().AddDate(0, 0, task.Frequency*7)
} else if *frequencyMetadata.Unit == "months" {
nextDueDate = baseDate.UTC().AddDate(0, task.Frequency, 0)
} else if *frequencyMetadata.Unit == "years" {
nextDueDate = baseDate.UTC().AddDate(task.Frequency, 0, 0)
} else {

return nil, fmt.Errorf("invalid frequency unit, cannot calculate next due date")
}
} else if task.FrequencyType == "days_of_the_week" {
//we can only assign to days of the week that part of the frequency metadata.days
//it's array of days of the week, for example ["monday", "tuesday", "wednesday"]

// we need to find the next day of the week in the frequency metadata.days that we can schedule
// if this the last or there is only one. will use same otherwise find the next one:

// find the index of the task day in the frequency metadata.days
// loop for next 7 days from the base, if the day in the frequency metadata.days then we can schedule it:
for i := 1; i <= 7; i++ {
nextDueDate = baseDate.AddDate(0, 0, i)
nextDay := strings.ToLower(nextDueDate.Weekday().String())
for _, day := range frequencyMetadata.Days {
if strings.ToLower(*day) == nextDay {
nextDate := nextDueDate.UTC()
return &nextDate, nil
} else if freq.Type == "custom" {
if freq.On == "interval" {
if freq.Unit == "hours" {
nextDueDate = baseDate.Add(time.Duration(freq.Every) * time.Hour)
} else if freq.Unit == "days" {
nextDueDate = baseDate.AddDate(0, 0, freq.Every)
} else if freq.Unit == "weeks" {
nextDueDate = baseDate.AddDate(0, 0, 7*freq.Every)
} else if freq.Unit == "months" {
nextDueDate = baseDate.AddDate(0, freq.Every, 0)
} else if freq.Unit == "years" {
nextDueDate = baseDate.AddDate(freq.Every, 0, 0)
}
} else if freq.On == "days_of_the_week" {
currentWeekDay := int32(baseDate.Weekday())
days := freq.Days

if len(days) == 0 {
return nil, errors.New("days of the week cannot be empty")
}

duringThisWeek := false
for _, day := range days {
if day > currentWeekDay {
duringThisWeek = true
nextDueDate = baseDate.AddDate(0, 0, int(day-currentWeekDay))
break
}
}
}
} else if task.FrequencyType == "day_of_the_month" {
for i := 1; i <= 12; i++ {
nextDueDate = baseDate.AddDate(0, i, 0)
// set the date to the first day of the month:
nextDueDate = time.Date(nextDueDate.Year(), nextDueDate.Month(), task.Frequency, nextDueDate.Hour(), nextDueDate.Minute(), 0, 0, nextDueDate.Location())
nextMonth := strings.ToLower(nextDueDate.Month().String())
for _, month := range frequencyMetadata.Months {
if *month == nextMonth {
nextDate := nextDueDate.UTC()
return &nextDate, nil

if !duringThisWeek {
daysUntilNextWeek := 7 - int(currentWeekDay)
nextDueDate = baseDate.AddDate(0, 0, daysUntilNextWeek+int(days[0]))
}
} else if freq.On == "day_of_the_months" {
currentMonth := int32(baseDate.Month())
months := freq.Months

if len(months) == 0 {
return nil, errors.New("months cannot be empty")
}

duringThisYear := false
for _, month := range months {
if month > currentMonth {
duringThisYear = true
nextDueDate = baseDate.AddDate(0, int(month-currentMonth), 0)
break
}
}

if !duringThisYear {
monthsUntilNextYear := 12 - int(currentMonth)
nextDueDate = baseDate.AddDate(0, monthsUntilNextYear+int(months[0]), 0)
}
}
} else if task.FrequencyType == "no_repeat" {
return nil, nil
} else if task.FrequencyType == "trigger" {
// if the task is a trigger task, then the next due date is nil
return nil, nil
} else {
return nil, fmt.Errorf("invalid frequency type, cannot calculate next due date")
}
return &nextDueDate, nil

return &nextDueDate, nil
}
2 changes: 1 addition & 1 deletion internal/services/planner/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func NewNotificationPlanner(nr *nRepo.NotificationRepository) *NotificationPlann
func (n *NotificationPlanner) GenerateNotifications(c context.Context, task *tModel.Task) bool {
n.nRepo.DeleteAllTaskNotifications(task.ID)
notifications := make([]*nModel.Notification, 0)
if !task.Notification || task.FrequencyType == "trigger" {
if !task.Notification {
return true
}
// TODO: Utility to deserialize from task.NotificationMetadata
Expand Down
Loading