package main import ( "database/sql" "errors" "fmt" "net/http" "strconv" "strings" "time" ) func (a *App) handleSetDaysPerMonth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/general", "error", "Form ungültig") return } days := mustInt(r.FormValue("days_per_month"), 2) if days < 1 || days > 31 { redirectWithMessage(w, r, "/general", "error", "Tage pro Monat 1-31") return } if _, err := a.db.Exec(`UPDATE settings SET days_per_month=? WHERE id=1`, days); err != nil { redirectWithMessage(w, r, "/general", "error", "Einstellung nicht gespeichert") return } redirectWithMessage(w, r, "/general", "info", "Tage pro Monat gespeichert") } func (a *App) handleSetCurrentTime(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/", "error", "Form ungültig") return } settings, err := a.getSettings() if err != nil { redirectWithMessage(w, r, "/", "error", "Einstellungen nicht lesbar") return } month := mustInt(r.FormValue("current_month"), 1) day := mustInt(r.FormValue("current_day"), 1) if month < 1 || month > 12 || day < 1 || day > settings.DaysPerMonth { redirectWithMessage(w, r, "/", "error", "Ingame-Zeit ungültig") return } if _, err := a.db.Exec(`UPDATE settings SET current_month=?, current_day=? WHERE id=1`, month, day); err != nil { redirectWithMessage(w, r, "/", "error", "Ingame-Zeit nicht gespeichert") return } redirectWithMessage(w, r, "/", "info", "Ingame-Zeit gespeichert") } func (a *App) handleCreateField(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/fields", "error", "Form ungültig") return } number := mustInt(r.FormValue("number"), 0) name := strings.TrimSpace(r.FormValue("name")) owned := r.FormValue("owned") == "on" if number <= 0 { redirectWithMessage(w, r, "/fields", "error", "Feldnummer ungültig") return } if _, err := a.db.Exec(`INSERT INTO fields(number,name,owned) VALUES (?,?,?)`, number, name, owned); err != nil { redirectWithMessage(w, r, "/fields", "error", "Feld nicht angelegt") return } redirectWithMessage(w, r, "/fields", "info", "Feld angelegt") } func (a *App) handleUpdateField(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/fields", "error", "Form ungültig") return } id := mustInt64(r.FormValue("id"), 0) name := strings.TrimSpace(r.FormValue("name")) owned := r.FormValue("owned") == "on" if id <= 0 { redirectWithMessage(w, r, "/fields", "error", "Feld-ID ungültig") return } if owned { if _, err := a.db.Exec(`UPDATE fields SET name=?,owned=? WHERE id=?`, name, owned, id); err != nil { redirectWithMessage(w, r, "/fields", "error", "Feld nicht aktualisiert") return } } else { if _, err := a.db.Exec(`UPDATE fields SET name=?,owned=?,group_key='',group_name='' WHERE id=?`, name, owned, id); err != nil { redirectWithMessage(w, r, "/fields", "error", "Feld nicht aktualisiert") return } } redirectWithMessage(w, r, "/fields", "info", "Feld aktualisiert") } func (a *App) handleDeleteField(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/fields", "error", "Form ungültig") return } id := mustInt64(r.FormValue("id"), 0) if id <= 0 { redirectWithMessage(w, r, "/fields", "error", "Feld-ID ungültig") return } if _, err := a.db.Exec(`DELETE FROM fields WHERE id=?`, id); err != nil { redirectWithMessage(w, r, "/fields", "error", "Feld nicht gelöscht") return } redirectWithMessage(w, r, "/fields", "info", "Feld gelöscht") } func (a *App) handleCreateFieldGroup(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/fields", "error", "Form ungültig") return } ids, err := parseInt64List(r.Form["field_ids"]) if err != nil || len(ids) == 0 { redirectWithMessage(w, r, "/fields", "error", "Mindestens ein Feld auswählen") return } fields, err := a.getFieldsByIDs(ids) if err != nil || len(fields) != len(ids) { redirectWithMessage(w, r, "/fields", "error", "Felder nicht gefunden") return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { name = autoGroupName(fields) } groupKey := fmt.Sprintf("g%d", time.Now().UnixNano()) tx, err := a.db.Begin() if err != nil { redirectWithMessage(w, r, "/fields", "error", "Gruppierung fehlgeschlagen") return } defer tx.Rollback() for _, f := range fields { if _, err := tx.Exec(`UPDATE fields SET group_key=?,group_name=? WHERE id=?`, groupKey, name, f.ID); err != nil { redirectWithMessage(w, r, "/fields", "error", "Gruppierung fehlgeschlagen") return } } if err := tx.Commit(); err != nil { redirectWithMessage(w, r, "/fields", "error", "Gruppierung fehlgeschlagen") return } redirectWithMessage(w, r, "/fields", "info", "Feldgruppe gespeichert") } func (a *App) handleDeleteFieldGroup(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/fields", "error", "Form ungültig") return } key := strings.TrimSpace(r.FormValue("group_key")) if key == "" { redirectWithMessage(w, r, "/fields", "error", "Gruppen-ID ungültig") return } if _, err := a.db.Exec(`UPDATE fields SET group_key='',group_name='' WHERE group_key=?`, key); err != nil { redirectWithMessage(w, r, "/fields", "error", "Gruppe nicht aufgelöst") return } redirectWithMessage(w, r, "/fields", "info", "Gruppe aufgelöst") } func (a *App) handleCreateCrop(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/crops", "error", "Form ungültig") return } name := strings.TrimSpace(r.FormValue("name")) start := mustInt(r.FormValue("sow_start_month"), 0) end := mustInt(r.FormValue("sow_end_month"), 0) grow := mustInt(r.FormValue("grow_months"), 0) regrowEnabled := r.FormValue("regrow_enabled") == "on" regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0) if err := validateCropInput(name, start, end, grow, regrowCycles); err != nil { redirectWithMessage(w, r, "/crops", "error", err.Error()) return } if !regrowEnabled { regrowCycles = 0 } if _, err := a.db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,?,?)`, name, start, end, grow, regrowEnabled, regrowCycles); err != nil { redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht angelegt") return } redirectWithMessage(w, r, "/crops", "info", "Feldfrucht angelegt") } func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/crops", "error", "Form ungültig") return } id := mustInt64(r.FormValue("id"), 0) name := strings.TrimSpace(r.FormValue("name")) start := mustInt(r.FormValue("sow_start_month"), 0) end := mustInt(r.FormValue("sow_end_month"), 0) grow := mustInt(r.FormValue("grow_months"), 0) regrowEnabled := r.FormValue("regrow_enabled") == "on" regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0) if id <= 0 { redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungültig") return } if err := validateCropInput(name, start, end, grow, regrowCycles); err != nil { redirectWithMessage(w, r, "/crops", "error", err.Error()) return } if !regrowEnabled { regrowCycles = 0 } if _, err := a.db.Exec(`UPDATE crops SET name=?,sow_start_month=?,sow_end_month=?,grow_months=?,regrow_enabled=?,regrow_cycles=? WHERE id=?`, name, start, end, grow, regrowEnabled, regrowCycles, id); err != nil { redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht aktualisiert") return } redirectWithMessage(w, r, "/crops", "info", "Feldfrucht aktualisiert") } func (a *App) handleDeleteCrop(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/crops", "error", "Form ungültig") return } id := mustInt64(r.FormValue("id"), 0) if id <= 0 { redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungültig") return } if _, err := a.db.Exec(`DELETE FROM crops WHERE id=?`, id); err != nil { redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht gelöscht") return } redirectWithMessage(w, r, "/crops", "info", "Feldfrucht gelöscht") } func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/planning", "error", "Form ungültig") return } targetRef := strings.TrimSpace(r.FormValue("target_ref")) cropID := mustInt64(r.FormValue("crop_id"), 0) startMonth := mustInt(r.FormValue("start_month"), 1) startDay := mustInt(r.FormValue("start_day"), 1) notes := strings.TrimSpace(r.FormValue("notes")) if targetRef == "" || cropID <= 0 { redirectWithMessage(w, r, "/planning", "error", "Planungsziel oder Feldfrucht ungültig") return } settings, err := a.getSettings() if err != nil { redirectWithMessage(w, r, "/planning", "error", "Einstellungen nicht lesbar") return } if startMonth < 1 || startMonth > 12 || startDay < 1 || startDay > settings.DaysPerMonth { redirectWithMessage(w, r, "/planning", "error", "Startdatum ungültig") return } var c Crop err = a.db.QueryRow(`SELECT id,name,sow_start_month,sow_end_month,grow_months FROM crops WHERE id=?`, cropID). Scan(&c.ID, &c.Name, &c.SowStartMonth, &c.SowEndMonth, &c.GrowMonths) if err != nil { redirectWithMessage(w, r, "/planning", "error", "Feldfrucht nicht gefunden") return } if !monthInWindow(startMonth, c.SowStartMonth, c.SowEndMonth) { redirectWithMessage(w, r, "/planning", "error", "Aussaat außerhalb des Zeitfensters") return } fieldID, targetName, err := a.resolvePlanningTarget(targetRef) if err != nil { redirectWithMessage(w, r, "/planning", "error", err.Error()) return } harvestMonth := wrapMonth(startMonth + c.GrowMonths) harvestDay := startDay _, err = a.db.Exec( `INSERT INTO plans(field_id,target_ref,target_name,crop_id,start_month,start_day,harvest_month,harvest_day,notes) VALUES (?,?,?,?,?,?,?,?,?)`, fieldID, targetRef, targetName, cropID, startMonth, startDay, harvestMonth, harvestDay, notes, ) if err != nil { redirectWithMessage(w, r, "/planning", "error", "Plan nicht gespeichert") return } redirectWithMessage(w, r, "/planning", "info", "Plan gespeichert") } func (a *App) handleDeletePlan(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/planning", "error", "Form ungültig") return } id := mustInt64(r.FormValue("id"), 0) if id <= 0 { redirectWithMessage(w, r, "/planning", "error", "Plan-ID ungültig") return } if _, err := a.db.Exec(`DELETE FROM plans WHERE id=?`, id); err != nil { redirectWithMessage(w, r, "/planning", "error", "Plan nicht gelöscht") return } redirectWithMessage(w, r, "/planning", "info", "Plan gelöscht") } func (a *App) handleToggleTaskDone(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/", "error", "Form ungültig") return } uid := strings.TrimSpace(r.FormValue("uid")) month := mustInt(r.FormValue("month"), 0) day := mustInt(r.FormValue("day"), 0) yearOffset := mustInt(r.FormValue("year_offset"), 0) currentDone := r.FormValue("completed") == "1" if uid == "" || month < 1 || month > 12 || day < 1 { redirectWithMessage(w, r, "/", "error", "Aufgabe ungültig") return } if currentDone { if _, err := a.db.Exec(`DELETE FROM task_completions WHERE task_uid=? AND month=? AND day=? AND year_offset=?`, uid, month, day, yearOffset); err != nil { redirectWithMessage(w, r, "/", "error", "Status konnte nicht geändert werden") return } redirectWithMessage(w, r, "/", "info", "Aufgabe wieder offen") return } if _, err := a.db.Exec(`INSERT INTO task_completions(task_uid,month,day,year_offset,done) VALUES (?,?,?,?,1) ON DUPLICATE KEY UPDATE done=1`, uid, month, day, yearOffset); err != nil { redirectWithMessage(w, r, "/", "error", "Status konnte nicht geändert werden") return } redirectWithMessage(w, r, "/", "info", "Aufgabe erledigt") } func (a *App) handleCreateTaskTemplate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/planning", "error", "Form ungültig") return } title := strings.TrimSpace(r.FormValue("title")) if title == "" { redirectWithMessage(w, r, "/planning", "error", "Template-Titel fehlt") return } if _, err := a.db.Exec(`INSERT INTO custom_task_templates(title) VALUES (?)`, title); err != nil { redirectWithMessage(w, r, "/planning", "error", "Template konnte nicht gespeichert werden") return } redirectWithMessage(w, r, "/planning", "info", "Template gespeichert") } func (a *App) handleCreateCustomTask(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/planning", "error", "Form ungültig") return } templateID := mustInt64(r.FormValue("template_id"), 0) title := strings.TrimSpace(r.FormValue("title")) month := mustInt(r.FormValue("month"), 0) day := mustInt(r.FormValue("day"), 0) yearOffset := mustInt(r.FormValue("year_offset"), 0) targetName := strings.TrimSpace(r.FormValue("target_name")) notes := strings.TrimSpace(r.FormValue("notes")) settings, err := a.getSettings() if err != nil { redirectWithMessage(w, r, "/planning", "error", "Einstellungen nicht lesbar") return } if month < 1 || month > 12 || day < 1 || day > settings.DaysPerMonth || yearOffset < 0 || yearOffset > 4 { redirectWithMessage(w, r, "/planning", "error", "Aufgaben-Datum ungültig") return } if templateID > 0 { if err = a.db.QueryRow(`SELECT title FROM custom_task_templates WHERE id=?`, templateID).Scan(&title); err != nil { redirectWithMessage(w, r, "/planning", "error", "Template nicht gefunden") return } } if title == "" { redirectWithMessage(w, r, "/planning", "error", "Aufgaben-Titel fehlt") return } if _, err = a.db.Exec(`INSERT INTO custom_tasks(template_id,title,month,day,year_offset,target_name,notes) VALUES (?,?,?,?,?,?,?)`, nullInt64(templateID), title, month, day, yearOffset, targetName, notes); err != nil { redirectWithMessage(w, r, "/planning", "error", "Aufgabe konnte nicht gespeichert werden") return } redirectWithMessage(w, r, "/planning", "info", "Aufgabe eingeplant") } func (a *App) handleDeleteCustomTask(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/planning", "error", "Form ungültig") return } id := mustInt64(r.FormValue("id"), 0) if id <= 0 { redirectWithMessage(w, r, "/planning", "error", "Aufgaben-ID ungültig") return } if _, err := a.db.Exec(`DELETE FROM custom_tasks WHERE id=?`, id); err != nil { redirectWithMessage(w, r, "/planning", "error", "Aufgabe konnte nicht gelöscht werden") return } redirectWithMessage(w, r, "/planning", "info", "Aufgabe gelöscht") } func nullInt64(v int64) sql.NullInt64 { if v <= 0 { return sql.NullInt64{} } return sql.NullInt64{Int64: v, Valid: true} } func (a *App) handleCreateCropStep(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/crops", "error", "Form ungültig") return } cropID := mustInt64(r.FormValue("crop_id"), 0) phase := strings.TrimSpace(r.FormValue("phase")) offset := mustInt(r.FormValue("month_offset"), -1) title := strings.TrimSpace(r.FormValue("title")) if cropID <= 0 || (phase != "pre" && phase != "post") || offset < 0 || offset > 24 || title == "" { redirectWithMessage(w, r, "/crops", "error", "Vor/Nachbereitung ungültig") return } if _, err := a.db.Exec(`INSERT INTO crop_steps(crop_id,phase,month_offset,title) VALUES (?,?,?,?)`, cropID, phase, offset, title); err != nil { redirectWithMessage(w, r, "/crops", "error", "Schritt nicht gespeichert") return } redirectWithMessage(w, r, "/crops", "info", "Schritt gespeichert") } func (a *App) handleDeleteCropStep(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectWithMessage(w, r, "/crops", "error", "Form ungültig") return } id := mustInt64(r.FormValue("id"), 0) if id <= 0 { redirectWithMessage(w, r, "/crops", "error", "Schritt-ID ungültig") return } if _, err := a.db.Exec(`DELETE FROM crop_steps WHERE id=?`, id); err != nil { redirectWithMessage(w, r, "/crops", "error", "Schritt nicht gelöscht") return } redirectWithMessage(w, r, "/crops", "info", "Schritt gelöscht") } func (a *App) resolvePlanningTarget(ref string) (sql.NullInt64, string, error) { if strings.HasPrefix(ref, "f:") { id := mustInt64(strings.TrimPrefix(ref, "f:"), 0) if id <= 0 { return sql.NullInt64{}, "", errors.New("Feld-Ziel ungültig") } var f Field err := a.db.QueryRow(`SELECT id,number,name,owned FROM fields WHERE id=?`, id).Scan(&f.ID, &f.Number, &f.Name, &f.Owned) if err != nil { return sql.NullInt64{}, "", errors.New("Feld nicht gefunden") } if !f.Owned { return sql.NullInt64{}, "", errors.New("Feld ist nicht im Besitz") } return sql.NullInt64{Int64: f.ID, Valid: true}, fieldLabel(f), nil } if strings.HasPrefix(ref, "g:") { key := strings.TrimPrefix(ref, "g:") if key == "" { return sql.NullInt64{}, "", errors.New("Gruppen-Ziel ungültig") } rows, err := a.db.Query(`SELECT number FROM fields WHERE group_key=? AND owned=1 ORDER BY number`, key) if err != nil { return sql.NullInt64{}, "", errors.New("Gruppe nicht lesbar") } defer rows.Close() var nums []string for rows.Next() { var n int if err := rows.Scan(&n); err != nil { return sql.NullInt64{}, "", errors.New("Gruppe nicht lesbar") } nums = append(nums, strconv.Itoa(n)) } if len(nums) == 0 { return sql.NullInt64{}, "", errors.New("Gruppe hat keine Felder im Besitz") } var groupName string _ = a.db.QueryRow(`SELECT COALESCE(group_name,'') FROM fields WHERE group_key=? LIMIT 1`, key).Scan(&groupName) if strings.TrimSpace(groupName) == "" { groupName = "Feld " + strings.Join(nums, "+") } return sql.NullInt64{}, groupName, nil } return sql.NullInt64{}, "", errors.New("Planungsziel ungültig") }