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