package main import ( "fmt" "sort" "strings" ) func buildTasksForDay(plans []Plan, crops []Crop, products []Product, stepMap map[int64][]CropStep, customTasks []CustomTask, doneMap map[string]bool, month, day int) []Task { tasks := collectTasksForDate(plans, crops, products, stepMap, customTasks, month, day, 0) applyCompletion(tasks, doneMap) sortTasks(tasks) return tasks } func buildCalendar(plans []Plan, crops []Crop, products []Product, stepMap map[int64][]CropStep, customTasks []CustomTask, doneMap map[string]bool, startPeriod, startDay, daysPerMonth, spanMonths int) []CalendarMonth { var out []CalendarMonth startCycle := periodCycle(startPeriod) for offset := 0; offset < spanMonths; offset++ { period := startPeriod + offset month := periodMonth(period) yearOffset := periodCycle(period) - startCycle label := monthNames[month-1] if yearOffset > 0 { label = fmt.Sprintf("%s (+%d Zyklus)", label, yearOffset) } fromDay := 1 if offset == 0 { fromDay = startDay } var days []CalendarDay for d := fromDay; d <= daysPerMonth; d++ { items := collectTasksForDate(plans, crops, products, stepMap, customTasks, period, d, yearOffset) applyCompletion(items, doneMap) sortTasks(items) days = append(days, CalendarDay{Day: d, Tasks: items, Groups: groupTasksByField(items)}) } out = append(out, CalendarMonth{ Offset: offset, Month: month, Label: label, YearOffset: yearOffset, Days: days, }) } return out } func groupTasksByField(tasks []Task) []FieldTaskGroup { if len(tasks) == 0 { return nil } order := make([]string, 0, len(tasks)) grouped := make(map[string][]Task) for _, t := range tasks { if _, ok := grouped[t.Field]; !ok { order = append(order, t.Field) } grouped[t.Field] = append(grouped[t.Field], t) } out := make([]FieldTaskGroup, 0, len(order)) for _, field := range order { out = append(out, FieldTaskGroup{Field: field, Tasks: grouped[field]}) } return out } func collectTasksForDate(plans []Plan, crops []Crop, products []Product, stepMap map[int64][]CropStep, customTasks []CustomTask, period, day, yearOffset int) []Task { var tasks []Task month := periodMonth(period) for _, p := range plans { tasks = append(tasks, expandPlanDayTasks(p, stepMap[p.CropID], period, day, yearOffset)...) } if day == 1 { for _, c := range crops { if c.BestSaleMonth == month { tasks = append(tasks, Task{ UID: fmt.Sprintf("sale:crop:%d", c.ID), Type: "Verkauf", Field: "Markt", Message: fmt.Sprintf("Bestpreis Feldfrucht: %s", c.Name), Month: month, Day: day, YearOffset: yearOffset, SortOrder: 35, }) } } for _, p := range products { if p.BestSaleMonth == month { tasks = append(tasks, Task{ UID: fmt.Sprintf("sale:product:%d", p.ID), Type: "Verkauf", Field: "Markt", Message: fmt.Sprintf("Bestpreis Produktionsgut: %s", p.Name), Month: month, Day: day, YearOffset: yearOffset, SortOrder: 36, }) } } } for _, c := range customTasks { if c.TaskPeriod == period && c.Day == day { field := c.TargetName if strings.TrimSpace(field) == "" { field = "Allgemein" } msg := c.Title if strings.TrimSpace(c.Notes) != "" { msg = fmt.Sprintf("%s (Notiz: %s)", msg, c.Notes) } tasks = append(tasks, Task{ UID: fmt.Sprintf("custom:%d", c.ID), Type: "Aufgabe", Field: field, Message: msg, Month: periodMonth(period), Day: day, YearOffset: yearOffset, SortOrder: 40, }) } } return tasks } func expandPlanDayTasks(p Plan, steps []CropStep, period, day, yearOffset int) []Task { field := p.TargetName if strings.TrimSpace(field) == "" { field = "Unbekanntes Feld" } month := periodMonth(period) var out []Task if p.StartPeriod == period && p.StartDay == day { out = append(out, Task{ UID: fmt.Sprintf("plan:%d:sow:%d", p.ID, p.StartPeriod), Type: "Aussaat", Field: field, Message: withOptionalNote(fmt.Sprintf("Aussaat %s", p.CropName), p.Notes), Month: month, Day: day, YearOffset: yearOffset, SortOrder: 10, }) } harvestPeriods := []int{p.HarvestPeriod} if p.RegrowEnabled { maxExtra := p.RegrowCycles if maxExtra == 0 { maxExtra = 24 } for i := 1; i <= maxExtra; i++ { harvestPeriods = append(harvestPeriods, p.HarvestPeriod+(i*p.HarvestDistance())) } } harvestPeriods = uniquePeriods(harvestPeriods) for _, harvestPeriod := range harvestPeriods { if harvestPeriod == period && p.HarvestDay == day { uid := fmt.Sprintf("plan:%d:harvest:%d", p.ID, harvestPeriod) out = append(out, Task{ UID: uid, Type: "Ernte", Field: field, Message: withOptionalNote(fmt.Sprintf("Ernte %s", p.CropName), p.Notes), Month: month, Day: day, YearOffset: yearOffset, SortOrder: 20, }) } } for _, s := range steps { switch s.Phase { case "pre": taskPeriod := p.StartPeriod - s.MonthOffset if taskPeriod == period && p.StartDay == day { out = append(out, Task{ UID: fmt.Sprintf("plan:%d:pre:%d:%d", p.ID, s.ID, taskPeriod), Type: "Vorbereitung", Field: field, Message: s.Title, Month: month, Day: day, YearOffset: yearOffset, SortOrder: 5, }) } case "post": for _, harvestPeriod := range harvestPeriods { taskPeriod := harvestPeriod + s.MonthOffset if taskPeriod == period && p.HarvestDay == day { out = append(out, Task{ UID: fmt.Sprintf("plan:%d:post:%d:%d", p.ID, s.ID, taskPeriod), Type: "Nachbereitung", Field: field, Message: s.Title, Month: month, Day: day, YearOffset: yearOffset, SortOrder: 30, }) } } } } return out } func applyCompletion(tasks []Task, doneMap map[string]bool) { for i := range tasks { k := completionKey(tasks[i].UID, tasks[i].Month, tasks[i].Day, tasks[i].YearOffset) tasks[i].Completed = doneMap[k] } } func (p Plan) HarvestDistance() int { if p.GrowMonths <= 0 { return 1 } return p.GrowMonths } func withOptionalNote(base, note string) string { n := strings.TrimSpace(note) if n == "" { return base } return fmt.Sprintf("%s (Notiz: %s)", base, n) } func sortTasks(tasks []Task) { sort.Slice(tasks, func(i, j int) bool { if tasks[i].Completed != tasks[j].Completed { return !tasks[i].Completed } if tasks[i].SortOrder == tasks[j].SortOrder { if tasks[i].Field == tasks[j].Field { return tasks[i].Message < tasks[j].Message } return tasks[i].Field < tasks[j].Field } return tasks[i].SortOrder < tasks[j].SortOrder }) } func uniquePeriods(values []int) []int { seen := make(map[int]bool) var out []int for _, v := range values { if !seen[v] { seen[v] = true out = append(out, v) } } return out } func completionKey(uid string, month, day, yearOffset int) string { return fmt.Sprintf("%s|%d|%d|%d", uid, month, day, yearOffset) }