/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package season import ( "context" "net/http" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/form" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/uuid" ) type AdminHandler struct { } func NewAdminHandler() *AdminHandler { return &AdminHandler{} } func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "new": switch r.Method { case http.MethodGet: f := newSeasonForm() f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "": switch r.Method { case http.MethodGet: serveSeasonIndex(w, r, user, company, conn) case http.MethodPost: addSeason(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } default: if !uuid.Valid(head) { http.NotFound(w, r) return } f := newSeasonForm() if err := f.FillFromDatabase(r.Context(), conn, head); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } switch r.Method { case http.MethodGet: f.MustRender(w, r, user, company) case http.MethodPut: editSeason(w, r, user, company, conn, f) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } } } } func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { campsites, err := collectSeasonEntries(r.Context(), company, conn) if err != nil { panic(err) } page := &seasonIndex{ Seasons: campsites, } page.MustRender(w, r, user, company) } func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) { rows, err := conn.Query(ctx, ` select slug , name , to_color(color)::text , active from season where company_id = $1 order by name`, company.ID) if err != nil { return nil, err } defer rows.Close() var seasons []*seasonEntry for rows.Next() { entry := &seasonEntry{} if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Color, &entry.Active); err != nil { return nil, err } seasons = append(seasons, entry) } return seasons, nil } type seasonEntry struct { Slug string Name string Color string Active bool } type seasonIndex struct { Seasons []*seasonEntry } func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "season/index.gohtml", page) } func processSeasonForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm, act func()) { if err := f.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { panic(err) } else if !ok { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } act() httplib.Redirect(w, r, "/admin/seasons", http.StatusSeeOther) } func addSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := newSeasonForm() processSeasonForm(w, r, user, company, conn, f, func() { conn.MustExec(r.Context(), "select add_season($1, $2, $3)", company.ID, f.Name, f.Color) }) } func editSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm) { processSeasonForm(w, r, user, company, conn, f, func() { conn.MustExec(r.Context(), "select edit_season($1, $2, $3, $4)", f.Slug, f.Name, f.Color, f.Active) }) } type seasonForm struct { Slug string Active *form.Checkbox Name *form.Input Color *form.Input } func newSeasonForm() *seasonForm { return &seasonForm{ Active: &form.Checkbox{ Name: "active", Checked: true, }, Name: &form.Input{ Name: "label", }, Color: &form.Input{ Name: "season", }, } } func (f *seasonForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error { f.Slug = slug row := conn.QueryRow(ctx, "select name, to_color(color)::text, active from season where slug = $1", slug) return row.Scan(&f.Name.Val, &f.Color.Val, &f.Active.Checked) } func (f *seasonForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Active.FillValue(r) f.Name.FillValue(r) f.Color.FillValue(r) return nil } func (f *seasonForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { v := form.NewValidator(l) v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty.")) if v.CheckRequired(f.Color, l.GettextNoop("Color can not be empty.")) { if _, err := v.CheckValidColor(ctx, conn, f.Color, l.Gettext("This color is not valid. It must be like #123abc.")); err != nil { return false, err } } return v.AllOK, nil } func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f) }