Add task completion toggle, custom task templates, and umlaut UI updates

This commit is contained in:
Kai
2026-02-16 13:40:02 +01:00
parent a1c1ef31a3
commit 723e9142b2
13 changed files with 548 additions and 118 deletions

View File

@@ -6,32 +6,14 @@ import (
"strings"
)
func buildTasksForDay(plans []Plan, stepMap map[int64][]CropStep, month, day int) []Task {
var tasks []Task
for _, p := range plans {
tasks = append(tasks, expandPlanDayTasks(p, stepMap[p.CropID], month, day)...)
}
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)
applyCompletion(tasks, doneMap)
sortTasks(tasks)
return tasks
}
func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, startMonth, startDay, daysPerMonth, spanMonths int) []CalendarMonth {
tasksByKey := make(map[string][]Task)
for _, p := range plans {
for m := 1; m <= 12; m++ {
for d := 1; d <= daysPerMonth; d++ {
dayTasks := expandPlanDayTasks(p, stepMap[p.CropID], m, d)
if len(dayTasks) > 0 {
key := fmt.Sprintf("%d-%d", m, d)
tasksByKey[key] = append(tasksByKey[key], dayTasks...)
}
}
}
}
for k := range tasksByKey {
sortTasks(tasksByKey[k])
}
func buildCalendar(plans []Plan, 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)
@@ -45,10 +27,12 @@ func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, startMonth, start
if offset == 0 {
fromDay = startDay
}
var days []CalendarDay
for d := fromDay; d <= daysPerMonth; d++ {
key := fmt.Sprintf("%d-%d", month, d)
items := append([]Task(nil), tasksByKey[key]...)
items := collectTasksForDate(plans, stepMap, customTasks, month, d, yearOffset)
applyCompletion(items, doneMap)
sortTasks(items)
days = append(days, CalendarDay{Day: d, Tasks: items})
}
out = append(out, CalendarMonth{
@@ -62,20 +46,57 @@ func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, startMonth, start
return out
}
func expandPlanDayTasks(p Plan, steps []CropStep, month, day int) []Task {
func collectTasksForDate(plans []Plan, 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)...)
}
for _, c := range customTasks {
if c.Month == month && c.Day == day && c.YearOffset == yearOffset {
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: month,
Day: day,
YearOffset: yearOffset,
SortOrder: 40,
})
}
}
return tasks
}
func expandPlanDayTasks(p Plan, steps []CropStep, month, day, yearOffset int) []Task {
field := p.TargetName
if strings.TrimSpace(field) == "" {
field = "Unbekanntes Feld"
}
var out []Task
baseHarvest := p.HarvestMonth
if p.StartMonth == month && p.StartDay == day {
if p.StartMonth == month && p.StartDay == day && yearOffset == 0 {
out = append(out, Task{
Type: "Aussaat",
Field: field,
Message: withOptionalNote(fmt.Sprintf("Aussaat %s", p.CropName), p.Notes),
SortOrder: 10,
UID: fmt.Sprintf("plan:%d:sow:0", p.ID),
Type: "Aussaat",
Field: field,
Message: withOptionalNote(fmt.Sprintf("Aussaat %s", p.CropName), p.Notes),
Month: month,
Day: day,
YearOffset: yearOffset,
SortOrder: 10,
})
}
@@ -90,13 +111,19 @@ func expandPlanDayTasks(p Plan, steps []CropStep, month, day int) []Task {
}
}
harvestMonths = uniqueMonths(harvestMonths)
for _, hm := range harvestMonths {
for idx, hm := range harvestMonths {
if hm == month && p.HarvestDay == day {
uid := fmt.Sprintf("plan:%d:harvest:%d", p.ID, idx)
out = append(out, Task{
Type: "Ernte",
Field: field,
Message: withOptionalNote(fmt.Sprintf("Ernte %s", p.CropName), p.Notes),
SortOrder: 20,
UID: uid,
Type: "Ernte",
Field: field,
Message: withOptionalNote(fmt.Sprintf("Ernte %s", p.CropName), p.Notes),
Month: month,
Day: day,
YearOffset: yearOffset,
SortOrder: 20,
})
}
}
@@ -105,23 +132,31 @@ func expandPlanDayTasks(p Plan, steps []CropStep, month, day int) []Task {
switch s.Phase {
case "pre":
taskMonth := wrapMonth(p.StartMonth - s.MonthOffset)
if taskMonth == month && p.StartDay == day {
if taskMonth == month && p.StartDay == day && yearOffset == 0 {
out = append(out, Task{
Type: "Vorbereitung",
Field: field,
Message: s.Title,
SortOrder: 5,
UID: fmt.Sprintf("plan:%d:pre:%d", p.ID, s.ID),
Type: "Vorbereitung",
Field: field,
Message: s.Title,
Month: month,
Day: day,
YearOffset: yearOffset,
SortOrder: 5,
})
}
case "post":
for _, hm := range harvestMonths {
for idx, hm := range harvestMonths {
taskMonth := wrapMonth(hm + s.MonthOffset)
if taskMonth == month && p.HarvestDay == day {
out = append(out, Task{
Type: "Nachbereitung",
Field: field,
Message: s.Title,
SortOrder: 30,
UID: fmt.Sprintf("plan:%d:post:%d:%d", p.ID, s.ID, idx),
Type: "Nachbereitung",
Field: field,
Message: s.Title,
Month: month,
Day: day,
YearOffset: yearOffset,
SortOrder: 30,
})
}
}
@@ -131,6 +166,13 @@ func expandPlanDayTasks(p Plan, steps []CropStep, month, day int) []Task {
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
@@ -148,6 +190,9 @@ func withOptionalNote(base, note string) string {
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
@@ -169,3 +214,7 @@ func uniqueMonths(values []int) []int {
}
return out
}
func completionKey(uid string, month, day, yearOffset int) string {
return fmt.Sprintf("%s|%d|%d|%d", uid, month, day, yearOffset)
}

View File

@@ -53,6 +53,32 @@ func ensureSchema(db *sql.DB) error {
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_crop_steps_crop FOREIGN KEY(crop_id) REFERENCES crops(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS custom_task_templates(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(140) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS custom_tasks(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_id BIGINT NULL,
title VARCHAR(140) NOT NULL,
month TINYINT NOT NULL,
day TINYINT NOT NULL,
year_offset SMALLINT NOT NULL DEFAULT 0,
target_name VARCHAR(120) NOT NULL DEFAULT '',
notes VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_custom_tasks_template FOREIGN KEY(template_id) REFERENCES custom_task_templates(id) ON DELETE SET NULL
)`,
`CREATE TABLE IF NOT EXISTS task_completions(
task_uid VARCHAR(190) NOT NULL,
month TINYINT NOT NULL,
day TINYINT NOT NULL,
year_offset SMALLINT NOT NULL DEFAULT 0,
done TINYINT(1) NOT NULL DEFAULT 1,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(task_uid,month,day,year_offset)
)`,
`CREATE TABLE IF NOT EXISTS plans(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
@@ -199,6 +225,67 @@ func (a *App) listCropStepsMap() (map[int64][]CropStep, error) {
return m, nil
}
func (a *App) listCustomTaskTemplates() ([]CustomTaskTemplate, error) {
rows, err := a.db.Query(`SELECT id,title FROM custom_task_templates ORDER BY title`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []CustomTaskTemplate
for rows.Next() {
var t CustomTaskTemplate
if err := rows.Scan(&t.ID, &t.Title); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (a *App) listCustomTasks() ([]CustomTask, error) {
rows, err := a.db.Query(`
SELECT id,template_id,title,month,day,year_offset,COALESCE(target_name,''),COALESCE(notes,'')
FROM custom_tasks
ORDER BY year_offset, month, day, title`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []CustomTask
for rows.Next() {
var t CustomTask
if err := rows.Scan(&t.ID, &t.TemplateID, &t.Title, &t.Month, &t.Day, &t.YearOffset, &t.TargetName, &t.Notes); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (a *App) listTaskCompletionsMap() (map[string]bool, error) {
rows, err := a.db.Query(`SELECT task_uid,month,day,year_offset,done FROM task_completions WHERE done=1`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]bool)
for rows.Next() {
var uid string
var month, day, yearOffset int
var done bool
if err := rows.Scan(&uid, &month, &day, &yearOffset, &done); err != nil {
return nil, err
}
if done {
out[completionKey(uid, month, day, yearOffset)] = true
}
}
return out, rows.Err()
}
func seedCrops(db *sql.DB) error {
items := []Crop{
{Name: "Weizen", SowStartMonth: 9, SowEndMonth: 11, GrowMonths: 10},

View File

@@ -1,4 +1,4 @@
package main
package main
import (
"errors"
@@ -116,7 +116,8 @@ func validateCropInput(name string, start, end, grow, regrowCycles int) error {
return errors.New("Wachstumsdauer muss 1-24 sein")
}
if regrowCycles < 0 || regrowCycles > 120 {
return errors.New("Regrow-Zyklen muessen 0-120 sein")
return errors.New("Regrow-Zyklen müssen 0-120 sein")
}
return nil
}

View File

@@ -1,4 +1,4 @@
package main
package main
import (
"database/sql"
@@ -16,7 +16,7 @@ func (a *App) handleSetDaysPerMonth(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/general", "error", "Form ungueltig")
redirectWithMessage(w, r, "/general", "error", "Form ungültig")
return
}
days := mustInt(r.FormValue("days_per_month"), 2)
@@ -37,7 +37,7 @@ func (a *App) handleSetCurrentTime(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/", "error", "Form ungueltig")
redirectWithMessage(w, r, "/", "error", "Form ungültig")
return
}
settings, err := a.getSettings()
@@ -48,7 +48,7 @@ func (a *App) handleSetCurrentTime(w http.ResponseWriter, r *http.Request) {
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 ungueltig")
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 {
@@ -64,14 +64,14 @@ func (a *App) handleCreateField(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
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 ungueltig")
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 {
@@ -87,14 +87,14 @@ func (a *App) handleUpdateField(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
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 ungueltig")
redirectWithMessage(w, r, "/fields", "error", "Feld-ID ungültig")
return
}
if owned {
@@ -117,19 +117,19 @@ func (a *App) handleDeleteField(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
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 ungueltig")
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 geloescht")
redirectWithMessage(w, r, "/fields", "error", "Feld nicht gelöscht")
return
}
redirectWithMessage(w, r, "/fields", "info", "Feld geloescht")
redirectWithMessage(w, r, "/fields", "info", "Feld gelöscht")
}
func (a *App) handleCreateFieldGroup(w http.ResponseWriter, r *http.Request) {
@@ -138,12 +138,12 @@ func (a *App) handleCreateFieldGroup(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
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 auswaehlen")
redirectWithMessage(w, r, "/fields", "error", "Mindestens ein Feld auswählen")
return
}
fields, err := a.getFieldsByIDs(ids)
@@ -182,19 +182,19 @@ func (a *App) handleDeleteFieldGroup(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
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 ungueltig")
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 aufgeloest")
redirectWithMessage(w, r, "/fields", "error", "Gruppe nicht aufgelöst")
return
}
redirectWithMessage(w, r, "/fields", "info", "Gruppe aufgeloest")
redirectWithMessage(w, r, "/fields", "info", "Gruppe aufgelöst")
}
func (a *App) handleCreateCrop(w http.ResponseWriter, r *http.Request) {
@@ -203,7 +203,7 @@ func (a *App) handleCreateCrop(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
return
}
name := strings.TrimSpace(r.FormValue("name"))
@@ -232,7 +232,7 @@ func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
return
}
id := mustInt64(r.FormValue("id"), 0)
@@ -243,7 +243,7 @@ func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) {
regrowEnabled := r.FormValue("regrow_enabled") == "on"
regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0)
if id <= 0 {
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungueltig")
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungültig")
return
}
if err := validateCropInput(name, start, end, grow, regrowCycles); err != nil {
@@ -266,19 +266,19 @@ func (a *App) handleDeleteCrop(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
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 ungueltig")
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 geloescht")
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht gelöscht")
return
}
redirectWithMessage(w, r, "/crops", "info", "Feldfrucht geloescht")
redirectWithMessage(w, r, "/crops", "info", "Feldfrucht gelöscht")
}
func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
@@ -287,7 +287,7 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/planning", "error", "Form ungueltig")
redirectWithMessage(w, r, "/planning", "error", "Form ungültig")
return
}
targetRef := strings.TrimSpace(r.FormValue("target_ref"))
@@ -296,7 +296,7 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
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 ungueltig")
redirectWithMessage(w, r, "/planning", "error", "Planungsziel oder Feldfrucht ungültig")
return
}
@@ -306,7 +306,7 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
return
}
if startMonth < 1 || startMonth > 12 || startDay < 1 || startDay > settings.DaysPerMonth {
redirectWithMessage(w, r, "/planning", "error", "Startdatum ungueltig")
redirectWithMessage(w, r, "/planning", "error", "Startdatum ungültig")
return
}
@@ -318,7 +318,7 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
return
}
if !monthInWindow(startMonth, c.SowStartMonth, c.SowEndMonth) {
redirectWithMessage(w, r, "/planning", "error", "Aussaat ausserhalb des Zeitfensters")
redirectWithMessage(w, r, "/planning", "error", "Aussaat außerhalb des Zeitfensters")
return
}
@@ -347,19 +347,148 @@ func (a *App) handleDeletePlan(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/planning", "error", "Form ungueltig")
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 ungueltig")
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 geloescht")
redirectWithMessage(w, r, "/planning", "error", "Plan nicht gelöscht")
return
}
redirectWithMessage(w, r, "/planning", "info", "Plan geloescht")
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) {
@@ -368,7 +497,7 @@ func (a *App) handleCreateCropStep(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
return
}
cropID := mustInt64(r.FormValue("crop_id"), 0)
@@ -376,7 +505,7 @@ func (a *App) handleCreateCropStep(w http.ResponseWriter, r *http.Request) {
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 ungueltig")
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 {
@@ -392,26 +521,26 @@ func (a *App) handleDeleteCropStep(w http.ResponseWriter, r *http.Request) {
return
}
if err := r.ParseForm(); err != nil {
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
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 ungueltig")
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 geloescht")
redirectWithMessage(w, r, "/crops", "error", "Schritt nicht gelöscht")
return
}
redirectWithMessage(w, r, "/crops", "info", "Schritt geloescht")
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 ungueltig")
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)
@@ -426,7 +555,7 @@ func (a *App) resolvePlanningTarget(ref string) (sql.NullInt64, string, error) {
if strings.HasPrefix(ref, "g:") {
key := strings.TrimPrefix(ref, "g:")
if key == "" {
return sql.NullInt64{}, "", errors.New("Gruppen-Ziel ungueltig")
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 {
@@ -451,5 +580,6 @@ func (a *App) resolvePlanningTarget(ref string) (sql.NullInt64, string, error) {
}
return sql.NullInt64{}, groupName, nil
}
return sql.NullInt64{}, "", errors.New("Planungsziel ungueltig")
return sql.NullInt64{}, "", errors.New("Planungsziel ungültig")
}

View File

@@ -36,6 +36,16 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "steps read failed", http.StatusInternalServerError)
return
}
customTasks, err := a.listCustomTasks()
if err != nil {
http.Error(w, "custom tasks read failed", http.StatusInternalServerError)
return
}
doneMap, err := a.listTaskCompletionsMap()
if err != nil {
http.Error(w, "completions read failed", http.StatusInternalServerError)
return
}
data := DashboardPage{
BasePage: BasePage{
ActivePath: "/",
@@ -44,8 +54,8 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
},
Settings: settings,
CurrentMonth: monthNames[settings.CurrentMonth-1],
TodayTasks: buildTasksForDay(plans, stepMap, settings.CurrentMonth, settings.CurrentDay),
Calendar: buildCalendar(plans, stepMap, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14),
TodayTasks: buildTasksForDay(plans, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay),
Calendar: buildCalendar(plans, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14),
PlanningCount: len(plans),
}
a.renderTemplate(w, "templates/dashboard.html", data)
@@ -125,6 +135,16 @@ func (a *App) handlePlanningPage(w http.ResponseWriter, r *http.Request) {
http.Error(w, "plans read failed", http.StatusInternalServerError)
return
}
taskTemplates, err := a.listCustomTaskTemplates()
if err != nil {
http.Error(w, "templates read failed", http.StatusInternalServerError)
return
}
customTasks, err := a.listCustomTasks()
if err != nil {
http.Error(w, "custom tasks read failed", http.StatusInternalServerError)
return
}
data := PlanningPage{
BasePage: BasePage{
ActivePath: "/planning",
@@ -136,6 +156,8 @@ func (a *App) handlePlanningPage(w http.ResponseWriter, r *http.Request) {
Crops: crops,
Plans: plans,
PlanningTargets: buildPlanningTargets(fields),
TaskTemplates: taskTemplates,
CustomTasks: customTasks,
}
a.renderTemplate(w, "templates/planning.html", data)
}

View File

@@ -13,7 +13,7 @@ import (
)
var monthNames = []string{
"Januar", "Februar", "Maerz", "April", "Mai", "Juni",
"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember",
}
@@ -61,9 +61,13 @@ func main() {
mux.HandleFunc("/crops/delete", app.handleDeleteCrop)
mux.HandleFunc("/crop-steps/create", app.handleCreateCropStep)
mux.HandleFunc("/crop-steps/delete", app.handleDeleteCropStep)
mux.HandleFunc("/task-templates/create", app.handleCreateTaskTemplate)
mux.HandleFunc("/custom-tasks/create", app.handleCreateCustomTask)
mux.HandleFunc("/custom-tasks/delete", app.handleDeleteCustomTask)
mux.HandleFunc("/plans/create", app.handleCreatePlan)
mux.HandleFunc("/plans/delete", app.handleDeletePlan)
mux.HandleFunc("/tasks/toggle", app.handleToggleTaskDone)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
srv := &http.Server{

View File

@@ -59,10 +59,31 @@ type CropStep struct {
Title string
}
type CustomTaskTemplate struct {
ID int64
Title string
}
type CustomTask struct {
ID int64
TemplateID sql.NullInt64
Title string
Month int
Day int
YearOffset int
TargetName string
Notes string
}
type Task struct {
UID string
Type string
Field string
Message string
Month int
Day int
YearOffset int
Completed bool
SortOrder int
}
@@ -123,6 +144,8 @@ type PlanningPage struct {
Crops []Crop
Plans []Plan
PlanningTargets []PlanningTarget
TaskTemplates []CustomTaskTemplate
CustomTasks []CustomTask
}
type GeneralPage struct {