package web import ( "fmt" "net/http" "net/url" "strconv" "strings" "time" "gardomatic.kleiax.de/lib/client" ) func (app *application) savePlantTasks(r *http.Request, gardenID, plantID int, forms []plantTaskForm) error { apiClient := client.FromContext(r.Context()) for _, form := range forms { if form.ID > 0 { existing, _, err := apiClient.Task(r.Context(), gardenID, form.ID) if err != nil { return err } if existing.PlantID == nil || *existing.PlantID != plantID || existing.TemplateID != nil { return fmt.Errorf("task %d does not belong to plant %d or was generated from a template", form.ID, plantID) } if form.Delete { if _, err = apiClient.DeleteTask(r.Context(), gardenID, form.ID); err != nil { return err } continue } } if form.Delete { continue } start, _ := parseTaskTime(form.DueAtStart, "", map[string]string{}) end, _ := parseTaskTime(form.DueAtEnd, "", map[string]string{}) active := form.Active input := client.TaskInput{PlantID: &plantID, Title: &form.Title, Description: &form.Description, DueAtStart: start, DueAtEnd: end, ClearDueAtStart: start == nil, ClearDueAtEnd: end == nil, Priority: &form.Priority, Active: &active, Recurrence: &form.Recurrence, RecurrenceInterval: &form.RecurrenceInterval, Tags: parseKeywords(form.Keywords), PlantStatusOnCompletion: &form.PlantStatusOnCompletion} if form.LocationID > 0 { input.LocationID = &form.LocationID } else { input.ClearLocationID = true } if form.ID > 0 { if _, _, err := apiClient.UpdateTask(r.Context(), gardenID, form.ID, input); err != nil { return err } } else if _, _, err := apiClient.CreateTask(r.Context(), gardenID, input); err != nil { return err } } return nil } func (app *application) savePlantTemplateStates(r *http.Request, gardenID, plantID, speciesID int) error { templates, _, err := client.FromContext(r.Context()).SpeciesTaskTemplates(r.Context(), gardenID, speciesID) if err != nil { return err } for _, item := range templates { values, present := r.PostForm["plant_template_active_"+strconv.Itoa(item.ID)] if !present { continue } enabled := len(values) > 0 && values[len(values)-1] == "true" if _, err = client.FromContext(r.Context()).SetTaskTemplateOptOut(r.Context(), gardenID, plantID, item.ID, !enabled); err != nil { return err } } return nil } func (app *application) plantTaskRow(w http.ResponseWriter, r *http.Request) { gardenID, err := app.readPathID(r, "gardenID") if err != nil { app.notFound(w) return } form := plantTaskFromValues(r.URL.Query()) newTask := r.URL.Query().Get("new") == "true" || form.RowKey == "" if form.RowKey == "" { form.RowKey = "task-new-" + strconv.FormatInt(time.Now().UnixNano(), 36) form.Active = true } form.Errors = map[string]string{} data := app.newTemplateData(r) if !app.loadPlantTaskDialogData(w, r, gardenID, data) { return } data.PendingPlantName = form.PendingPlantName data.Garden = &client.Garden{ID: gardenID} data.Form, data.TaskDialogNew = form, newTask app.renderTemplate(w, http.StatusOK, "plant_form.tmpl", "plant_task_dialog", data) } func (app *application) plantTaskRowPost(w http.ResponseWriter, r *http.Request) { gardenID, err := app.readPathID(r, "gardenID") if err != nil { app.notFound(w) return } if err = r.ParseForm(); err != nil { app.clientError(w, http.StatusBadRequest) return } form := plantTaskFromValues(r.PostForm) form.Title, form.Description = strings.TrimSpace(form.Title), strings.TrimSpace(form.Description) form.Errors = map[string]string{} if form.Title == "" { form.Errors["title"] = "Ein Titel ist erforderlich." } start, _ := parseTaskTime(form.DueAtStart, "due_at_start", form.Errors) end, _ := parseTaskTime(form.DueAtEnd, "due_at_end", form.Errors) if start != nil && end != nil && start.After(*end) { form.Errors["due_at_end"] = "Das Ende darf nicht vor dem Beginn liegen." } newTask := r.PostForm.Get("new") == "true" data := app.newTemplateData(r) data.Garden, data.Form, data.TaskDialogNew = &client.Garden{ID: gardenID}, form, newTask data.PendingPlantName = form.PendingPlantName if len(form.Errors) > 0 { if !app.loadPlantTaskDialogData(w, r, gardenID, data) { return } w.Header().Set("HX-Retarget", "#plant-task-dialog-host") w.Header().Set("HX-Reswap", "innerHTML") app.renderTemplate(w, http.StatusOK, "plant_form.tmpl", "plant_task_dialog", data) return } w.Header().Set("HX-Trigger", "plantTaskSaved") data.PlantTasks = []plantTaskForm{form} app.renderTemplate(w, http.StatusOK, "plant_form.tmpl", "plant_task_row_fragment", data) } func (app *application) loadPlantTaskDialogData(w http.ResponseWriter, r *http.Request, gardenID int, data *templateData) bool { apiClient := client.FromContext(r.Context()) var err error data.TaskPriorities, _, err = apiClient.TaskPriorities(r.Context()) if err == nil { data.Locations, _, err = apiClient.Locations(r.Context(), gardenID) } if err == nil { data.Plants, _, err = apiClient.Plants(r.Context(), gardenID) } if err != nil { app.handleAPIError(w, r, err) return false } data.TagSuggestions = app.loadTagSuggestions(r, gardenID) return true } func plantTaskFromValues(values url.Values) plantTaskForm { id, _ := strconv.Atoi(values.Get("plant_task_id")) priority, _ := strconv.Atoi(values.Get("plant_task_priority")) plantID, _ := strconv.Atoi(values.Get("plant_task_plant_id")) if plantID == 0 { plantID, _ = strconv.Atoi(values.Get("plant_id")) } locationID, _ := strconv.Atoi(values.Get("plant_task_location_id")) if locationID == 0 { locationID, _ = strconv.Atoi(values.Get("location_id")) } recurrenceInterval, _ := strconv.Atoi(values.Get("plant_task_recurrence_interval")) if recurrenceInterval < 1 { recurrenceInterval = 1 } pendingPlantName := values.Get("pending_plant_name") if pendingPlantName == "" { pendingPlantName = values.Get("name") } return plantTaskForm{ ID: id, RowKey: values.Get("plant_task_row_key"), Title: values.Get("plant_task_title"), Description: values.Get("plant_task_description"), DueAtStart: values.Get("plant_task_due_at_start"), PlantID: plantID, DueAtEnd: values.Get("plant_task_due_at_end"), Priority: priority, LocationID: locationID, Recurrence: values.Get("plant_task_recurrence"), RecurrenceInterval: recurrenceInterval, Keywords: values.Get("plant_task_keywords"), PlantStatusOnCompletion: values.Get("plant_task_status_on_completion"), PendingPlantName: strings.TrimSpace(pendingPlantName), Active: values.Get("plant_task_active") != "false", Delete: values.Get("plant_task_delete") == "true", } } func (app *application) plantTemplateTasks(w http.ResponseWriter, r *http.Request) { gardenID, err := app.readPathID(r, "gardenID") if err != nil { app.notFound(w) return } speciesID, _ := strconv.Atoi(r.URL.Query().Get("species_id")) plantID, _ := strconv.Atoi(r.URL.Query().Get("plant_id")) data := app.newTemplateData(r) data.Garden, data.SpeciesID, data.PlantID = &client.Garden{ID: gardenID}, speciesID, plantID data.TaskTemplates, data.TemplateOptOuts = map[int][]client.SpeciesTaskTemplate{}, map[int]bool{} if speciesID > 0 { templates, _, loadErr := client.FromContext(r.Context()).SpeciesTaskTemplates(r.Context(), gardenID, speciesID) if loadErr != nil { app.handleAPIError(w, r, loadErr) return } data.TaskTemplates[speciesID] = templates } if plantID > 0 { ids, _, loadErr := client.FromContext(r.Context()).TaskTemplateOptOuts(r.Context(), gardenID, plantID) if loadErr != nil { app.handleAPIError(w, r, loadErr) return } for _, id := range ids { data.TemplateOptOuts[id] = true } } app.renderTemplate(w, http.StatusOK, "plant_form.tmpl", "plant_template_tasks", data) } func (app *application) plantTemplateTaskView(w http.ResponseWriter, r *http.Request) { gardenID, err := app.readPathID(r, "gardenID") if err != nil { app.notFound(w) return } templateID, err := app.readPathID(r, "templateID") if err != nil { app.notFound(w) return } speciesID, _ := strconv.Atoi(r.URL.Query().Get("species_id")) if speciesID < 1 { app.notFound(w) return } item, _, err := client.FromContext(r.Context()).SpeciesTaskTemplate(r.Context(), gardenID, speciesID, templateID) if err != nil { app.handleAPIError(w, r, err) return } data := app.newTemplateData(r) data.Garden, data.SpeciesID = &client.Garden{ID: gardenID}, speciesID priorities, _, err := client.FromContext(r.Context()).TaskPriorities(r.Context()) if err != nil { app.handleAPIError(w, r, err) return } data.TaskPriorities = priorities data.TaskTemplates = map[int][]client.SpeciesTaskTemplate{speciesID: {item}} app.renderTemplate(w, http.StatusOK, "plant_form.tmpl", "plant_template_task_dialog", data) }