Add task completion toggle, custom task templates, and umlaut UI updates
This commit is contained in:
@@ -6,32 +6,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildTasksForDay(plans []Plan, stepMap map[int64][]CropStep, month, day int) []Task {
|
func buildTasksForDay(plans []Plan, stepMap map[int64][]CropStep, customTasks []CustomTask, doneMap map[string]bool, month, day int) []Task {
|
||||||
var tasks []Task
|
tasks := collectTasksForDate(plans, stepMap, customTasks, month, day, 0)
|
||||||
for _, p := range plans {
|
applyCompletion(tasks, doneMap)
|
||||||
tasks = append(tasks, expandPlanDayTasks(p, stepMap[p.CropID], month, day)...)
|
|
||||||
}
|
|
||||||
sortTasks(tasks)
|
sortTasks(tasks)
|
||||||
return tasks
|
return tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, startMonth, startDay, daysPerMonth, spanMonths int) []CalendarMonth {
|
func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, customTasks []CustomTask, doneMap map[string]bool, 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])
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []CalendarMonth
|
var out []CalendarMonth
|
||||||
for offset := 0; offset < spanMonths; offset++ {
|
for offset := 0; offset < spanMonths; offset++ {
|
||||||
month := wrapMonth(startMonth + offset)
|
month := wrapMonth(startMonth + offset)
|
||||||
@@ -45,10 +27,12 @@ func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, startMonth, start
|
|||||||
if offset == 0 {
|
if offset == 0 {
|
||||||
fromDay = startDay
|
fromDay = startDay
|
||||||
}
|
}
|
||||||
|
|
||||||
var days []CalendarDay
|
var days []CalendarDay
|
||||||
for d := fromDay; d <= daysPerMonth; d++ {
|
for d := fromDay; d <= daysPerMonth; d++ {
|
||||||
key := fmt.Sprintf("%d-%d", month, d)
|
items := collectTasksForDate(plans, stepMap, customTasks, month, d, yearOffset)
|
||||||
items := append([]Task(nil), tasksByKey[key]...)
|
applyCompletion(items, doneMap)
|
||||||
|
sortTasks(items)
|
||||||
days = append(days, CalendarDay{Day: d, Tasks: items})
|
days = append(days, CalendarDay{Day: d, Tasks: items})
|
||||||
}
|
}
|
||||||
out = append(out, CalendarMonth{
|
out = append(out, CalendarMonth{
|
||||||
@@ -62,20 +46,57 @@ func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, startMonth, start
|
|||||||
return out
|
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
|
field := p.TargetName
|
||||||
if strings.TrimSpace(field) == "" {
|
if strings.TrimSpace(field) == "" {
|
||||||
field = "Unbekanntes Feld"
|
field = "Unbekanntes Feld"
|
||||||
}
|
}
|
||||||
|
|
||||||
var out []Task
|
var out []Task
|
||||||
baseHarvest := p.HarvestMonth
|
baseHarvest := p.HarvestMonth
|
||||||
|
|
||||||
if p.StartMonth == month && p.StartDay == day {
|
if p.StartMonth == month && p.StartDay == day && yearOffset == 0 {
|
||||||
out = append(out, Task{
|
out = append(out, Task{
|
||||||
Type: "Aussaat",
|
UID: fmt.Sprintf("plan:%d:sow:0", p.ID),
|
||||||
Field: field,
|
Type: "Aussaat",
|
||||||
Message: withOptionalNote(fmt.Sprintf("Aussaat %s", p.CropName), p.Notes),
|
Field: field,
|
||||||
SortOrder: 10,
|
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)
|
harvestMonths = uniqueMonths(harvestMonths)
|
||||||
for _, hm := range harvestMonths {
|
|
||||||
|
for idx, hm := range harvestMonths {
|
||||||
if hm == month && p.HarvestDay == day {
|
if hm == month && p.HarvestDay == day {
|
||||||
|
uid := fmt.Sprintf("plan:%d:harvest:%d", p.ID, idx)
|
||||||
out = append(out, Task{
|
out = append(out, Task{
|
||||||
Type: "Ernte",
|
UID: uid,
|
||||||
Field: field,
|
Type: "Ernte",
|
||||||
Message: withOptionalNote(fmt.Sprintf("Ernte %s", p.CropName), p.Notes),
|
Field: field,
|
||||||
SortOrder: 20,
|
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 {
|
switch s.Phase {
|
||||||
case "pre":
|
case "pre":
|
||||||
taskMonth := wrapMonth(p.StartMonth - s.MonthOffset)
|
taskMonth := wrapMonth(p.StartMonth - s.MonthOffset)
|
||||||
if taskMonth == month && p.StartDay == day {
|
if taskMonth == month && p.StartDay == day && yearOffset == 0 {
|
||||||
out = append(out, Task{
|
out = append(out, Task{
|
||||||
Type: "Vorbereitung",
|
UID: fmt.Sprintf("plan:%d:pre:%d", p.ID, s.ID),
|
||||||
Field: field,
|
Type: "Vorbereitung",
|
||||||
Message: s.Title,
|
Field: field,
|
||||||
SortOrder: 5,
|
Message: s.Title,
|
||||||
|
Month: month,
|
||||||
|
Day: day,
|
||||||
|
YearOffset: yearOffset,
|
||||||
|
SortOrder: 5,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "post":
|
case "post":
|
||||||
for _, hm := range harvestMonths {
|
for idx, hm := range harvestMonths {
|
||||||
taskMonth := wrapMonth(hm + s.MonthOffset)
|
taskMonth := wrapMonth(hm + s.MonthOffset)
|
||||||
if taskMonth == month && p.HarvestDay == day {
|
if taskMonth == month && p.HarvestDay == day {
|
||||||
out = append(out, Task{
|
out = append(out, Task{
|
||||||
Type: "Nachbereitung",
|
UID: fmt.Sprintf("plan:%d:post:%d:%d", p.ID, s.ID, idx),
|
||||||
Field: field,
|
Type: "Nachbereitung",
|
||||||
Message: s.Title,
|
Field: field,
|
||||||
SortOrder: 30,
|
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
|
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 {
|
func (p Plan) HarvestDistance() int {
|
||||||
if p.GrowMonths <= 0 {
|
if p.GrowMonths <= 0 {
|
||||||
return 1
|
return 1
|
||||||
@@ -148,6 +190,9 @@ func withOptionalNote(base, note string) string {
|
|||||||
|
|
||||||
func sortTasks(tasks []Task) {
|
func sortTasks(tasks []Task) {
|
||||||
sort.Slice(tasks, func(i, j int) bool {
|
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].SortOrder == tasks[j].SortOrder {
|
||||||
if tasks[i].Field == tasks[j].Field {
|
if tasks[i].Field == tasks[j].Field {
|
||||||
return tasks[i].Message < tasks[j].Message
|
return tasks[i].Message < tasks[j].Message
|
||||||
@@ -169,3 +214,7 @@ func uniqueMonths(values []int) []int {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func completionKey(uid string, month, day, yearOffset int) string {
|
||||||
|
return fmt.Sprintf("%s|%d|%d|%d", uid, month, day, yearOffset)
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,6 +53,32 @@ func ensureSchema(db *sql.DB) error {
|
|||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
CONSTRAINT fk_crop_steps_crop FOREIGN KEY(crop_id) REFERENCES crops(id) ON DELETE CASCADE
|
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(
|
`CREATE TABLE IF NOT EXISTS plans(
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -199,6 +225,67 @@ func (a *App) listCropStepsMap() (map[int64][]CropStep, error) {
|
|||||||
return m, nil
|
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 {
|
func seedCrops(db *sql.DB) error {
|
||||||
items := []Crop{
|
items := []Crop{
|
||||||
{Name: "Weizen", SowStartMonth: 9, SowEndMonth: 11, GrowMonths: 10},
|
{Name: "Weizen", SowStartMonth: 9, SowEndMonth: 11, GrowMonths: 10},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -116,7 +116,8 @@ func validateCropInput(name string, start, end, grow, regrowCycles int) error {
|
|||||||
return errors.New("Wachstumsdauer muss 1-24 sein")
|
return errors.New("Wachstumsdauer muss 1-24 sein")
|
||||||
}
|
}
|
||||||
if regrowCycles < 0 || regrowCycles > 120 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@@ -16,7 +16,7 @@ func (a *App) handleSetDaysPerMonth(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/general", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/general", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
days := mustInt(r.FormValue("days_per_month"), 2)
|
days := mustInt(r.FormValue("days_per_month"), 2)
|
||||||
@@ -37,7 +37,7 @@ func (a *App) handleSetCurrentTime(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
settings, err := a.getSettings()
|
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)
|
month := mustInt(r.FormValue("current_month"), 1)
|
||||||
day := mustInt(r.FormValue("current_day"), 1)
|
day := mustInt(r.FormValue("current_day"), 1)
|
||||||
if month < 1 || month > 12 || day < 1 || day > settings.DaysPerMonth {
|
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
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`UPDATE settings SET current_month=?, current_day=? WHERE id=1`, month, day); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
number := mustInt(r.FormValue("number"), 0)
|
number := mustInt(r.FormValue("number"), 0)
|
||||||
name := strings.TrimSpace(r.FormValue("name"))
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
owned := r.FormValue("owned") == "on"
|
owned := r.FormValue("owned") == "on"
|
||||||
if number <= 0 {
|
if number <= 0 {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Feldnummer ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Feldnummer ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`INSERT INTO fields(number,name,owned) VALUES (?,?,?)`, number, name, owned); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id := mustInt64(r.FormValue("id"), 0)
|
id := mustInt64(r.FormValue("id"), 0)
|
||||||
name := strings.TrimSpace(r.FormValue("name"))
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
owned := r.FormValue("owned") == "on"
|
owned := r.FormValue("owned") == "on"
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Feld-ID ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Feld-ID ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if owned {
|
if owned {
|
||||||
@@ -117,19 +117,19 @@ func (a *App) handleDeleteField(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id := mustInt64(r.FormValue("id"), 0)
|
id := mustInt64(r.FormValue("id"), 0)
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Feld-ID ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Feld-ID ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`DELETE FROM fields WHERE id=?`, id); err != nil {
|
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
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ids, err := parseInt64List(r.Form["field_ids"])
|
ids, err := parseInt64List(r.Form["field_ids"])
|
||||||
if err != nil || len(ids) == 0 {
|
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
|
return
|
||||||
}
|
}
|
||||||
fields, err := a.getFieldsByIDs(ids)
|
fields, err := a.getFieldsByIDs(ids)
|
||||||
@@ -182,19 +182,19 @@ func (a *App) handleDeleteFieldGroup(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
key := strings.TrimSpace(r.FormValue("group_key"))
|
key := strings.TrimSpace(r.FormValue("group_key"))
|
||||||
if key == "" {
|
if key == "" {
|
||||||
redirectWithMessage(w, r, "/fields", "error", "Gruppen-ID ungueltig")
|
redirectWithMessage(w, r, "/fields", "error", "Gruppen-ID ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`UPDATE fields SET group_key='',group_name='' WHERE group_key=?`, key); err != nil {
|
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
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := strings.TrimSpace(r.FormValue("name"))
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
@@ -232,7 +232,7 @@ func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id := mustInt64(r.FormValue("id"), 0)
|
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"
|
regrowEnabled := r.FormValue("regrow_enabled") == "on"
|
||||||
regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0)
|
regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0)
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := validateCropInput(name, start, end, grow, regrowCycles); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id := mustInt64(r.FormValue("id"), 0)
|
id := mustInt64(r.FormValue("id"), 0)
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`DELETE FROM crops WHERE id=?`, id); err != nil {
|
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
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/planning", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/planning", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetRef := strings.TrimSpace(r.FormValue("target_ref"))
|
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)
|
startDay := mustInt(r.FormValue("start_day"), 1)
|
||||||
notes := strings.TrimSpace(r.FormValue("notes"))
|
notes := strings.TrimSpace(r.FormValue("notes"))
|
||||||
if targetRef == "" || cropID <= 0 {
|
if targetRef == "" || cropID <= 0 {
|
||||||
redirectWithMessage(w, r, "/planning", "error", "Planungsziel oder Feldfrucht ungueltig")
|
redirectWithMessage(w, r, "/planning", "error", "Planungsziel oder Feldfrucht ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if startMonth < 1 || startMonth > 12 || startDay < 1 || startDay > settings.DaysPerMonth {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !monthInWindow(startMonth, c.SowStartMonth, c.SowEndMonth) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,19 +347,148 @@ func (a *App) handleDeletePlan(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/planning", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/planning", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id := mustInt64(r.FormValue("id"), 0)
|
id := mustInt64(r.FormValue("id"), 0)
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
redirectWithMessage(w, r, "/planning", "error", "Plan-ID ungueltig")
|
redirectWithMessage(w, r, "/planning", "error", "Plan-ID ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`DELETE FROM plans WHERE id=?`, id); err != nil {
|
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
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cropID := mustInt64(r.FormValue("crop_id"), 0)
|
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)
|
offset := mustInt(r.FormValue("month_offset"), -1)
|
||||||
title := strings.TrimSpace(r.FormValue("title"))
|
title := strings.TrimSpace(r.FormValue("title"))
|
||||||
if cropID <= 0 || (phase != "pre" && phase != "post") || offset < 0 || offset > 24 || 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
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`INSERT INTO crop_steps(crop_id,phase,month_offset,title) VALUES (?,?,?,?)`, cropID, phase, offset, title); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Form ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id := mustInt64(r.FormValue("id"), 0)
|
id := mustInt64(r.FormValue("id"), 0)
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
redirectWithMessage(w, r, "/crops", "error", "Schritt-ID ungueltig")
|
redirectWithMessage(w, r, "/crops", "error", "Schritt-ID ungültig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.db.Exec(`DELETE FROM crop_steps WHERE id=?`, id); err != nil {
|
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
|
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) {
|
func (a *App) resolvePlanningTarget(ref string) (sql.NullInt64, string, error) {
|
||||||
if strings.HasPrefix(ref, "f:") {
|
if strings.HasPrefix(ref, "f:") {
|
||||||
id := mustInt64(strings.TrimPrefix(ref, "f:"), 0)
|
id := mustInt64(strings.TrimPrefix(ref, "f:"), 0)
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
return sql.NullInt64{}, "", errors.New("Feld-Ziel ungueltig")
|
return sql.NullInt64{}, "", errors.New("Feld-Ziel ungültig")
|
||||||
}
|
}
|
||||||
var f Field
|
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)
|
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:") {
|
if strings.HasPrefix(ref, "g:") {
|
||||||
key := strings.TrimPrefix(ref, "g:")
|
key := strings.TrimPrefix(ref, "g:")
|
||||||
if key == "" {
|
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)
|
rows, err := a.db.Query(`SELECT number FROM fields WHERE group_key=? AND owned=1 ORDER BY number`, key)
|
||||||
if err != nil {
|
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{}, groupName, nil
|
||||||
}
|
}
|
||||||
return sql.NullInt64{}, "", errors.New("Planungsziel ungueltig")
|
return sql.NullInt64{}, "", errors.New("Planungsziel ungültig")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "steps read failed", http.StatusInternalServerError)
|
http.Error(w, "steps read failed", http.StatusInternalServerError)
|
||||||
return
|
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{
|
data := DashboardPage{
|
||||||
BasePage: BasePage{
|
BasePage: BasePage{
|
||||||
ActivePath: "/",
|
ActivePath: "/",
|
||||||
@@ -44,8 +54,8 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
},
|
},
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
CurrentMonth: monthNames[settings.CurrentMonth-1],
|
CurrentMonth: monthNames[settings.CurrentMonth-1],
|
||||||
TodayTasks: buildTasksForDay(plans, stepMap, settings.CurrentMonth, settings.CurrentDay),
|
TodayTasks: buildTasksForDay(plans, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay),
|
||||||
Calendar: buildCalendar(plans, stepMap, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14),
|
Calendar: buildCalendar(plans, stepMap, customTasks, doneMap, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14),
|
||||||
PlanningCount: len(plans),
|
PlanningCount: len(plans),
|
||||||
}
|
}
|
||||||
a.renderTemplate(w, "templates/dashboard.html", data)
|
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)
|
http.Error(w, "plans read failed", http.StatusInternalServerError)
|
||||||
return
|
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{
|
data := PlanningPage{
|
||||||
BasePage: BasePage{
|
BasePage: BasePage{
|
||||||
ActivePath: "/planning",
|
ActivePath: "/planning",
|
||||||
@@ -136,6 +156,8 @@ func (a *App) handlePlanningPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
Crops: crops,
|
Crops: crops,
|
||||||
Plans: plans,
|
Plans: plans,
|
||||||
PlanningTargets: buildPlanningTargets(fields),
|
PlanningTargets: buildPlanningTargets(fields),
|
||||||
|
TaskTemplates: taskTemplates,
|
||||||
|
CustomTasks: customTasks,
|
||||||
}
|
}
|
||||||
a.renderTemplate(w, "templates/planning.html", data)
|
a.renderTemplate(w, "templates/planning.html", data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var monthNames = []string{
|
var monthNames = []string{
|
||||||
"Januar", "Februar", "Maerz", "April", "Mai", "Juni",
|
"Januar", "Februar", "März", "April", "Mai", "Juni",
|
||||||
"Juli", "August", "September", "Oktober", "November", "Dezember",
|
"Juli", "August", "September", "Oktober", "November", "Dezember",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +61,13 @@ func main() {
|
|||||||
mux.HandleFunc("/crops/delete", app.handleDeleteCrop)
|
mux.HandleFunc("/crops/delete", app.handleDeleteCrop)
|
||||||
mux.HandleFunc("/crop-steps/create", app.handleCreateCropStep)
|
mux.HandleFunc("/crop-steps/create", app.handleCreateCropStep)
|
||||||
mux.HandleFunc("/crop-steps/delete", app.handleDeleteCropStep)
|
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/create", app.handleCreatePlan)
|
||||||
mux.HandleFunc("/plans/delete", app.handleDeletePlan)
|
mux.HandleFunc("/plans/delete", app.handleDeletePlan)
|
||||||
|
mux.HandleFunc("/tasks/toggle", app.handleToggleTaskDone)
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
|
|||||||
@@ -59,10 +59,31 @@ type CropStep struct {
|
|||||||
Title string
|
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 {
|
type Task struct {
|
||||||
|
UID string
|
||||||
Type string
|
Type string
|
||||||
Field string
|
Field string
|
||||||
Message string
|
Message string
|
||||||
|
Month int
|
||||||
|
Day int
|
||||||
|
YearOffset int
|
||||||
|
Completed bool
|
||||||
SortOrder int
|
SortOrder int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +144,8 @@ type PlanningPage struct {
|
|||||||
Crops []Crop
|
Crops []Crop
|
||||||
Plans []Plan
|
Plans []Plan
|
||||||
PlanningTargets []PlanningTarget
|
PlanningTargets []PlanningTarget
|
||||||
|
TaskTemplates []CustomTaskTemplate
|
||||||
|
CustomTasks []CustomTask
|
||||||
}
|
}
|
||||||
|
|
||||||
type GeneralPage struct {
|
type GeneralPage struct {
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ button:hover { background: linear-gradient(180deg, #7cbc19, #5e8a12); }
|
|||||||
.btn-small { padding: .35rem .58rem; font-size: .9rem; }
|
.btn-small { padding: .35rem .58rem; font-size: .9rem; }
|
||||||
.danger { background: linear-gradient(180deg, #cc4747, #9a2e2e); }
|
.danger { background: linear-gradient(180deg, #cc4747, #9a2e2e); }
|
||||||
.danger:hover { background: linear-gradient(180deg, #b43d3d, #842626); }
|
.danger:hover { background: linear-gradient(180deg, #b43d3d, #842626); }
|
||||||
|
.secondary { background: linear-gradient(180deg, #7b8a97, #596673); }
|
||||||
|
.secondary:hover { background: linear-gradient(180deg, #6d7b87, #4c5863); }
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -197,6 +199,13 @@ button:hover { background: linear-gradient(180deg, #7cbc19, #5e8a12); }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tasks li { margin: .3rem 0; }
|
.tasks li { margin: .3rem 0; }
|
||||||
|
.tasks li,
|
||||||
|
.task-sublist li {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.task-sublist {
|
.task-sublist {
|
||||||
margin: .3rem 0 0;
|
margin: .3rem 0 0;
|
||||||
@@ -204,6 +213,15 @@ button:hover { background: linear-gradient(180deg, #7cbc19, #5e8a12); }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.muted { color: var(--muted); }
|
.muted { color: var(--muted); }
|
||||||
|
.done-task {
|
||||||
|
opacity: .75;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.inline-form {
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-grid {
|
.calendar-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>FarmCal - Feldfruechte</title>
|
<title>FarmCal - Feldfrüchte</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/farmcal-icon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/farmcal-icon.svg">
|
||||||
<link rel="stylesheet" href="/static/styles.css">
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="top"><h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1><p>Feldfruechte verwalten</p></header>
|
<header class="top"><h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1><p>Feldfrüchte verwalten</p></header>
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||||
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfruechte</a>
|
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfrüchte</a>
|
||||||
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
||||||
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<label>Aussaat von Monat<input type="number" name="sow_start_month" min="1" max="12" required></label>
|
<label>Aussaat von Monat<input type="number" name="sow_start_month" min="1" max="12" required></label>
|
||||||
<label>Aussaat bis Monat<input type="number" name="sow_end_month" min="1" max="12" required></label>
|
<label>Aussaat bis Monat<input type="number" name="sow_end_month" min="1" max="12" required></label>
|
||||||
<label>Wachstum (Monate)<input type="number" name="grow_months" min="1" max="24" required></label>
|
<label>Wachstum (Monate)<input type="number" name="grow_months" min="1" max="24" required></label>
|
||||||
<label class="check"><input type="checkbox" name="regrow_enabled"> Waechst nach Ernte erneut</label>
|
<label class="check"><input type="checkbox" name="regrow_enabled"> Wächst nach Ernte erneut</label>
|
||||||
<label>Zusatz-Ernten (0 = unendlich)
|
<label>Zusatz-Ernten (0 = unendlich)
|
||||||
<input type="number" name="regrow_cycles" min="0" max="120" value="0">
|
<input type="number" name="regrow_cycles" min="0" max="120" value="0">
|
||||||
</label>
|
</label>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card full-width">
|
<section class="card full-width">
|
||||||
<h2>Bestehende Feldfruechte</h2>
|
<h2>Bestehende Feldfrüchte</h2>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Name</th><th>Aussaat</th><th>Wachstum</th><th>Aktionen</th></tr></thead>
|
<thead><tr><th>Name</th><th>Aussaat</th><th>Wachstum</th><th>Aktionen</th></tr></thead>
|
||||||
@@ -56,13 +56,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button type="submit" class="btn-small">Speichern</button>
|
<button type="submit" class="btn-small">Speichern</button>
|
||||||
<button type="submit" formaction="/crops/delete" formnovalidate class="btn-small danger" onclick="return confirm('Feldfrucht wirklich loeschen?')">Loeschen</button>
|
<button type="submit" formaction="/crops/delete" formnovalidate class="btn-small danger" onclick="return confirm('Feldfrucht wirklich löschen?')">Löschen</button>
|
||||||
</td>
|
</td>
|
||||||
</form>
|
</form>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="4">Keine Feldfruechte vorhanden.</td></tr>
|
<tr><td colspan="4">Keine Feldfrüchte vorhanden.</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<form method="post" action="/crop-steps/create" class="grid">
|
<form method="post" action="/crop-steps/create" class="grid">
|
||||||
<label>Feldfrucht
|
<label>Feldfrucht
|
||||||
<select name="crop_id" required>
|
<select name="crop_id" required>
|
||||||
<option value="">Bitte waehlen</option>
|
<option value="">Bitte wählen</option>
|
||||||
{{range .Crops}}
|
{{range .Crops}}
|
||||||
<option value="{{.ID}}">{{.Name}}</option>
|
<option value="{{.ID}}">{{.Name}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<form method="post" action="/crop-steps/delete">
|
<form method="post" action="/crop-steps/delete">
|
||||||
<input type="hidden" name="id" value="{{.ID}}">
|
<input type="hidden" name="id" value="{{.ID}}">
|
||||||
<button type="submit" class="btn-small danger" onclick="return confirm('Schritt loeschen?')">Loeschen</button>
|
<button type="submit" class="btn-small danger" onclick="return confirm('Schritt löschen?')">Löschen</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||||
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfruechte</a>
|
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfrüchte</a>
|
||||||
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
||||||
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Aktuelle Ingame-Zeit</h2>
|
<h2>Aktuelle Ingame-Zeit</h2>
|
||||||
<p><strong>{{.CurrentMonth}} Tag {{.Settings.CurrentDay}}</strong> bei {{.Settings.DaysPerMonth}} Tagen pro Monat.</p>
|
<p><strong>{{.CurrentMonth}} Tag {{.Settings.CurrentDay}}</strong> bei {{.Settings.DaysPerMonth}} Tagen pro Monat.</p>
|
||||||
<p>{{.PlanningCount}} Plan-Eintraege insgesamt.</p>
|
<p>{{.PlanningCount}} Plan-Einträge insgesamt.</p>
|
||||||
<form method="post" action="/settings/time" class="grid">
|
<form method="post" action="/settings/time" class="grid">
|
||||||
<label>Monat
|
<label>Monat
|
||||||
<select name="current_month">
|
<select name="current_month">
|
||||||
<option value="1" {{if eq .Settings.CurrentMonth 1}}selected{{end}}>Januar</option>
|
<option value="1" {{if eq .Settings.CurrentMonth 1}}selected{{end}}>Januar</option>
|
||||||
<option value="2" {{if eq .Settings.CurrentMonth 2}}selected{{end}}>Februar</option>
|
<option value="2" {{if eq .Settings.CurrentMonth 2}}selected{{end}}>Februar</option>
|
||||||
<option value="3" {{if eq .Settings.CurrentMonth 3}}selected{{end}}>Maerz</option>
|
<option value="3" {{if eq .Settings.CurrentMonth 3}}selected{{end}}>März</option>
|
||||||
<option value="4" {{if eq .Settings.CurrentMonth 4}}selected{{end}}>April</option>
|
<option value="4" {{if eq .Settings.CurrentMonth 4}}selected{{end}}>April</option>
|
||||||
<option value="5" {{if eq .Settings.CurrentMonth 5}}selected{{end}}>Mai</option>
|
<option value="5" {{if eq .Settings.CurrentMonth 5}}selected{{end}}>Mai</option>
|
||||||
<option value="6" {{if eq .Settings.CurrentMonth 6}}selected{{end}}>Juni</option>
|
<option value="6" {{if eq .Settings.CurrentMonth 6}}selected{{end}}>Juni</option>
|
||||||
@@ -55,11 +55,21 @@
|
|||||||
{{if .TodayTasks}}
|
{{if .TodayTasks}}
|
||||||
<ul class="tasks">
|
<ul class="tasks">
|
||||||
{{range .TodayTasks}}
|
{{range .TodayTasks}}
|
||||||
<li><strong>{{.Type}}:</strong> {{.Message}}</li>
|
<li class="{{if .Completed}}done-task{{end}}">
|
||||||
|
<span><strong>{{.Type}}:</strong> {{.Message}}</span>
|
||||||
|
<form method="post" action="/tasks/toggle" class="inline-form">
|
||||||
|
<input type="hidden" name="uid" value="{{.UID}}">
|
||||||
|
<input type="hidden" name="month" value="{{.Month}}">
|
||||||
|
<input type="hidden" name="day" value="{{.Day}}">
|
||||||
|
<input type="hidden" name="year_offset" value="{{.YearOffset}}">
|
||||||
|
<input type="hidden" name="completed" value="{{if .Completed}}1{{else}}0{{end}}">
|
||||||
|
<button type="submit" class="btn-small {{if .Completed}}secondary{{end}}">{{if .Completed}}Offen{{else}}Erledigt{{end}}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>Keine Aufgaben fuer den aktuellen Ingame-Tag.</p>
|
<p>Keine Aufgaben für den aktuellen Ingame-Tag.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -76,11 +86,21 @@
|
|||||||
{{if .Tasks}}
|
{{if .Tasks}}
|
||||||
<ul class="task-sublist">
|
<ul class="task-sublist">
|
||||||
{{range .Tasks}}
|
{{range .Tasks}}
|
||||||
<li>{{.Field}}: {{.Message}}</li>
|
<li class="{{if .Completed}}done-task{{end}}">
|
||||||
|
<span>{{.Field}}: {{.Message}}</span>
|
||||||
|
<form method="post" action="/tasks/toggle" class="inline-form">
|
||||||
|
<input type="hidden" name="uid" value="{{.UID}}">
|
||||||
|
<input type="hidden" name="month" value="{{.Month}}">
|
||||||
|
<input type="hidden" name="day" value="{{.Day}}">
|
||||||
|
<input type="hidden" name="year_offset" value="{{.YearOffset}}">
|
||||||
|
<input type="hidden" name="completed" value="{{if .Completed}}1{{else}}0{{end}}">
|
||||||
|
<button type="submit" class="btn-small {{if .Completed}}secondary{{end}}">{{if .Completed}}Offen{{else}}Erledigt{{end}}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="muted">Keine Eintraege</span>
|
<span class="muted">Keine Einträge</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||||
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfruechte</a>
|
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfrüchte</a>
|
||||||
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
||||||
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<td>{{if .GroupName}}{{.GroupName}}{{else}}-{{end}}</td>
|
<td>{{if .GroupName}}{{.GroupName}}{{else}}-{{end}}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button type="submit" class="btn-small">Speichern</button>
|
<button type="submit" class="btn-small">Speichern</button>
|
||||||
<button type="submit" formaction="/fields/delete" formnovalidate class="btn-small danger" onclick="return confirm('Feld wirklich loeschen?')">Loeschen</button>
|
<button type="submit" formaction="/fields/delete" formnovalidate class="btn-small danger" onclick="return confirm('Feld wirklich löschen?')">Löschen</button>
|
||||||
</td>
|
</td>
|
||||||
</form>
|
</form>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<form method="post" action="/field-groups/delete">
|
<form method="post" action="/field-groups/delete">
|
||||||
<input type="hidden" name="group_key" value="{{.Key}}">
|
<input type="hidden" name="group_key" value="{{.Key}}">
|
||||||
<button type="submit" class="btn-small danger" onclick="return confirm('Gruppe aufloesen?')">Aufloesen</button>
|
<button type="submit" class="btn-small danger" onclick="return confirm('Gruppe auflösen?')">Auflösen</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||||
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfruechte</a>
|
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfrüchte</a>
|
||||||
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
||||||
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -12,18 +12,18 @@
|
|||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||||
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfruechte</a>
|
<a href="/crops" class="{{if eq .ActivePath "/crops"}}active{{end}}">Feldfrüchte</a>
|
||||||
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
<a href="/fields" class="{{if eq .ActivePath "/fields"}}active{{end}}">Felder & Gruppen</a>
|
||||||
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
<a href="/general" class="{{if eq .ActivePath "/general"}}active{{end}}">Allgemein</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="layout">
|
<main class="layout">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Neuer Plan</h2>
|
<h2>Neuer Anbau-Plan</h2>
|
||||||
<form method="post" action="/plans/create" class="grid">
|
<form method="post" action="/plans/create" class="grid">
|
||||||
<label>Planungsziel
|
<label>Planungsziel
|
||||||
<select name="target_ref" required>
|
<select name="target_ref" required>
|
||||||
<option value="">Bitte waehlen</option>
|
<option value="">Bitte wählen</option>
|
||||||
{{range .PlanningTargets}}
|
{{range .PlanningTargets}}
|
||||||
<option value="{{.Ref}}">{{.Label}}</option>
|
<option value="{{.Ref}}">{{.Label}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>Feldfrucht
|
<label>Feldfrucht
|
||||||
<select name="crop_id" required>
|
<select name="crop_id" required>
|
||||||
<option value="">Bitte waehlen</option>
|
<option value="">Bitte wählen</option>
|
||||||
{{range .Crops}}
|
{{range .Crops}}
|
||||||
<option value="{{.ID}}">{{.Name}} (Monat {{.SowStartMonth}}-{{.SowEndMonth}})</option>
|
<option value="{{.ID}}">{{.Name}} (Monat {{.SowStartMonth}}-{{.SowEndMonth}})</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -54,8 +54,55 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Aufgaben-Template speichern</h2>
|
||||||
|
<form method="post" action="/task-templates/create" class="grid">
|
||||||
|
<label class="full">Template-Titel
|
||||||
|
<input type="text" name="title" maxlength="140" placeholder="z.B. Schweine füttern" required>
|
||||||
|
</label>
|
||||||
|
<button type="submit">Template speichern</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Eigene Aufgabe einplanen</h2>
|
||||||
|
<form method="post" action="/custom-tasks/create" class="grid">
|
||||||
|
<label>Template (optional)
|
||||||
|
<select name="template_id">
|
||||||
|
<option value="0">Kein Template</option>
|
||||||
|
{{range .TaskTemplates}}
|
||||||
|
<option value="{{.ID}}">{{.Title}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Alternativer Titel
|
||||||
|
<input type="text" name="title" maxlength="140" placeholder="leer lassen wenn Template genutzt wird">
|
||||||
|
</label>
|
||||||
|
<label>Monat
|
||||||
|
<select name="month">
|
||||||
|
{{range .Months}}
|
||||||
|
<option value="{{.Value}}" {{if eq $.Settings.CurrentMonth .Value}}selected{{end}}>{{.Label}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Tag
|
||||||
|
<input type="number" name="day" min="1" max="{{.Settings.DaysPerMonth}}" value="{{.Settings.CurrentDay}}">
|
||||||
|
</label>
|
||||||
|
<label>Jahr-Offset
|
||||||
|
<input type="number" name="year_offset" min="0" max="4" value="0">
|
||||||
|
</label>
|
||||||
|
<label>Ziel (optional)
|
||||||
|
<input type="text" name="target_name" maxlength="120" placeholder="z.B. Hof oder Feld 1+2">
|
||||||
|
</label>
|
||||||
|
<label class="full">Notiz (optional)
|
||||||
|
<input type="text" name="notes" maxlength="255">
|
||||||
|
</label>
|
||||||
|
<button type="submit">Aufgabe einplanen</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card full-width">
|
<section class="card full-width">
|
||||||
<h2>Geplante Durchlaeufe</h2>
|
<h2>Geplante Durchläufe</h2>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Ziel</th><th>Frucht</th><th>Aussaat</th><th>Ernte</th><th>Notiz</th><th>Aktion</th></tr></thead>
|
<thead><tr><th>Ziel</th><th>Frucht</th><th>Aussaat</th><th>Ernte</th><th>Notiz</th><th>Aktion</th></tr></thead>
|
||||||
@@ -71,7 +118,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<form method="post" action="/plans/delete">
|
<form method="post" action="/plans/delete">
|
||||||
<input type="hidden" name="id" value="{{.ID}}">
|
<input type="hidden" name="id" value="{{.ID}}">
|
||||||
<button type="submit" class="btn-small danger" onclick="return confirm('Plan wirklich loeschen?')">Loeschen</button>
|
<button type="submit" class="btn-small danger" onclick="return confirm('Plan wirklich löschen?')">Löschen</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -83,6 +130,35 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card full-width">
|
||||||
|
<h2>Eigene Aufgaben</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Titel</th><th>Ziel</th><th>Datum</th><th>Notiz</th><th>Aktion</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{{if .CustomTasks}}
|
||||||
|
{{range .CustomTasks}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Title}}</td>
|
||||||
|
<td>{{if .TargetName}}{{.TargetName}}{{else}}Allgemein{{end}}</td>
|
||||||
|
<td>Monat {{.Month}} Tag {{.Day}} (Jahr +{{.YearOffset}})</td>
|
||||||
|
<td>{{.Notes}}</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/custom-tasks/delete">
|
||||||
|
<input type="hidden" name="id" value="{{.ID}}">
|
||||||
|
<button type="submit" class="btn-small danger" onclick="return confirm('Aufgabe wirklich löschen?')">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<tr><td colspan="5">Keine eigenen Aufgaben geplant.</td></tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{{if .Error}}<div class="toast error">{{.Error}}</div>{{end}}
|
{{if .Error}}<div class="toast error">{{.Error}}</div>{{end}}
|
||||||
{{if .Info}}<div class="toast info">{{.Info}}</div>{{end}}
|
{{if .Info}}<div class="toast info">{{.Info}}</div>{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user