@@ -0,0 +1,142 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gardomatic.kleiax.de/internal/daterange"
|
||||
"gardomatic.kleiax.de/internal/storage"
|
||||
)
|
||||
|
||||
func (app *application) generateTasks(gardenID, userID int, now time.Time) error {
|
||||
plants, err := app.models.Plants.GetAllForGarden(gardenID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing, err := app.models.Tasks.GetAllForGarden(gardenID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, plant := range plants {
|
||||
if plant.SpeciesID == nil || (plant.Status != "alive" && plant.Status != "active") {
|
||||
continue
|
||||
}
|
||||
var species storage.Species
|
||||
templates, err := app.models.SpeciesTaskTemplates.GetAllForSpecies(gardenID, *plant.SpeciesID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, template := range templates {
|
||||
if !template.Active {
|
||||
continue
|
||||
}
|
||||
if template.TriggerType == storage.TaskTriggerRelativeToSowing || template.TriggerType == storage.TaskTriggerRelativeToHarvest || template.TriggerType == storage.TaskTriggerRelativeToSpeciesPlanting {
|
||||
species, err = app.models.Species.Get(gardenID, *plant.SpeciesID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if app.models.TaskTemplateOptOuts != nil {
|
||||
optedOut, optErr := app.models.TaskTemplateOptOuts.IsOptedOut(gardenID, plant.ID, template.ID)
|
||||
if optErr != nil {
|
||||
return optErr
|
||||
}
|
||||
if optedOut {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, task := range generatedTasksForTemplate(plant, species, template, existing, gardenID, userID, now) {
|
||||
if _, err := app.models.Tasks.Insert(task); err != nil && !errors.Is(err, storage.ErrConflict) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generatedTasksForTemplate(plant storage.Plant, species storage.Species, template storage.SpeciesTaskTemplate, existing []storage.Task, gardenID, userID int, now time.Time) []storage.Task {
|
||||
var start, generatedFor time.Time
|
||||
switch template.TriggerType {
|
||||
case storage.TaskTriggerMonthOfYear:
|
||||
if template.MonthFrom == nil || template.DayFrom == nil {
|
||||
return nil
|
||||
}
|
||||
start = nextAnnualStart(now, *template.MonthFrom, *template.DayFrom)
|
||||
generatedFor = daterange.Date(start)
|
||||
case storage.TaskTriggerRelativeToPlanting:
|
||||
if plant.AcquiredAt == nil {
|
||||
return nil
|
||||
}
|
||||
base := daterange.Date(*plant.AcquiredAt)
|
||||
start, generatedFor = addTemplateDuration(base, template.TriggerOffset, template.TriggerOffsetUnit), base
|
||||
case storage.TaskTriggerRelativeToLastTask:
|
||||
base, ok := latestCompletion(existing, plant.ID, template.ID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
base = daterange.Date(base)
|
||||
start, generatedFor = addTemplateDuration(base, template.TriggerOffset, template.TriggerOffsetUnit), base
|
||||
case storage.TaskTriggerRelativeToSowing, storage.TaskTriggerRelativeToHarvest, storage.TaskTriggerRelativeToSpeciesPlanting:
|
||||
month, day := species.SowMonthFrom, species.SowDayFrom
|
||||
if template.TriggerType == storage.TaskTriggerRelativeToHarvest {
|
||||
month, day = species.HarvestMonthFrom, species.HarvestDayFrom
|
||||
} else if template.TriggerType == storage.TaskTriggerRelativeToSpeciesPlanting {
|
||||
month, day = species.PlantingMonthFrom, species.PlantingDayFrom
|
||||
}
|
||||
if month == nil {
|
||||
return nil
|
||||
}
|
||||
d := 1
|
||||
if day != nil {
|
||||
d = *day
|
||||
}
|
||||
base := nextAnnualStart(now, *month, d)
|
||||
start, generatedFor = addTemplateDuration(base, template.TriggerOffset, template.TriggerOffsetUnit), daterange.Date(base)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
makeTask := func(windowStart, windowEnd, slot time.Time) storage.Task {
|
||||
plantID, templateID := plant.ID, template.ID
|
||||
return storage.Task{GardenID: gardenID, PlantID: &plantID, TemplateID: &templateID, Title: template.Title, Description: template.Description, DueAtStart: &windowStart, DueAtEnd: &windowEnd, GeneratedFor: &slot, Recurrence: template.Recurrence, RecurrenceInterval: template.RecurrenceInterval, Priority: template.Priority, Active: true, CreatedBy: userID}
|
||||
}
|
||||
end := endOfDay(addTemplateDuration(start, template.Duration, template.DurationUnit))
|
||||
return []storage.Task{makeTask(start, end, generatedFor)}
|
||||
}
|
||||
|
||||
func nextAnnualStart(now time.Time, month, day int) time.Time {
|
||||
lastDay := time.Date(now.Year(), time.Month(month)+1, 0, 0, 0, 0, 0, now.Location()).Day()
|
||||
if day > lastDay {
|
||||
day = lastDay
|
||||
}
|
||||
result := time.Date(now.Year(), time.Month(month), day, 0, 0, 0, 0, now.Location())
|
||||
if result.Before(daterange.Date(now)) {
|
||||
result = result.AddDate(1, 0, 0)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func addTemplateDuration(value time.Time, amount int, unit storage.TaskDurationUnit) time.Time {
|
||||
switch unit {
|
||||
case storage.TaskDurationWeek:
|
||||
return value.AddDate(0, 0, amount*7)
|
||||
case storage.TaskDurationMonth:
|
||||
return addClampedDate(value, 0, amount)
|
||||
default:
|
||||
return value.AddDate(0, 0, amount)
|
||||
}
|
||||
}
|
||||
|
||||
func endOfDay(value time.Time) time.Time {
|
||||
return daterange.Date(value).AddDate(0, 0, 1).Add(-time.Nanosecond)
|
||||
}
|
||||
|
||||
func latestCompletion(tasks []storage.Task, plantID, templateID int) (time.Time, bool) {
|
||||
var latest time.Time
|
||||
for _, task := range tasks {
|
||||
if task.PlantID != nil && *task.PlantID == plantID && task.TemplateID != nil && *task.TemplateID == templateID && task.CompletedAt != nil && task.CompletedAt.After(latest) {
|
||||
latest = *task.CompletedAt
|
||||
}
|
||||
}
|
||||
return latest, !latest.IsZero()
|
||||
}
|
||||
Reference in New Issue
Block a user