Add themed UI, favicon, plan deletion, regrow cycles, and crop prep/post tasks
This commit is contained in:
@@ -6,48 +6,30 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildTasksForDay(plans []Plan, month, day int) []Task {
|
func buildTasksForDay(plans []Plan, stepMap map[int64][]CropStep, month, day int) []Task {
|
||||||
var tasks []Task
|
var tasks []Task
|
||||||
for _, p := range plans {
|
for _, p := range plans {
|
||||||
field := p.TargetName
|
tasks = append(tasks, expandPlanDayTasks(p, stepMap[p.CropID], month, day)...)
|
||||||
if strings.TrimSpace(field) == "" {
|
|
||||||
field = "Unbekanntes Feld"
|
|
||||||
}
|
}
|
||||||
if p.StartMonth == month && p.StartDay == day {
|
sortTasks(tasks)
|
||||||
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})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
})
|
|
||||||
return 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)
|
tasksByKey := make(map[string][]Task)
|
||||||
for _, p := range plans {
|
for _, p := range plans {
|
||||||
field := p.TargetName
|
for m := 1; m <= 12; m++ {
|
||||||
if strings.TrimSpace(field) == "" {
|
for d := 1; d <= daysPerMonth; d++ {
|
||||||
field = "Unbekanntes Feld"
|
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),
|
for k := range tasksByKey {
|
||||||
SortOrder: 1,
|
sortTasks(tasksByKey[k])
|
||||||
})
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var out []CalendarMonth
|
var out []CalendarMonth
|
||||||
@@ -64,16 +46,10 @@ func buildCalendar(plans []Plan, startMonth, startDay, daysPerMonth, spanMonths
|
|||||||
fromDay = startDay
|
fromDay = startDay
|
||||||
}
|
}
|
||||||
var days []CalendarDay
|
var days []CalendarDay
|
||||||
for day := fromDay; day <= daysPerMonth; day++ {
|
for d := fromDay; d <= daysPerMonth; d++ {
|
||||||
key := fmt.Sprintf("%d-%d", month, day)
|
key := fmt.Sprintf("%d-%d", month, d)
|
||||||
items := append([]Task(nil), tasksByKey[key]...)
|
items := append([]Task(nil), tasksByKey[key]...)
|
||||||
sort.Slice(items, func(i, j int) bool {
|
days = append(days, CalendarDay{Day: d, Tasks: items})
|
||||||
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})
|
|
||||||
}
|
}
|
||||||
out = append(out, CalendarMonth{
|
out = append(out, CalendarMonth{
|
||||||
Offset: offset,
|
Offset: offset,
|
||||||
@@ -85,3 +61,111 @@ func buildCalendar(plans []Plan, startMonth, startDay, daysPerMonth, spanMonths
|
|||||||
}
|
}
|
||||||
return out
|
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,
|
name VARCHAR(80) NOT NULL UNIQUE,
|
||||||
sow_start_month TINYINT NOT NULL,
|
sow_start_month TINYINT NOT NULL,
|
||||||
sow_end_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(
|
`CREATE TABLE IF NOT EXISTS plans(
|
||||||
@@ -114,7 +128,7 @@ func (a *App) getFieldsByIDs(ids []int64) ([]Field, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) listCrops() ([]Crop, 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -122,7 +136,7 @@ func (a *App) listCrops() ([]Crop, error) {
|
|||||||
var out []Crop
|
var out []Crop
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var c Crop
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
out = append(out, c)
|
out = append(out, c)
|
||||||
@@ -132,7 +146,7 @@ func (a *App) listCrops() ([]Crop, error) {
|
|||||||
|
|
||||||
func (a *App) listPlans() ([]Plan, error) {
|
func (a *App) listPlans() ([]Plan, error) {
|
||||||
rows, err := a.db.Query(`
|
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
|
FROM plans p
|
||||||
JOIN crops c ON c.id=p.crop_id
|
JOIN crops c ON c.id=p.crop_id
|
||||||
ORDER BY p.start_month,p.start_day,p.id DESC`)
|
ORDER BY p.start_month,p.start_day,p.id DESC`)
|
||||||
@@ -143,7 +157,7 @@ func (a *App) listPlans() ([]Plan, error) {
|
|||||||
var out []Plan
|
var out []Plan
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var p Plan
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
out = append(out, p)
|
out = append(out, p)
|
||||||
@@ -151,6 +165,40 @@ func (a *App) listPlans() ([]Plan, error) {
|
|||||||
return out, rows.Err()
|
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 {
|
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},
|
||||||
@@ -166,7 +214,7 @@ func seedCrops(db *sql.DB) error {
|
|||||||
{Name: "Baumwolle", SowStartMonth: 2, SowEndMonth: 3, GrowMonths: 8},
|
{Name: "Baumwolle", SowStartMonth: 2, SowEndMonth: 3, GrowMonths: 8},
|
||||||
}
|
}
|
||||||
for _, c := range items {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func wrapMonth(v int) int {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCropInput(name string, start, end, grow int) error {
|
func validateCropInput(name string, start, end, grow, regrowCycles int) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return errors.New("Name der Feldfrucht fehlt")
|
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 {
|
if grow < 1 || grow > 24 {
|
||||||
return errors.New("Wachstumsdauer muss 1-24 sein")
|
return errors.New("Wachstumsdauer muss 1-24 sein")
|
||||||
}
|
}
|
||||||
|
if regrowCycles < 0 || regrowCycles > 120 {
|
||||||
|
return errors.New("Regrow-Zyklen muessen 0-120 sein")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,11 +210,16 @@ func (a *App) handleCreateCrop(w http.ResponseWriter, r *http.Request) {
|
|||||||
start := mustInt(r.FormValue("sow_start_month"), 0)
|
start := mustInt(r.FormValue("sow_start_month"), 0)
|
||||||
end := mustInt(r.FormValue("sow_end_month"), 0)
|
end := mustInt(r.FormValue("sow_end_month"), 0)
|
||||||
grow := mustInt(r.FormValue("grow_months"), 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())
|
redirectWithMessage(w, r, "/crops", "error", err.Error())
|
||||||
return
|
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")
|
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht angelegt")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -235,15 +240,20 @@ func (a *App) handleUpdateCrop(w http.ResponseWriter, r *http.Request) {
|
|||||||
start := mustInt(r.FormValue("sow_start_month"), 0)
|
start := mustInt(r.FormValue("sow_start_month"), 0)
|
||||||
end := mustInt(r.FormValue("sow_end_month"), 0)
|
end := mustInt(r.FormValue("sow_end_month"), 0)
|
||||||
grow := mustInt(r.FormValue("grow_months"), 0)
|
grow := mustInt(r.FormValue("grow_months"), 0)
|
||||||
|
regrowEnabled := r.FormValue("regrow_enabled") == "on"
|
||||||
|
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 ungueltig")
|
||||||
return
|
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())
|
redirectWithMessage(w, r, "/crops", "error", err.Error())
|
||||||
return
|
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")
|
redirectWithMessage(w, r, "/crops", "error", "Feldfrucht nicht aktualisiert")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -331,6 +341,72 @@ func (a *App) handleCreatePlan(w http.ResponseWriter, r *http.Request) {
|
|||||||
redirectWithMessage(w, r, "/planning", "info", "Plan gespeichert")
|
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) {
|
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)
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "plans read failed", http.StatusInternalServerError)
|
http.Error(w, "plans read failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
stepMap, err := a.listCropStepsMap()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "steps read failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
data := DashboardPage{
|
data := DashboardPage{
|
||||||
BasePage: BasePage{
|
BasePage: BasePage{
|
||||||
ActivePath: "/",
|
ActivePath: "/",
|
||||||
@@ -39,8 +44,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, settings.CurrentMonth, settings.CurrentDay),
|
TodayTasks: buildTasksForDay(plans, stepMap, settings.CurrentMonth, settings.CurrentDay),
|
||||||
Calendar: buildCalendar(plans, settings.CurrentMonth, settings.CurrentDay, settings.DaysPerMonth, 14),
|
Calendar: buildCalendar(plans, stepMap, 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)
|
||||||
@@ -78,6 +83,11 @@ func (a *App) handleCropsPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "crops read failed", http.StatusInternalServerError)
|
http.Error(w, "crops read failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
steps, err := a.listCropSteps()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "steps read failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
data := CropsPage{
|
data := CropsPage{
|
||||||
BasePage: BasePage{
|
BasePage: BasePage{
|
||||||
ActivePath: "/crops",
|
ActivePath: "/crops",
|
||||||
@@ -85,6 +95,7 @@ func (a *App) handleCropsPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
Info: r.URL.Query().Get("info"),
|
Info: r.URL.Query().Get("info"),
|
||||||
},
|
},
|
||||||
Crops: crops,
|
Crops: crops,
|
||||||
|
CropSteps: steps,
|
||||||
}
|
}
|
||||||
a.renderTemplate(w, "templates/crops.html", data)
|
a.renderTemplate(w, "templates/crops.html", data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,11 @@ func main() {
|
|||||||
mux.HandleFunc("/crops/create", app.handleCreateCrop)
|
mux.HandleFunc("/crops/create", app.handleCreateCrop)
|
||||||
mux.HandleFunc("/crops/update", app.handleUpdateCrop)
|
mux.HandleFunc("/crops/update", app.handleUpdateCrop)
|
||||||
mux.HandleFunc("/crops/delete", app.handleDeleteCrop)
|
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/create", app.handleCreatePlan)
|
||||||
|
mux.HandleFunc("/plans/delete", app.handleDeletePlan)
|
||||||
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{
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ type Crop struct {
|
|||||||
SowStartMonth int
|
SowStartMonth int
|
||||||
SowEndMonth int
|
SowEndMonth int
|
||||||
GrowMonths int
|
GrowMonths int
|
||||||
|
RegrowEnabled bool
|
||||||
|
RegrowCycles int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plan struct {
|
type Plan struct {
|
||||||
@@ -38,11 +40,23 @@ type Plan struct {
|
|||||||
TargetName string
|
TargetName string
|
||||||
CropID int64
|
CropID int64
|
||||||
CropName string
|
CropName string
|
||||||
|
GrowMonths int
|
||||||
StartMonth int
|
StartMonth int
|
||||||
StartDay int
|
StartDay int
|
||||||
HarvestMonth int
|
HarvestMonth int
|
||||||
HarvestDay int
|
HarvestDay int
|
||||||
Notes string
|
Notes string
|
||||||
|
RegrowEnabled bool
|
||||||
|
RegrowCycles int
|
||||||
|
}
|
||||||
|
|
||||||
|
type CropStep struct {
|
||||||
|
ID int64
|
||||||
|
CropID int64
|
||||||
|
CropName string
|
||||||
|
Phase string
|
||||||
|
MonthOffset int
|
||||||
|
Title string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
@@ -99,6 +113,7 @@ type FieldsPage struct {
|
|||||||
type CropsPage struct {
|
type CropsPage struct {
|
||||||
BasePage
|
BasePage
|
||||||
Crops []Crop
|
Crops []Crop
|
||||||
|
CropSteps []CropStep
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlanningPage struct {
|
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 {
|
:root {
|
||||||
--bg: #f2f5ef;
|
--sky-1: #59b8df;
|
||||||
--card: #ffffff;
|
--sky-2: #f3d49a;
|
||||||
--text: #1f2a1d;
|
--field-1: #87b52a;
|
||||||
--muted: #5d6a5a;
|
--field-2: #3f6620;
|
||||||
--accent: #365f2c;
|
--soil: #6f4e2d;
|
||||||
--accent-2: #93b877;
|
--card: #ffffffee;
|
||||||
--danger: #9b2f2f;
|
--text: #1c2328;
|
||||||
|
--muted: #55606a;
|
||||||
|
--accent: #7eb61a;
|
||||||
|
--accent-dark: #537b0f;
|
||||||
|
--danger: #ac2f2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Segoe UI", Tahoma, sans-serif;
|
font-family: "Bahnschrift", "Trebuchet MS", sans-serif;
|
||||||
background: radial-gradient(circle at top, #e8f1df, var(--bg));
|
|
||||||
color: var(--text);
|
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 {
|
.top {
|
||||||
padding: 1.2rem 1rem;
|
position: relative;
|
||||||
background: linear-gradient(90deg, #2f4f2a, #4d7a40);
|
padding: 1.1rem 1rem;
|
||||||
color: #fff;
|
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 {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: .7rem 1rem;
|
padding: .75rem 1rem;
|
||||||
max-width: 1280px;
|
max-width: 1320px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs a {
|
.tabs a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #234020;
|
color: #21331b;
|
||||||
background: #e7efdf;
|
background: #f2f8e7d9;
|
||||||
border: 1px solid #cbd8c3;
|
border: 1px solid #99bd5f;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: .35rem .75rem;
|
padding: .36rem .85rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 8px #1b331111;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs a.active {
|
.tabs a.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: var(--accent);
|
border-color: var(--accent-dark);
|
||||||
border-color: var(--accent);
|
background: linear-gradient(180deg, #8fcf22, #6d9f14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
@@ -52,61 +88,85 @@ body {
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-width: 1280px;
|
max-width: 1320px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: 1px solid #dbe6d5;
|
border: 1px solid #ffffffc4;
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
padding: 1rem;
|
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 {
|
.card h2 {
|
||||||
margin-top: 0;
|
margin: 0 0 .7rem;
|
||||||
font-size: 1.05rem;
|
font-size: 1.06rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width { grid-column: 1 / -1; }
|
.full-width { grid-column: 1 / -1; }
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: .8rem;
|
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; }
|
.full { grid-column: 1 / -1; }
|
||||||
.mt { margin-top: .8rem; }
|
.mt { margin-top: .8rem; }
|
||||||
|
.mt-xs { margin-top: .3rem; }
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: .35rem;
|
gap: .35rem;
|
||||||
font-weight: 600;
|
|
||||||
color: var(--muted);
|
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;
|
font-weight: 700;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
button:hover { background: #2d5124; }
|
|
||||||
.btn-small { padding: .4rem .6rem; font-size: .9rem; }
|
input, select, textarea {
|
||||||
.danger { background: #a13939; }
|
font: inherit;
|
||||||
.danger:hover { background: #8f2f2f; }
|
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 {
|
.check {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .45rem;
|
gap: .45rem;
|
||||||
}
|
}
|
||||||
.check input { width: 1rem; height: 1rem; }
|
|
||||||
|
.check input {
|
||||||
|
width: 1.05rem;
|
||||||
|
height: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -117,65 +177,91 @@ button:hover { background: #2d5124; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: .45rem;
|
gap: .45rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-inputs {
|
.inline-inputs {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: .35rem;
|
gap: .35rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-inputs input {
|
.inline-inputs input {
|
||||||
width: 5rem;
|
width: 5.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tasks {
|
.tasks {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1.1rem;
|
padding-left: 1.1rem;
|
||||||
}
|
}
|
||||||
.tasks li { margin: .35rem 0; }
|
|
||||||
|
.tasks li { margin: .3rem 0; }
|
||||||
|
|
||||||
.task-sublist {
|
.task-sublist {
|
||||||
margin: .3rem 0 0;
|
margin: .3rem 0 0;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted { color: var(--muted); }
|
.muted { color: var(--muted); }
|
||||||
|
|
||||||
.calendar-grid {
|
.calendar-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-card {
|
.month-card {
|
||||||
border: 1px solid #dbe6d5;
|
border: 1px solid #cad5ad;
|
||||||
border-radius: 10px;
|
border-radius: 11px;
|
||||||
padding: .6rem;
|
padding: .65rem;
|
||||||
background: #f9fbf7;
|
background: linear-gradient(180deg, #fbfff5, #f0f7de);
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-card h3 {
|
.month-card h3 {
|
||||||
margin: 0 0 .4rem;
|
margin: 0 0 .45rem;
|
||||||
font-size: .98rem;
|
font-size: .98rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-days {
|
.month-days {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
max-height: 260px;
|
max-height: 260px;
|
||||||
overflow: auto;
|
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 {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 680px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
border-bottom: 1px solid #e2eadf;
|
border-bottom: 1px solid #d7decb;
|
||||||
white-space: nowrap;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #eef5e8;
|
background: #edf5de;
|
||||||
color: #384534;
|
color: #2f3a24;
|
||||||
|
}
|
||||||
|
|
||||||
|
td form {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td input,
|
||||||
|
td select {
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
@@ -186,10 +272,13 @@ th {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
max-width: 380px;
|
max-width: 380px;
|
||||||
|
box-shadow: 0 8px 18px #0000003f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.error { background: var(--danger); }
|
.toast.error { background: var(--danger); }
|
||||||
.toast.info { background: #2f6d7a; }
|
.toast.info { background: #2f6d7a; }
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 760px) {
|
||||||
.layout { grid-template-columns: 1fr; }
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
table { min-width: 540px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
<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 - Feldfruechte</title>
|
||||||
|
<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>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">
|
<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>
|
||||||
@@ -24,6 +25,10 @@
|
|||||||
<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>Zusatz-Ernten (0 = unendlich)
|
||||||
|
<input type="number" name="regrow_cycles" min="0" max="120" value="0">
|
||||||
|
</label>
|
||||||
<button type="submit">Anlegen</button>
|
<button type="submit">Anlegen</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -44,7 +49,11 @@
|
|||||||
<span>bis</span>
|
<span>bis</span>
|
||||||
<input type="number" name="sow_end_month" min="1" max="12" value="{{.SowEndMonth}}" required>
|
<input type="number" name="sow_end_month" min="1" max="12" value="{{.SowEndMonth}}" required>
|
||||||
</td>
|
</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">
|
<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 loeschen?')">Loeschen</button>
|
||||||
@@ -59,6 +68,59 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</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}}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
<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 - Dashboard</title>
|
<title>FarmCal - Dashboard</title>
|
||||||
|
<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">
|
<header class="top">
|
||||||
<h1>FarmCal</h1>
|
<h1><img src="/static/farmcal-icon.svg" alt="" class="brand-icon">FarmCal</h1>
|
||||||
<p>Dashboard</p>
|
<p>Dashboard</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
<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 - Felder</title>
|
<title>FarmCal - Felder</title>
|
||||||
|
<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>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">
|
<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>
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
<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 - Allgemein</title>
|
<title>FarmCal - Allgemein</title>
|
||||||
|
<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>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">
|
<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>
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
<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 - Anbau planen</title>
|
<title>FarmCal - Anbau planen</title>
|
||||||
|
<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>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">
|
<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>
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
<h2>Geplante Durchlaeufe</h2>
|
<h2>Geplante Durchlaeufe</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></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>
|
<tbody>
|
||||||
{{if .Plans}}
|
{{if .Plans}}
|
||||||
{{range .Plans}}
|
{{range .Plans}}
|
||||||
@@ -67,10 +68,16 @@
|
|||||||
<td>Monat {{.StartMonth}} Tag {{.StartDay}}</td>
|
<td>Monat {{.StartMonth}} Tag {{.StartDay}}</td>
|
||||||
<td>Monat {{.HarvestMonth}} Tag {{.HarvestDay}}</td>
|
<td>Monat {{.HarvestMonth}} Tag {{.HarvestDay}}</td>
|
||||||
<td>{{.Notes}}</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>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="5">Keine Planung vorhanden.</td></tr>
|
<tr><td colspan="6">Keine Planung vorhanden.</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user