Files
Gardomatic/internal/api/task_generator.go
T
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

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()
}