Add themed UI, favicon, plan deletion, regrow cycles, and crop prep/post tasks
This commit is contained in:
@@ -6,48 +6,30 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func buildTasksForDay(plans []Plan, month, day int) []Task {
|
||||
func buildTasksForDay(plans []Plan, stepMap map[int64][]CropStep, month, day int) []Task {
|
||||
var tasks []Task
|
||||
for _, p := range plans {
|
||||
field := p.TargetName
|
||||
if strings.TrimSpace(field) == "" {
|
||||
field = "Unbekanntes Feld"
|
||||
}
|
||||
if p.StartMonth == month && p.StartDay == day {
|
||||
tasks = append(tasks, Task{Type: "Aussaat", Field: field, Message: fmt.Sprintf("%s auf %s aussaeen", p.CropName, field), SortOrder: 1})
|
||||
}
|
||||
if p.HarvestMonth == month && p.HarvestDay == day {
|
||||
tasks = append(tasks, Task{Type: "Ernte", Field: field, Message: fmt.Sprintf("%s auf %s ernten", p.CropName, field), SortOrder: 2})
|
||||
}
|
||||
tasks = append(tasks, expandPlanDayTasks(p, stepMap[p.CropID], month, day)...)
|
||||
}
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
if tasks[i].SortOrder == tasks[j].SortOrder {
|
||||
return tasks[i].Field < tasks[j].Field
|
||||
}
|
||||
return tasks[i].SortOrder < tasks[j].SortOrder
|
||||
})
|
||||
sortTasks(tasks)
|
||||
return tasks
|
||||
}
|
||||
|
||||
func buildCalendar(plans []Plan, startMonth, startDay, daysPerMonth, spanMonths int) []CalendarMonth {
|
||||
func buildCalendar(plans []Plan, stepMap map[int64][]CropStep, startMonth, startDay, daysPerMonth, spanMonths int) []CalendarMonth {
|
||||
tasksByKey := make(map[string][]Task)
|
||||
for _, p := range plans {
|
||||
field := p.TargetName
|
||||
if strings.TrimSpace(field) == "" {
|
||||
field = "Unbekanntes Feld"
|
||||
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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
tasksByKey[fmt.Sprintf("%d-%d", p.StartMonth, p.StartDay)] = append(tasksByKey[fmt.Sprintf("%d-%d", p.StartMonth, p.StartDay)], Task{
|
||||
Type: "Aussaat",
|
||||
Field: field,
|
||||
Message: fmt.Sprintf("Aussaat %s", p.CropName),
|
||||
SortOrder: 1,
|
||||
})
|
||||
tasksByKey[fmt.Sprintf("%d-%d", p.HarvestMonth, p.HarvestDay)] = append(tasksByKey[fmt.Sprintf("%d-%d", p.HarvestMonth, p.HarvestDay)], Task{
|
||||
Type: "Ernte",
|
||||
Field: field,
|
||||
Message: fmt.Sprintf("Ernte %s", p.CropName),
|
||||
SortOrder: 2,
|
||||
})
|
||||
}
|
||||
for k := range tasksByKey {
|
||||
sortTasks(tasksByKey[k])
|
||||
}
|
||||
|
||||
var out []CalendarMonth
|
||||
@@ -64,16 +46,10 @@ func buildCalendar(plans []Plan, startMonth, startDay, daysPerMonth, spanMonths
|
||||
fromDay = startDay
|
||||
}
|
||||
var days []CalendarDay
|
||||
for day := fromDay; day <= daysPerMonth; day++ {
|
||||
key := fmt.Sprintf("%d-%d", month, day)
|
||||
for d := fromDay; d <= daysPerMonth; d++ {
|
||||
key := fmt.Sprintf("%d-%d", month, d)
|
||||
items := append([]Task(nil), tasksByKey[key]...)
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].SortOrder == items[j].SortOrder {
|
||||
return items[i].Field < items[j].Field
|
||||
}
|
||||
return items[i].SortOrder < items[j].SortOrder
|
||||
})
|
||||
days = append(days, CalendarDay{Day: day, Tasks: items})
|
||||
days = append(days, CalendarDay{Day: d, Tasks: items})
|
||||
}
|
||||
out = append(out, CalendarMonth{
|
||||
Offset: offset,
|
||||
@@ -85,3 +61,111 @@ func buildCalendar(plans []Plan, startMonth, startDay, daysPerMonth, spanMonths
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func expandPlanDayTasks(p Plan, steps []CropStep, month, day 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 {
|
||||
out = append(out, Task{
|
||||
Type: "Aussaat",
|
||||
Field: field,
|
||||
Message: withOptionalNote(fmt.Sprintf("Aussaat %s", p.CropName), p.Notes),
|
||||
SortOrder: 10,
|
||||
})
|
||||
}
|
||||
|
||||
harvestMonths := []int{baseHarvest}
|
||||
if p.RegrowEnabled {
|
||||
maxExtra := p.RegrowCycles
|
||||
if maxExtra == 0 {
|
||||
maxExtra = 24
|
||||
}
|
||||
for i := 1; i <= maxExtra; i++ {
|
||||
harvestMonths = append(harvestMonths, wrapMonth(baseHarvest+(i*p.HarvestDistance())))
|
||||
}
|
||||
}
|
||||
harvestMonths = uniqueMonths(harvestMonths)
|
||||
for _, hm := range harvestMonths {
|
||||
if hm == month && p.HarvestDay == day {
|
||||
out = append(out, Task{
|
||||
Type: "Ernte",
|
||||
Field: field,
|
||||
Message: withOptionalNote(fmt.Sprintf("Ernte %s", p.CropName), p.Notes),
|
||||
SortOrder: 20,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range steps {
|
||||
switch s.Phase {
|
||||
case "pre":
|
||||
taskMonth := wrapMonth(p.StartMonth - s.MonthOffset)
|
||||
if taskMonth == month && p.StartDay == day {
|
||||
out = append(out, Task{
|
||||
Type: "Vorbereitung",
|
||||
Field: field,
|
||||
Message: s.Title,
|
||||
SortOrder: 5,
|
||||
})
|
||||
}
|
||||
case "post":
|
||||
for _, 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (p Plan) HarvestDistance() int {
|
||||
if p.GrowMonths <= 0 {
|
||||
return 1
|
||||
}
|
||||
return p.GrowMonths
|
||||
}
|
||||
|
||||
func withOptionalNote(base, note string) string {
|
||||
n := strings.TrimSpace(note)
|
||||
if n == "" {
|
||||
return base
|
||||
}
|
||||
return fmt.Sprintf("%s (Notiz: %s)", base, n)
|
||||
}
|
||||
|
||||
func sortTasks(tasks []Task) {
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
if tasks[i].SortOrder == tasks[j].SortOrder {
|
||||
if tasks[i].Field == tasks[j].Field {
|
||||
return tasks[i].Message < tasks[j].Message
|
||||
}
|
||||
return tasks[i].Field < tasks[j].Field
|
||||
}
|
||||
return tasks[i].SortOrder < tasks[j].SortOrder
|
||||
})
|
||||
}
|
||||
|
||||
func uniqueMonths(values []int) []int {
|
||||
seen := make(map[int]bool)
|
||||
var out []int
|
||||
for _, v := range values {
|
||||
if !seen[v] {
|
||||
seen[v] = true
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -37,7 +37,21 @@ func ensureSchema(db *sql.DB) error {
|
||||
name VARCHAR(80) NOT NULL UNIQUE,
|
||||
sow_start_month TINYINT NOT NULL,
|
||||
sow_end_month TINYINT NOT NULL,
|
||||
grow_months TINYINT NOT NULL
|
||||
grow_months TINYINT NOT NULL,
|
||||
regrow_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
regrow_cycles INT NOT NULL DEFAULT 0
|
||||
)`,
|
||||
`ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_enabled TINYINT(1) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_cycles INT NOT NULL DEFAULT 0`,
|
||||
`UPDATE crops SET regrow_cycles=0 WHERE regrow_cycles < 0`,
|
||||
`CREATE TABLE IF NOT EXISTS crop_steps(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
crop_id BIGINT NOT NULL,
|
||||
phase ENUM('pre','post') NOT NULL,
|
||||
month_offset INT NOT NULL DEFAULT 0,
|
||||
title VARCHAR(140) NOT NULL,
|
||||
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 plans(
|
||||
@@ -114,7 +128,7 @@ func (a *App) getFieldsByIDs(ids []int64) ([]Field, error) {
|
||||
}
|
||||
|
||||
func (a *App) listCrops() ([]Crop, error) {
|
||||
rows, err := a.db.Query(`SELECT id,name,sow_start_month,sow_end_month,grow_months FROM crops ORDER BY name`)
|
||||
rows, err := a.db.Query(`SELECT id,name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles FROM crops ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -122,7 +136,7 @@ func (a *App) listCrops() ([]Crop, error) {
|
||||
var out []Crop
|
||||
for rows.Next() {
|
||||
var c Crop
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.SowStartMonth, &c.SowEndMonth, &c.GrowMonths); err != nil {
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.SowStartMonth, &c.SowEndMonth, &c.GrowMonths, &c.RegrowEnabled, &c.RegrowCycles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
@@ -132,7 +146,7 @@ func (a *App) listCrops() ([]Crop, error) {
|
||||
|
||||
func (a *App) listPlans() ([]Plan, error) {
|
||||
rows, err := a.db.Query(`
|
||||
SELECT p.id,p.field_id,COALESCE(p.target_ref,''),COALESCE(p.target_name,''),p.crop_id,COALESCE(c.name,''),p.start_month,p.start_day,p.harvest_month,p.harvest_day,COALESCE(p.notes,'')
|
||||
SELECT p.id,p.field_id,COALESCE(p.target_ref,''),COALESCE(p.target_name,''),p.crop_id,COALESCE(c.name,''),COALESCE(c.grow_months,1),p.start_month,p.start_day,p.harvest_month,p.harvest_day,COALESCE(p.notes,''),COALESCE(c.regrow_enabled,0),COALESCE(c.regrow_cycles,0)
|
||||
FROM plans p
|
||||
JOIN crops c ON c.id=p.crop_id
|
||||
ORDER BY p.start_month,p.start_day,p.id DESC`)
|
||||
@@ -143,7 +157,7 @@ func (a *App) listPlans() ([]Plan, error) {
|
||||
var out []Plan
|
||||
for rows.Next() {
|
||||
var p Plan
|
||||
if err := rows.Scan(&p.ID, &p.FieldID, &p.TargetRef, &p.TargetName, &p.CropID, &p.CropName, &p.StartMonth, &p.StartDay, &p.HarvestMonth, &p.HarvestDay, &p.Notes); err != nil {
|
||||
if err := rows.Scan(&p.ID, &p.FieldID, &p.TargetRef, &p.TargetName, &p.CropID, &p.CropName, &p.GrowMonths, &p.StartMonth, &p.StartDay, &p.HarvestMonth, &p.HarvestDay, &p.Notes, &p.RegrowEnabled, &p.RegrowCycles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
@@ -151,6 +165,40 @@ func (a *App) listPlans() ([]Plan, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (a *App) listCropSteps() ([]CropStep, error) {
|
||||
rows, err := a.db.Query(`
|
||||
SELECT s.id,s.crop_id,COALESCE(c.name,''),s.phase,s.month_offset,s.title
|
||||
FROM crop_steps s
|
||||
JOIN crops c ON c.id=s.crop_id
|
||||
ORDER BY c.name, s.phase, s.month_offset, s.id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []CropStep
|
||||
for rows.Next() {
|
||||
var s CropStep
|
||||
if err := rows.Scan(&s.ID, &s.CropID, &s.CropName, &s.Phase, &s.MonthOffset, &s.Title); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (a *App) listCropStepsMap() (map[int64][]CropStep, error) {
|
||||
all, err := a.listCropSteps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[int64][]CropStep)
|
||||
for _, s := range all {
|
||||
m[s.CropID] = append(m[s.CropID], s)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func seedCrops(db *sql.DB) error {
|
||||
items := []Crop{
|
||||
{Name: "Weizen", SowStartMonth: 9, SowEndMonth: 11, GrowMonths: 10},
|
||||
@@ -166,7 +214,7 @@ func seedCrops(db *sql.DB) error {
|
||||
{Name: "Baumwolle", SowStartMonth: 2, SowEndMonth: 3, GrowMonths: 8},
|
||||
}
|
||||
for _, c := range items {
|
||||
if _, err := db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE sow_start_month=VALUES(sow_start_month),sow_end_month=VALUES(sow_end_month),grow_months=VALUES(grow_months)`, c.Name, c.SowStartMonth, c.SowEndMonth, c.GrowMonths); err != nil {
|
||||
if _, err := db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,0,0) ON DUPLICATE KEY UPDATE sow_start_month=VALUES(sow_start_month),sow_end_month=VALUES(sow_end_month),grow_months=VALUES(grow_months)`, c.Name, c.SowStartMonth, c.SowEndMonth, c.GrowMonths); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func wrapMonth(v int) int {
|
||||
return m
|
||||
}
|
||||
|
||||
func validateCropInput(name string, start, end, grow int) error {
|
||||
func validateCropInput(name string, start, end, grow, regrowCycles int) error {
|
||||
if name == "" {
|
||||
return errors.New("Name der Feldfrucht fehlt")
|
||||
}
|
||||
@@ -115,5 +115,8 @@ func validateCropInput(name string, start, end, grow int) error {
|
||||
if grow < 1 || grow > 24 {
|
||||
return errors.New("Wachstumsdauer muss 1-24 sein")
|
||||
}
|
||||
if regrowCycles < 0 || regrowCycles > 120 {
|
||||
return errors.New("Regrow-Zyklen muessen 0-120 sein")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -210,11 +210,16 @@ func (a *App) handleCreateCrop(w http.ResponseWriter, r *http.Request) {
|
||||
start := mustInt(r.FormValue("sow_start_month"), 0)
|
||||
end := mustInt(r.FormValue("sow_end_month"), 0)
|
||||
grow := mustInt(r.FormValue("grow_months"), 0)
|
||||
if err := validateCropInput(name, start, end, grow); err != nil {
|
||||
regrowEnabled := r.FormValue("regrow_enabled") == "on"
|
||||
regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0)
|
||||
if err := validateCropInput(name, start, end, grow, regrowCycles); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := a.db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months) VALUES (?,?,?,?)`, name, start, end, grow); err != nil {
|
||||
if !regrowEnabled {
|
||||
regrowCycles = 0
|
||||
}
|
||||
if _, err := a.db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,?,?)`, name, start, end, grow, regrowEnabled, regrowCycles); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht angelegt")
|
||||
return
|
||||
}
|
||||
@@ -235,15 +240,20 @@ func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) {
|
||||
start := mustInt(r.FormValue("sow_start_month"), 0)
|
||||
end := mustInt(r.FormValue("sow_end_month"), 0)
|
||||
grow := mustInt(r.FormValue("grow_months"), 0)
|
||||
regrowEnabled := r.FormValue("regrow_enabled") == "on"
|
||||
regrowCycles := mustInt(r.FormValue("regrow_cycles"), 0)
|
||||
if id <= 0 {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht-ID ungueltig")
|
||||
return
|
||||
}
|
||||
if err := validateCropInput(name, start, end, grow); err != nil {
|
||||
if err := validateCropInput(name, start, end, grow, regrowCycles); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := a.db.Exec(`UPDATE crops SET name=?,sow_start_month=?,sow_end_month=?,grow_months=? WHERE id=?`, name, start, end, grow, id); err != nil {
|
||||
if !regrowEnabled {
|
||||
regrowCycles = 0
|
||||
}
|
||||
if _, err := a.db.Exec(`UPDATE crops SET name=?,sow_start_month=?,sow_end_month=?,grow_months=?,regrow_enabled=?,regrow_cycles=? WHERE id=?`, name, start, end, grow, regrowEnabled, regrowCycles, id); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht aktualisiert")
|
||||
return
|
||||
}
|
||||
@@ -331,6 +341,72 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
|
||||
redirectWithMessage(w, r, "/planning", "info", "Plan gespeichert")
|
||||
}
|
||||
|
||||
func (a *App) handleDeletePlan(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
redirectWithMessage(w, r, "/planning", "error", "Form ungueltig")
|
||||
return
|
||||
}
|
||||
id := mustInt64(r.FormValue("id"), 0)
|
||||
if id <= 0 {
|
||||
redirectWithMessage(w, r, "/planning", "error", "Plan-ID ungueltig")
|
||||
return
|
||||
}
|
||||
if _, err := a.db.Exec(`DELETE FROM plans WHERE id=?`, id); err != nil {
|
||||
redirectWithMessage(w, r, "/planning", "error", "Plan nicht geloescht")
|
||||
return
|
||||
}
|
||||
redirectWithMessage(w, r, "/planning", "info", "Plan geloescht")
|
||||
}
|
||||
|
||||
func (a *App) handleCreateCropStep(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
|
||||
return
|
||||
}
|
||||
cropID := mustInt64(r.FormValue("crop_id"), 0)
|
||||
phase := strings.TrimSpace(r.FormValue("phase"))
|
||||
offset := mustInt(r.FormValue("month_offset"), -1)
|
||||
title := strings.TrimSpace(r.FormValue("title"))
|
||||
if cropID <= 0 || (phase != "pre" && phase != "post") || offset < 0 || offset > 24 || title == "" {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Vor/Nachbereitung ungueltig")
|
||||
return
|
||||
}
|
||||
if _, err := a.db.Exec(`INSERT INTO crop_steps(crop_id,phase,month_offset,title) VALUES (?,?,?,?)`, cropID, phase, offset, title); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Schritt nicht gespeichert")
|
||||
return
|
||||
}
|
||||
redirectWithMessage(w, r, "/crops", "info", "Schritt gespeichert")
|
||||
}
|
||||
|
||||
func (a *App) handleDeleteCropStep(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Form ungueltig")
|
||||
return
|
||||
}
|
||||
id := mustInt64(r.FormValue("id"), 0)
|
||||
if id <= 0 {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Schritt-ID ungueltig")
|
||||
return
|
||||
}
|
||||
if _, err := a.db.Exec(`DELETE FROM crop_steps WHERE id=?`, id); err != nil {
|
||||
redirectWithMessage(w, r, "/crops", "error", "Schritt nicht geloescht")
|
||||
return
|
||||
}
|
||||
redirectWithMessage(w, r, "/crops", "info", "Schritt geloescht")
|
||||
}
|
||||
|
||||
func (a *App) resolvePlanningTarget(ref string) (sql.NullInt64, string, error) {
|
||||
if strings.HasPrefix(ref, "f:") {
|
||||
id := mustInt64(strings.TrimPrefix(ref, "f:"), 0)
|
||||
|
||||
@@ -31,6 +31,11 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "plans read failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
stepMap, err := a.listCropStepsMap()
|
||||
if err != nil {
|
||||
http.Error(w, "steps read failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data := DashboardPage{
|
||||
BasePage: BasePage{
|
||||
ActivePath: "/",
|
||||
@@ -39,8 +44,8 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
Settings: settings,
|
||||
CurrentMonth: monthNames[settings.CurrentMonth-1],
|
||||
TodayTasks: buildTasksForDay(plans, settings.CurrentMonth, settings.CurrentDay),
|
||||
Calendar: buildCalendar(plans, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14),
|
||||
TodayTasks: buildTasksForDay(plans, stepMap, settings.CurrentMonth, settings.CurrentDay),
|
||||
Calendar: buildCalendar(plans, stepMap, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14),
|
||||
PlanningCount: len(plans),
|
||||
}
|
||||
a.renderTemplate(w, "templates/dashboard.html", data)
|
||||
@@ -78,13 +83,19 @@ func (a *App) handleCropsPage(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "crops read failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
steps, err := a.listCropSteps()
|
||||
if err != nil {
|
||||
http.Error(w, "steps read failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data := CropsPage{
|
||||
BasePage: BasePage{
|
||||
ActivePath: "/crops",
|
||||
Error: r.URL.Query().Get("error"),
|
||||
Info: r.URL.Query().Get("info"),
|
||||
},
|
||||
Crops: crops,
|
||||
Crops: crops,
|
||||
CropSteps: steps,
|
||||
}
|
||||
a.renderTemplate(w, "templates/crops.html", data)
|
||||
}
|
||||
|
||||
@@ -59,8 +59,11 @@ func main() {
|
||||
mux.HandleFunc("/crops/create", app.handleCreateCrop)
|
||||
mux.HandleFunc("/crops/update", app.handleUpdateCrop)
|
||||
mux.HandleFunc("/crops/delete", app.handleDeleteCrop)
|
||||
mux.HandleFunc("/crop-steps/create", app.handleCreateCropStep)
|
||||
mux.HandleFunc("/crop-steps/delete", app.handleDeleteCropStep)
|
||||
|
||||
mux.HandleFunc("/plans/create", app.handleCreatePlan)
|
||||
mux.HandleFunc("/plans/delete", app.handleDeletePlan)
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
srv := &http.Server{
|
||||
|
||||
@@ -29,6 +29,8 @@ type Crop struct {
|
||||
SowStartMonth int
|
||||
SowEndMonth int
|
||||
GrowMonths int
|
||||
RegrowEnabled bool
|
||||
RegrowCycles int
|
||||
}
|
||||
|
||||
type Plan struct {
|
||||
@@ -38,11 +40,23 @@ type Plan struct {
|
||||
TargetName string
|
||||
CropID int64
|
||||
CropName string
|
||||
GrowMonths int
|
||||
StartMonth int
|
||||
StartDay int
|
||||
HarvestMonth int
|
||||
HarvestDay int
|
||||
Notes string
|
||||
RegrowEnabled bool
|
||||
RegrowCycles int
|
||||
}
|
||||
|
||||
type CropStep struct {
|
||||
ID int64
|
||||
CropID int64
|
||||
CropName string
|
||||
Phase string
|
||||
MonthOffset int
|
||||
Title string
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
@@ -98,7 +112,8 @@ type FieldsPage struct {
|
||||
|
||||
type CropsPage struct {
|
||||
BasePage
|
||||
Crops []Crop
|
||||
Crops []Crop
|
||||
CropSteps []CropStep
|
||||
}
|
||||
|
||||
type PlanningPage struct {
|
||||
|
||||
24
static/farmcal-icon.svg
Normal file
24
static/farmcal-icon.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="FarmCal Icon">
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#5db6d8"/>
|
||||
<stop offset="100%" stop-color="#f6d27a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="field" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#8dbf2f"/>
|
||||
<stop offset="100%" stop-color="#4f7a1c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" rx="40" fill="url(#sky)"/>
|
||||
<path d="M0 180 C40 150, 100 145, 256 170 L256 256 L0 256 Z" fill="url(#field)"/>
|
||||
<circle cx="194" cy="62" r="20" fill="#ffe98a" opacity=".9"/>
|
||||
<rect x="58" y="118" width="118" height="36" rx="8" fill="#1f2c34"/>
|
||||
<rect x="136" y="84" width="48" height="40" rx="8" fill="#b8d7e9"/>
|
||||
<rect x="150" y="94" width="24" height="22" rx="3" fill="#8fb3c9"/>
|
||||
<rect x="82" y="98" width="54" height="26" rx="6" fill="#7fbf28"/>
|
||||
<circle cx="86" cy="176" r="24" fill="#1f2c34"/>
|
||||
<circle cx="86" cy="176" r="10" fill="#a73b2f"/>
|
||||
<circle cx="172" cy="176" r="34" fill="#1f2c34"/>
|
||||
<circle cx="172" cy="176" r="14" fill="#a73b2f"/>
|
||||
<rect x="52" y="154" width="154" height="10" rx="5" fill="#141d23"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,50 +1,86 @@
|
||||
:root {
|
||||
--bg: #f2f5ef;
|
||||
--card: #ffffff;
|
||||
--text: #1f2a1d;
|
||||
--muted: #5d6a5a;
|
||||
--accent: #365f2c;
|
||||
--accent-2: #93b877;
|
||||
--danger: #9b2f2f;
|
||||
--sky-1: #59b8df;
|
||||
--sky-2: #f3d49a;
|
||||
--field-1: #87b52a;
|
||||
--field-2: #3f6620;
|
||||
--soil: #6f4e2d;
|
||||
--card: #ffffffee;
|
||||
--text: #1c2328;
|
||||
--muted: #55606a;
|
||||
--accent: #7eb61a;
|
||||
--accent-dark: #537b0f;
|
||||
--danger: #ac2f2f;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Tahoma, sans-serif;
|
||||
background: radial-gradient(circle at top, #e8f1df, var(--bg));
|
||||
font-family: "Bahnschrift", "Trebuchet MS", sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(1100px 500px at 78% 14%, #fff6d6aa 0%, transparent 55%),
|
||||
linear-gradient(180deg, var(--sky-1) 0%, var(--sky-2) 35%, #f2e4c0 55%, #d4c17f 68%, #8ea347 77%, var(--field-1) 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.top {
|
||||
padding: 1.2rem 1rem;
|
||||
background: linear-gradient(90deg, #2f4f2a, #4d7a40);
|
||||
position: relative;
|
||||
padding: 1.1rem 1rem;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 10px #1d2b33aa;
|
||||
background:
|
||||
linear-gradient(130deg, #2f6e89cc, #6ca741cc),
|
||||
repeating-linear-gradient(130deg, #00000011 0 10px, #ffffff10 10px 20px);
|
||||
border-bottom: 2px solid #ffffff44;
|
||||
}
|
||||
|
||||
.top h1 {
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .55rem;
|
||||
font-size: clamp(1.5rem, 2.8vw, 2.1rem);
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: .4rem;
|
||||
box-shadow: 0 4px 14px #00000055;
|
||||
}
|
||||
|
||||
.top p {
|
||||
margin: .25rem 0 0;
|
||||
font-weight: 600;
|
||||
opacity: .96;
|
||||
}
|
||||
.top h1 { margin: 0; }
|
||||
.top p { margin: .35rem 0 0; opacity: .9; }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: .7rem 1rem;
|
||||
max-width: 1280px;
|
||||
padding: .75rem 1rem;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabs a {
|
||||
text-decoration: none;
|
||||
color: #234020;
|
||||
background: #e7efdf;
|
||||
border: 1px solid #cbd8c3;
|
||||
color: #21331b;
|
||||
background: #f2f8e7d9;
|
||||
border: 1px solid #99bd5f;
|
||||
border-radius: 999px;
|
||||
padding: .35rem .75rem;
|
||||
font-weight: 600;
|
||||
padding: .36rem .85rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px #1b331111;
|
||||
}
|
||||
|
||||
.tabs a.active {
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
border-color: var(--accent-dark);
|
||||
background: linear-gradient(180deg, #8fcf22, #6d9f14);
|
||||
}
|
||||
|
||||
.layout {
|
||||
@@ -52,61 +88,85 @@ body {
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
max-width: 1280px;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid #dbe6d5;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #ffffffc4;
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 10px 28px #2b2d181e;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.05rem;
|
||||
margin: 0 0 .7rem;
|
||||
font-size: 1.06rem;
|
||||
}
|
||||
|
||||
.full-width { grid-column: 1 / -1; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: .8rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.full { grid-column: 1 / -1; }
|
||||
.mt { margin-top: .8rem; }
|
||||
.mt-xs { margin-top: .3rem; }
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: .35rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
input, select, button {
|
||||
font: inherit;
|
||||
padding: .55rem .65rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #c9d8c2;
|
||||
}
|
||||
button {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
min-width: 0;
|
||||
}
|
||||
button:hover { background: #2d5124; }
|
||||
.btn-small { padding: .4rem .6rem; font-size: .9rem; }
|
||||
.danger { background: #a13939; }
|
||||
.danger:hover { background: #8f2f2f; }
|
||||
|
||||
input, select, textarea {
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
padding: .52rem .65rem;
|
||||
border-radius: 9px;
|
||||
border: 1px solid #b7c7a2;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
padding: .55rem .85rem;
|
||||
background: linear-gradient(180deg, #8fcf22, #6d9f14);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
button:hover { background: linear-gradient(180deg, #7cbc19, #5e8a12); }
|
||||
|
||||
.btn-small { padding: .35rem .58rem; font-size: .9rem; }
|
||||
.danger { background: linear-gradient(180deg, #cc4747, #9a2e2e); }
|
||||
.danger:hover { background: linear-gradient(180deg, #b43d3d, #842626); }
|
||||
|
||||
.check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .45rem;
|
||||
}
|
||||
.check input { width: 1rem; height: 1rem; }
|
||||
|
||||
.check input {
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--muted);
|
||||
@@ -117,65 +177,91 @@ button:hover { background: #2d5124; }
|
||||
display: flex;
|
||||
gap: .45rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inline-inputs {
|
||||
display: inline-flex;
|
||||
gap: .35rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inline-inputs input {
|
||||
width: 5rem;
|
||||
width: 5.2rem;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
.tasks li { margin: .35rem 0; }
|
||||
|
||||
.tasks li { margin: .3rem 0; }
|
||||
|
||||
.task-sublist {
|
||||
margin: .3rem 0 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.muted { color: var(--muted); }
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
gap: .75rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.month-card {
|
||||
border: 1px solid #dbe6d5;
|
||||
border-radius: 10px;
|
||||
padding: .6rem;
|
||||
background: #f9fbf7;
|
||||
border: 1px solid #cad5ad;
|
||||
border-radius: 11px;
|
||||
padding: .65rem;
|
||||
background: linear-gradient(180deg, #fbfff5, #f0f7de);
|
||||
}
|
||||
|
||||
.month-card h3 {
|
||||
margin: 0 0 .4rem;
|
||||
margin: 0 0 .45rem;
|
||||
font-size: .98rem;
|
||||
}
|
||||
|
||||
.month-days {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
.month-days li { margin: .25rem 0; }
|
||||
|
||||
.table-wrap { overflow: auto; }
|
||||
.month-days li { margin: .24rem 0; }
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 680px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: .5rem;
|
||||
border-bottom: 1px solid #e2eadf;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #d7decb;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #eef5e8;
|
||||
color: #384534;
|
||||
background: #edf5de;
|
||||
color: #2f3a24;
|
||||
}
|
||||
|
||||
td form {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
td input,
|
||||
td select {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.toast {
|
||||
@@ -186,10 +272,13 @@ th {
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
max-width: 380px;
|
||||
box-shadow: 0 8px 18px #0000003f;
|
||||
}
|
||||
|
||||
.toast.error { background: var(--danger); }
|
||||
.toast.info { background: #2f6d7a; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
@media (max-width: 760px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
table { min-width: 540px; }
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FarmCal - Feldfruechte</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/farmcal-icon.svg">
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top"><h1>FarmCal</h1><p>Feldfruechte verwalten</p></header>
|
||||
<header class="top"><h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1><p>Feldfruechte verwalten</p></header>
|
||||
<nav class="tabs">
|
||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||
@@ -24,6 +25,10 @@
|
||||
<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>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>Zusatz-Ernten (0 = unendlich)
|
||||
<input type="number" name="regrow_cycles" min="0" max="120" value="0">
|
||||
</label>
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -44,7 +49,11 @@
|
||||
<span>bis</span>
|
||||
<input type="number" name="sow_end_month" min="1" max="12" value="{{.SowEndMonth}}" required>
|
||||
</td>
|
||||
<td><input type="number" name="grow_months" min="1" max="24" value="{{.GrowMonths}}" required></td>
|
||||
<td>
|
||||
<input type="number" name="grow_months" min="1" max="24" value="{{.GrowMonths}}" required>
|
||||
<label class="check mt-xs"><input type="checkbox" name="regrow_enabled" {{if .RegrowEnabled}}checked{{end}}> Regrow</label>
|
||||
<input type="number" name="regrow_cycles" min="0" max="120" value="{{.RegrowCycles}}" title="0 = unendlich">
|
||||
</td>
|
||||
<td class="actions">
|
||||
<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>
|
||||
@@ -59,6 +68,59 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card full-width">
|
||||
<h2>Vor- und Nachbereitung je Feldfrucht</h2>
|
||||
<form method="post" action="/crop-steps/create" class="grid">
|
||||
<label>Feldfrucht
|
||||
<select name="crop_id" required>
|
||||
<option value="">Bitte waehlen</option>
|
||||
{{range .Crops}}
|
||||
<option value="{{.ID}}">{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</label>
|
||||
<label>Phase
|
||||
<select name="phase" required>
|
||||
<option value="pre">Vorbereitung (vor Aussaat)</option>
|
||||
<option value="post">Nachbereitung (nach Ernte)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Monats-Offset
|
||||
<input type="number" name="month_offset" min="0" max="24" value="0" required>
|
||||
</label>
|
||||
<label class="full">Aufgabe
|
||||
<input type="text" name="title" maxlength="140" required placeholder="z.B. Kalken oder Mulchen">
|
||||
</label>
|
||||
<button type="submit">Schritt speichern</button>
|
||||
</form>
|
||||
|
||||
<div class="table-wrap mt">
|
||||
<table>
|
||||
<thead><tr><th>Feldfrucht</th><th>Phase</th><th>Offset</th><th>Aufgabe</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
{{if .CropSteps}}
|
||||
{{range .CropSteps}}
|
||||
<tr>
|
||||
<td>{{.CropName}}</td>
|
||||
<td>{{if eq .Phase "pre"}}Vorbereitung{{else}}Nachbereitung{{end}}</td>
|
||||
<td>{{.MonthOffset}}</td>
|
||||
<td>{{.Title}}</td>
|
||||
<td>
|
||||
<form method="post" action="/crop-steps/delete">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button type="submit" class="btn-small danger" onclick="return confirm('Schritt loeschen?')">Loeschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr><td colspan="5">Keine Schritte vorhanden.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{if .Error}}<div class="toast error">{{.Error}}</div>{{end}}
|
||||
{{if .Info}}<div class="toast info">{{.Info}}</div>{{end}}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FarmCal - Dashboard</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/farmcal-icon.svg">
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top">
|
||||
<h1>FarmCal</h1>
|
||||
<h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1>
|
||||
<p>Dashboard</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FarmCal - Felder</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/farmcal-icon.svg">
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top"><h1>FarmCal</h1><p>Felder und Feldgruppen</p></header>
|
||||
<header class="top"><h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1><p>Felder und Feldgruppen</p></header>
|
||||
<nav class="tabs">
|
||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FarmCal - Allgemein</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/farmcal-icon.svg">
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top"><h1>FarmCal</h1><p>Allgemeine Einstellungen</p></header>
|
||||
<header class="top"><h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1><p>Allgemeine Einstellungen</p></header>
|
||||
<nav class="tabs">
|
||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FarmCal - Anbau planen</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/farmcal-icon.svg">
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top"><h1>FarmCal</h1><p>Anbau planen</p></header>
|
||||
<header class="top"><h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1><p>Anbau planen</p></header>
|
||||
<nav class="tabs">
|
||||
<a href="/" class="{{if eq .ActivePath "/"}}active{{end}}">Dashboard</a>
|
||||
<a href="/planning" class="{{if eq .ActivePath "/planning"}}active{{end}}">Anbau planen</a>
|
||||
@@ -57,7 +58,7 @@
|
||||
<h2>Geplante Durchlaeufe</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Ziel</th><th>Frucht</th><th>Aussaat</th><th>Ernte</th><th>Notiz</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>
|
||||
<tbody>
|
||||
{{if .Plans}}
|
||||
{{range .Plans}}
|
||||
@@ -67,10 +68,16 @@
|
||||
<td>Monat {{.StartMonth}} Tag {{.StartDay}}</td>
|
||||
<td>Monat {{.HarvestMonth}} Tag {{.HarvestDay}}</td>
|
||||
<td>{{.Notes}}</td>
|
||||
<td>
|
||||
<form method="post" action="/plans/delete">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button type="submit" class="btn-small danger" onclick="return confirm('Plan wirklich loeschen?')">Loeschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr><td colspan="5">Keine Planung vorhanden.</td></tr>
|
||||
<tr><td colspan="6">Keine Planung vorhanden.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user