328 lines
10 KiB
Go
328 lines
10 KiB
Go
package web
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gardomatic.kleiax.de/lib/client"
|
|
)
|
|
|
|
type taskForm struct {
|
|
CSRFToken string `form:"csrf_token"`
|
|
PlantID int `form:"plant_id"`
|
|
LocationID int `form:"location_id"`
|
|
Title string `form:"title"`
|
|
Description string `form:"description"`
|
|
DueAtStart string `form:"due_at_start"`
|
|
DueAtEnd string `form:"due_at_end"`
|
|
Recurrence string `form:"recurrence"`
|
|
RecurrenceInterval int `form:"recurrence_interval"`
|
|
Priority int `form:"priority"`
|
|
Keywords string `form:"keywords"`
|
|
PlantStatusOnCompletion string `form:"status_on_completion"`
|
|
Errors map[string]string
|
|
}
|
|
|
|
func (app *application) tasks(w http.ResponseWriter, r *http.Request) {
|
|
gardenID, err := app.readPathID(r, "gardenID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
apiClient := client.FromContext(r.Context())
|
|
garden, _, err := apiClient.Garden(r.Context(), gardenID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
tasks, _, err := apiClient.Tasks(r.Context(), gardenID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
plants, _, err := apiClient.Plants(r.Context(), gardenID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
locations, _, err := apiClient.Locations(r.Context(), gardenID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
priorities, _, err := apiClient.TaskPriorities(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
taskYears := taskYears(tasks)
|
|
filters := collectionFilters(r, "status", "plant", "location", "priority", "month", "year", "sort")
|
|
if !r.URL.Query().Has("status") {
|
|
filters["status"] = "open"
|
|
}
|
|
tasks = filterTasks(tasks, filters)
|
|
sortTasks(tasks, filters["sort"])
|
|
tasks, pagination := paginateCollection(r, tasks)
|
|
data := app.newTemplateData(r)
|
|
data.Garden, data.Tasks, data.Plants, data.Locations, data.TaskPriorities, data.TaskYears = &garden, tasks, plants, locations, priorities, taskYears
|
|
data.Filters, data.Pagination = filters, pagination
|
|
app.render(w, http.StatusOK, "tasks.tmpl", data)
|
|
}
|
|
|
|
func filterTasks(tasks []client.Task, filters map[string]string) []client.Task {
|
|
result := make([]client.Task, 0, len(tasks))
|
|
plantID, _ := strconv.Atoi(filters["plant"])
|
|
locationID, _ := strconv.Atoi(filters["location"])
|
|
priority, _ := strconv.Atoi(filters["priority"])
|
|
month, _ := strconv.Atoi(filters["month"])
|
|
year, _ := strconv.Atoi(filters["year"])
|
|
for _, task := range tasks {
|
|
if task.Active != nil && !*task.Active {
|
|
continue
|
|
}
|
|
if filters["status"] == "open" && task.CompletedAt != nil {
|
|
continue
|
|
}
|
|
if filters["status"] == "done" && task.CompletedAt == nil {
|
|
continue
|
|
}
|
|
if plantID > 0 && (task.PlantID == nil || *task.PlantID != plantID) {
|
|
continue
|
|
}
|
|
if locationID > 0 && (task.LocationID == nil || *task.LocationID != locationID) {
|
|
continue
|
|
}
|
|
if filters["priority"] != "" && task.Priority != priority {
|
|
continue
|
|
}
|
|
if month > 0 && !taskInMonth(task, month) {
|
|
continue
|
|
}
|
|
if year > 0 && !taskInYear(task, year) {
|
|
continue
|
|
}
|
|
result = append(result, task)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (app *application) taskCreate(w http.ResponseWriter, r *http.Request) {
|
|
app.renderTaskForm(w, r, taskForm{RecurrenceInterval: 1, Errors: map[string]string{}}, http.StatusOK, 0)
|
|
}
|
|
|
|
func (app *application) taskEdit(w http.ResponseWriter, r *http.Request) {
|
|
gardenID, err := app.readPathID(r, "gardenID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
taskID, err := app.readPathID(r, "taskID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
task, _, err := client.FromContext(r.Context()).Task(r.Context(), gardenID, taskID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
form := taskForm{Title: task.Title, Description: task.Description, Recurrence: task.Recurrence, RecurrenceInterval: task.RecurrenceInterval, Priority: task.Priority, Keywords: strings.Join(task.Tags, ", "), Errors: map[string]string{}}
|
|
if form.RecurrenceInterval < 1 {
|
|
form.RecurrenceInterval = 1
|
|
}
|
|
if task.PlantID != nil {
|
|
form.PlantID = *task.PlantID
|
|
}
|
|
if task.LocationID != nil {
|
|
form.LocationID = *task.LocationID
|
|
}
|
|
if task.PlantStatusOnCompletion != nil {
|
|
form.PlantStatusOnCompletion = *task.PlantStatusOnCompletion
|
|
}
|
|
if task.DueAtStart != nil {
|
|
form.DueAtStart = task.DueAtStart.Local().Format("2006-01-02")
|
|
}
|
|
if task.DueAtEnd != nil {
|
|
form.DueAtEnd = task.DueAtEnd.Local().Format("2006-01-02")
|
|
}
|
|
app.renderTaskForm(w, r, form, http.StatusOK, taskID)
|
|
}
|
|
|
|
func (app *application) taskSave(w http.ResponseWriter, r *http.Request) {
|
|
gardenID, err := app.readPathID(r, "gardenID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
taskID := 0
|
|
if id, readErr := app.readPathID(r, "taskID"); readErr == nil {
|
|
taskID = id
|
|
}
|
|
var form taskForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
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."
|
|
}
|
|
input := client.TaskInput{Title: &form.Title, Description: &form.Description, Priority: &form.Priority, Tags: parseKeywords(form.Keywords)}
|
|
if form.PlantID > 0 {
|
|
input.PlantID = &form.PlantID
|
|
} else {
|
|
input.ClearPlantID = true
|
|
}
|
|
if form.LocationID > 0 {
|
|
input.LocationID = &form.LocationID
|
|
} else {
|
|
input.ClearLocationID = true
|
|
}
|
|
input.DueAtStart, input.ClearDueAtStart = parseTaskTime(form.DueAtStart, "due_at_start", form.Errors)
|
|
input.DueAtEnd, input.ClearDueAtEnd = parseTaskTime(form.DueAtEnd, "due_at_end", form.Errors)
|
|
if input.DueAtStart != nil && input.DueAtEnd != nil && input.DueAtStart.After(*input.DueAtEnd) {
|
|
form.Errors["due_at_end"] = "Das Ende darf nicht vor dem Beginn liegen."
|
|
}
|
|
if form.Recurrence != "" && form.Recurrence != "daily" && form.Recurrence != "weekly" && form.Recurrence != "monthly" && form.Recurrence != "yearly" {
|
|
form.Errors["recurrence"] = "Bitte eine gültige Wiederholung auswählen."
|
|
}
|
|
if form.Recurrence != "" && input.DueAtStart == nil && input.DueAtEnd == nil {
|
|
form.Errors["recurrence"] = "Für eine Wiederholung ist ein Fälligkeitsdatum erforderlich."
|
|
}
|
|
if form.Recurrence != "" && form.RecurrenceInterval < 1 {
|
|
form.Errors["recurrence_interval"] = "Der Faktor muss mindestens 1 sein."
|
|
}
|
|
input.Recurrence = &form.Recurrence
|
|
input.RecurrenceInterval = &form.RecurrenceInterval
|
|
input.PlantStatusOnCompletion = &form.PlantStatusOnCompletion
|
|
if form.PlantStatusOnCompletion != "" && form.PlantID < 1 {
|
|
form.Errors["plant_status_on_completion"] = "Bitte zuerst eine Pflanze auswählen."
|
|
}
|
|
if len(form.Errors) == 0 {
|
|
apiClient := client.FromContext(r.Context())
|
|
var saveErr error
|
|
if taskID > 0 {
|
|
_, _, saveErr = apiClient.UpdateTask(r.Context(), gardenID, taskID, input)
|
|
} else {
|
|
_, _, saveErr = apiClient.CreateTask(r.Context(), gardenID, input)
|
|
}
|
|
if saveErr == nil {
|
|
http.Redirect(w, r, webPath("tasks", gardenID), http.StatusSeeOther)
|
|
return
|
|
}
|
|
var apiError *client.APIError
|
|
if errors.As(saveErr, &apiError) && apiError.StatusCode == http.StatusUnprocessableEntity {
|
|
form.Errors = apiError.Validation
|
|
} else {
|
|
app.handleAPIError(w, r, saveErr)
|
|
return
|
|
}
|
|
}
|
|
app.renderTaskForm(w, r, form, http.StatusUnprocessableEntity, taskID)
|
|
}
|
|
|
|
func parseTaskTime(value, field string, validation map[string]string) (*time.Time, bool) {
|
|
if strings.TrimSpace(value) == "" {
|
|
return nil, true
|
|
}
|
|
parsed, err := time.ParseInLocation("2006-01-02", value, time.Local)
|
|
if err != nil {
|
|
// Accept legacy clients that still submit a datetime; current forms are day-only.
|
|
parsed, err = time.ParseInLocation("2006-01-02T15:04", value, time.Local)
|
|
}
|
|
if err != nil {
|
|
validation[field] = "Bitte ein vollständiges Datum angeben."
|
|
return nil, false
|
|
}
|
|
return &parsed, false
|
|
}
|
|
|
|
func (app *application) taskComplete(w http.ResponseWriter, r *http.Request) {
|
|
app.setTaskCompleted(w, r, true)
|
|
}
|
|
|
|
func (app *application) taskReopen(w http.ResponseWriter, r *http.Request) {
|
|
app.setTaskCompleted(w, r, false)
|
|
}
|
|
|
|
func (app *application) setTaskCompleted(w http.ResponseWriter, r *http.Request, completed bool) {
|
|
gardenID, err := app.readPathID(r, "gardenID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
taskID, err := app.readPathID(r, "taskID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
if _, _, err := client.FromContext(r.Context()).UpdateTask(r.Context(), gardenID, taskID, client.TaskInput{Completed: &completed}); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, webPath("tasks", gardenID), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) taskDelete(w http.ResponseWriter, r *http.Request) {
|
|
gardenID, err := app.readPathID(r, "gardenID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
taskID, err := app.readPathID(r, "taskID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
if _, err := client.FromContext(r.Context()).DeleteTask(r.Context(), gardenID, taskID); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, webPath("tasks", gardenID), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) renderTaskForm(w http.ResponseWriter, r *http.Request, form taskForm, status, taskID int) {
|
|
gardenID, err := app.readPathID(r, "gardenID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
apiClient := client.FromContext(r.Context())
|
|
garden, _, err := apiClient.Garden(r.Context(), gardenID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
plants, _, err := apiClient.Plants(r.Context(), gardenID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
locations, _, err := apiClient.Locations(r.Context(), gardenID)
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
priorities, _, err := apiClient.TaskPriorities(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
data := app.newTemplateData(r)
|
|
data.Garden, data.Plants, data.Locations, data.TaskPriorities, data.Form, data.TaskID = &garden, plants, locations, priorities, form, taskID
|
|
if taskID > 0 {
|
|
task, _, taskErr := apiClient.Task(r.Context(), gardenID, taskID)
|
|
if taskErr != nil {
|
|
app.handleAPIError(w, r, taskErr)
|
|
return
|
|
}
|
|
data.TaskCreatedBy = task.CreatedBy
|
|
}
|
|
data.TagSuggestions = app.loadTagSuggestions(r, gardenID)
|
|
app.render(w, status, "task_form.tmpl", data)
|
|
}
|