From b1c502a919820eba8df4df1bde2cfc181ea6a67b Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 16 Feb 2026 14:03:19 +0100 Subject: [PATCH] Add sales-month planning for crops/products and fix today task label format --- cmd/server/calendar.go | 40 ++++++++++++++--- cmd/server/db.go | 33 ++++++++++++-- cmd/server/handlers_actions.go | 80 +++++++++++++++++++++++++++++++++- cmd/server/handlers_pages.go | 20 ++++++++- cmd/server/main.go | 3 ++ cmd/server/types.go | 8 ++++ templates/crops.html | 45 ++++++++++++++++++- templates/dashboard.html | 2 +- 8 files changed, 216 insertions(+), 15 deletions(-) diff --git a/cmd/server/calendar.go b/cmd/server/calendar.go index 0cb4a14..3fa4a15 100644 --- a/cmd/server/calendar.go +++ b/cmd/server/calendar.go @@ -6,14 +6,14 @@ import ( "strings" ) -func buildTasksForDay(plans []Plan, stepMap map[int64][]CropStep, customTasks []CustomTask, doneMap map[string]bool, month, day int) []Task { - tasks := collectTasksForDate(plans, stepMap, customTasks, month, day, 0) +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, stepMap map[int64][]CropStep, customTasks []CustomTask, doneMap map[string]bool, startMonth, startDay, daysPerMonth, spanMonths int) []CalendarMonth { +func buildCalendar(plans []Plan, crops []Crop, products []Product, stepMap map[int64][]CropStep, customTasks []CustomTask, doneMap map[string]bool, startMonth, startDay, daysPerMonth, spanMonths int) []CalendarMonth { var out []CalendarMonth for offset := 0; offset < spanMonths; offset++ { month := wrapMonth(startMonth + offset) @@ -30,7 +30,7 @@ func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, customTasks []Cus var days []CalendarDay for d := fromDay; d <= daysPerMonth; d++ { - items := collectTasksForDate(plans, stepMap, customTasks, month, d, yearOffset) + items := collectTasksForDate(plans, crops, products, stepMap, customTasks, month, d, yearOffset) applyCompletion(items, doneMap) sortTasks(items) days = append(days, CalendarDay{Day: d, Tasks: items}) @@ -46,12 +46,42 @@ func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, customTasks []Cus return out } -func collectTasksForDate(plans []Plan, stepMap map[int64][]CropStep, customTasks []CustomTask, month, day, yearOffset int) []Task { +func collectTasksForDate(plans []Plan, crops []Crop, products []Product, stepMap map[int64][]CropStep, customTasks []CustomTask, month, day, yearOffset int) []Task { var tasks []Task for _, p := range plans { tasks = append(tasks, expandPlanDayTasks(p, stepMap[p.CropID], month, 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.Month == month && c.Day == day && c.YearOffset == yearOffset { field := c.TargetName diff --git a/cmd/server/db.go b/cmd/server/db.go index 23ae727..9d0def6 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -38,12 +38,21 @@ func ensureSchema(db *sql.DB) error { sow_start_month TINYINT NOT NULL, sow_end_month TINYINT NOT NULL, grow_months TINYINT NOT NULL, + best_sale_month TINYINT NOT NULL DEFAULT 0, regrow_enabled TINYINT(1) NOT NULL DEFAULT 0, regrow_cycles INT NOT NULL DEFAULT 0 )`, + `ALTER TABLE crops ADD COLUMN IF NOT EXISTS best_sale_month TINYINT NOT NULL DEFAULT 0`, `ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_enabled TINYINT(1) NOT NULL DEFAULT 0`, `ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_cycles INT NOT NULL DEFAULT 0`, `UPDATE crops SET regrow_cycles=0 WHERE regrow_cycles < 0`, + `UPDATE crops SET best_sale_month=0 WHERE best_sale_month < 0 OR best_sale_month > 12`, + `CREATE TABLE IF NOT EXISTS products( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL UNIQUE, + best_sale_month TINYINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + )`, `CREATE TABLE IF NOT EXISTS crop_steps( id BIGINT AUTO_INCREMENT PRIMARY KEY, crop_id BIGINT NOT NULL, @@ -154,7 +163,7 @@ func (a *App) getFieldsByIDs(ids []int64) ([]Field, error) { } func (a *App) listCrops() ([]Crop, error) { - rows, err := a.db.Query(`SELECT id,name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles FROM crops ORDER BY name`) + rows, err := a.db.Query(`SELECT id,name,sow_start_month,sow_end_month,grow_months,best_sale_month,regrow_enabled,regrow_cycles FROM crops ORDER BY name`) if err != nil { return nil, err } @@ -162,7 +171,7 @@ func (a *App) listCrops() ([]Crop, error) { var out []Crop for rows.Next() { var c Crop - if err := rows.Scan(&c.ID, &c.Name, &c.SowStartMonth, &c.SowEndMonth, &c.GrowMonths, &c.RegrowEnabled, &c.RegrowCycles); err != nil { + if err := rows.Scan(&c.ID, &c.Name, &c.SowStartMonth, &c.SowEndMonth, &c.GrowMonths, &c.BestSaleMonth, &c.RegrowEnabled, &c.RegrowCycles); err != nil { return nil, err } out = append(out, c) @@ -170,6 +179,24 @@ func (a *App) listCrops() ([]Crop, error) { return out, rows.Err() } +func (a *App) listProducts() ([]Product, error) { + rows, err := a.db.Query(`SELECT id,name,best_sale_month FROM products ORDER BY name`) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []Product + for rows.Next() { + var p Product + if err := rows.Scan(&p.ID, &p.Name, &p.BestSaleMonth); err != nil { + return nil, err + } + out = append(out, p) + } + return out, rows.Err() +} + func (a *App) listPlans() ([]Plan, error) { rows, err := a.db.Query(` SELECT p.id,p.field_id,COALESCE(p.target_ref,''),COALESCE(p.target_name,''),p.crop_id,COALESCE(c.name,''),COALESCE(c.grow_months,1),p.start_month,p.start_day,p.harvest_month,p.harvest_day,COALESCE(p.notes,''),COALESCE(c.regrow_enabled,0),COALESCE(c.regrow_cycles,0) @@ -301,7 +328,7 @@ func seedCrops(db *sql.DB) error { {Name: "Baumwolle", SowStartMonth: 2, SowEndMonth: 3, GrowMonths: 8}, } for _, c := range items { - if _, err := db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,0,0) ON DUPLICATE KEY UPDATE sow_start_month=VALUES(sow_start_month),sow_end_month=VALUES(sow_end_month),grow_months=VALUES(grow_months)`, c.Name, c.SowStartMonth, c.SowEndMonth, c.GrowMonths); err != nil { + if _, err := db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,best_sale_month,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,0,0,0) ON DUPLICATE KEY UPDATE sow_start_month=VALUES(sow_start_month),sow_end_month=VALUES(sow_end_month),grow_months=VALUES(grow_months)`, c.Name, c.SowStartMonth, c.SowEndMonth, c.GrowMonths); err != nil { return err } } diff --git a/cmd/server/handlers_actions.go b/cmd/server/handlers_actions.go index b7f56ae..835dd0c 100644 --- a/cmd/server/handlers_actions.go +++ b/cmd/server/handlers_actions.go @@ -210,16 +210,21 @@ func (a *App) handleCreateCrop(w http.ResponseWriter, r *http.Request) { start := mustInt(r.FormValue("sow_start_month"), 0) end := mustInt(r.FormValue("sow_end_month"), 0) grow := mustInt(r.FormValue("grow_months"), 0) + bestSaleMonth := mustInt(r.FormValue("best_sale_month"), 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 bestSaleMonth < 0 || bestSaleMonth > 12 { + redirectWithMessage(w, r, "/crops", "error", "Bestpreis-Monat muss 0-12 sein") + 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 { + if _, err := a.db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,best_sale_month,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,?,?,?)`, name, start, end, grow, bestSaleMonth, regrowEnabled, regrowCycles); err != nil { redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht angelegt") return } @@ -240,6 +245,7 @@ func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) { start := mustInt(r.FormValue("sow_start_month"), 0) end := mustInt(r.FormValue("sow_end_month"), 0) grow := mustInt(r.FormValue("grow_months"), 0) + bestSaleMonth := mustInt(r.FormValue("best_sale_month"), 0) regrowEnabled := r.FormValue("regrow_enabled") == "on" regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0) if id <= 0 { @@ -250,10 +256,14 @@ func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) { redirectWithMessage(w, r, "/crops", "error", err.Error()) return } + if bestSaleMonth < 0 || bestSaleMonth > 12 { + redirectWithMessage(w, r, "/crops", "error", "Bestpreis-Monat muss 0-12 sein") + 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 { + if _, err := a.db.Exec(`UPDATE crops SET name=?,sow_start_month=?,sow_end_month=?,grow_months=?,best_sale_month=?,regrow_enabled=?,regrow_cycles=? WHERE id=?`, name, start, end, grow, bestSaleMonth, regrowEnabled, regrowCycles, id); err != nil { redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht aktualisiert") return } @@ -281,6 +291,72 @@ func (a *App) handleDeleteCrop(w http.ResponseWriter, r *http.Request) { redirectWithMessage(w, r, "/crops", "info", "Feldfrucht gelöscht") } +func (a *App) handleCreateProduct(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")) + bestSaleMonth := mustInt(r.FormValue("best_sale_month"), 0) + if name == "" || bestSaleMonth < 1 || bestSaleMonth > 12 { + redirectWithMessage(w, r, "/crops", "error", "Produktionsgut ungültig") + return + } + if _, err := a.db.Exec(`INSERT INTO products(name,best_sale_month) VALUES (?,?)`, name, bestSaleMonth); err != nil { + redirectWithMessage(w, r, "/crops", "error", "Produktionsgut nicht angelegt") + return + } + redirectWithMessage(w, r, "/crops", "info", "Produktionsgut angelegt") +} + +func (a *App) handleUpdateProduct(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")) + bestSaleMonth := mustInt(r.FormValue("best_sale_month"), 0) + if id <= 0 || name == "" || bestSaleMonth < 1 || bestSaleMonth > 12 { + redirectWithMessage(w, r, "/crops", "error", "Produktionsgut ungültig") + return + } + if _, err := a.db.Exec(`UPDATE products SET name=?,best_sale_month=? WHERE id=?`, name, bestSaleMonth, id); err != nil { + redirectWithMessage(w, r, "/crops", "error", "Produktionsgut nicht aktualisiert") + return + } + redirectWithMessage(w, r, "/crops", "info", "Produktionsgut aktualisiert") +} + +func (a *App) handleDeleteProduct(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", "Produktionsgut-ID ungültig") + return + } + if _, err := a.db.Exec(`DELETE FROM products WHERE id=?`, id); err != nil { + redirectWithMessage(w, r, "/crops", "error", "Produktionsgut nicht gelöscht") + return + } + redirectWithMessage(w, r, "/crops", "info", "Produktionsgut 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) diff --git a/cmd/server/handlers_pages.go b/cmd/server/handlers_pages.go index 016fe22..d1318df 100644 --- a/cmd/server/handlers_pages.go +++ b/cmd/server/handlers_pages.go @@ -31,6 +31,16 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) { http.Error(w, "plans read failed", http.StatusInternalServerError) return } + crops, err := a.listCrops() + if err != nil { + http.Error(w, "crops read failed", http.StatusInternalServerError) + return + } + products, err := a.listProducts() + if err != nil { + http.Error(w, "products read failed", http.StatusInternalServerError) + return + } stepMap, err := a.listCropStepsMap() if err != nil { http.Error(w, "steps read failed", http.StatusInternalServerError) @@ -54,8 +64,8 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) { }, Settings: settings, CurrentMonth: monthNames[settings.CurrentMonth-1], - TodayTasks: buildTasksForDay(plans, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay), - Calendar: buildCalendar(plans, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14), + TodayTasks: buildTasksForDay(plans, crops, products, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay), + Calendar: buildCalendar(plans, crops, products, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14), PlanningCount: len(plans), } a.renderTemplate(w, "templates/dashboard.html", data) @@ -98,6 +108,11 @@ func (a *App) handleCropsPage(w http.ResponseWriter, r *http.Request) { http.Error(w, "steps read failed", http.StatusInternalServerError) return } + products, err := a.listProducts() + if err != nil { + http.Error(w, "products read failed", http.StatusInternalServerError) + return + } data := CropsPage{ BasePage: BasePage{ ActivePath: "/crops", @@ -106,6 +121,7 @@ func (a *App) handleCropsPage(w http.ResponseWriter, r *http.Request) { }, Crops: crops, CropSteps: steps, + Products: products, } a.renderTemplate(w, "templates/crops.html", data) } diff --git a/cmd/server/main.go b/cmd/server/main.go index 5ba471a..1de5076 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -59,6 +59,9 @@ func main() { mux.HandleFunc("/crops/create", app.handleCreateCrop) mux.HandleFunc("/crops/update", app.handleUpdateCrop) mux.HandleFunc("/crops/delete", app.handleDeleteCrop) + mux.HandleFunc("/products/create", app.handleCreateProduct) + mux.HandleFunc("/products/update", app.handleUpdateProduct) + mux.HandleFunc("/products/delete", app.handleDeleteProduct) mux.HandleFunc("/crop-steps/create", app.handleCreateCropStep) mux.HandleFunc("/crop-steps/delete", app.handleDeleteCropStep) mux.HandleFunc("/task-templates/create", app.handleCreateTaskTemplate) diff --git a/cmd/server/types.go b/cmd/server/types.go index ddbab14..9016b1e 100644 --- a/cmd/server/types.go +++ b/cmd/server/types.go @@ -29,10 +29,17 @@ type Crop struct { SowStartMonth int SowEndMonth int GrowMonths int + BestSaleMonth int RegrowEnabled bool RegrowCycles int } +type Product struct { + ID int64 + Name string + BestSaleMonth int +} + type Plan struct { ID int64 FieldID sql.NullInt64 @@ -135,6 +142,7 @@ type CropsPage struct { BasePage Crops []Crop CropSteps []CropStep + Products []Product } type PlanningPage struct { diff --git a/templates/crops.html b/templates/crops.html index 2e3af63..0d29506 100644 --- a/templates/crops.html +++ b/templates/crops.html @@ -25,6 +25,9 @@ +