143 lines
5.0 KiB
Go
143 lines
5.0 KiB
Go
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()
|
|
}
|