/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package campsite import ( "context" "net/http" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/campsite/types" "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" ) type AdminHandler struct { types *types.AdminHandler } func NewAdminHandler(locales locale.Locales) *AdminHandler { return &AdminHandler{ types: types.NewAdminHandler(locales), } } 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 := newCampsiteForm(r.Context(), conn) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "types": h.types.Handler(user, company, conn).ServeHTTP(w, r) case "": switch r.Method { case http.MethodGet: serveCampsiteIndex(w, r, user, company, conn) case http.MethodPost: addCampsite(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } default: f := newCampsiteForm(r.Context(), conn) if err := f.FillFromDatabase(r.Context(), conn, company, head); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: f.MustRender(w, r, user, company) case http.MethodPut: editCampsite(w, r, user, company, conn, f) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } default: http.NotFound(w, r) } } } } func serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { campsites, err := collectCampsiteEntries(r.Context(), company, conn) if err != nil { panic(err) } page := &campsiteIndex{ Campsites: campsites, } page.MustRender(w, r, user, company) } func collectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*campsiteEntry, error) { rows, err := conn.Query(ctx, ` select campsite.label , campsite_type.name , campsite.active from campsite join campsite_type using (campsite_type_id) where campsite.company_id = $1 order by label`, company.ID) if err != nil { return nil, err } defer rows.Close() var campsites []*campsiteEntry for rows.Next() { entry := &campsiteEntry{} if err = rows.Scan(&entry.Label, &entry.Type, &entry.Active); err != nil { return nil, err } campsites = append(campsites, entry) } return campsites, nil } type campsiteEntry struct { Label string Type string Active bool } type campsiteIndex struct { Campsites []*campsiteEntry } func (page *campsiteIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdminFiles(w, r, user, company, page, "campsite/index.gohtml", "web/templates/campsite_map.svg") } func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := newCampsiteForm(r.Context(), conn) 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 !f.Valid(user.Locale) { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } conn.MustExec(r.Context(), "select add_campsite($1, $2)", f.CampsiteType, f.Label) httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther) } func editCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm) { 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 !f.Valid(user.Locale) { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } conn.MustExec(r.Context(), "select edit_campsite($1, $2, $3, $4)", f.ID, f.CampsiteType, f.Label, f.Active) httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther) } type campsiteForm struct { ID int CurrentLabel string Active *form.Checkbox CampsiteType *form.Select Label *form.Input } func newCampsiteForm(ctx context.Context, conn *database.Conn) *campsiteForm { campsiteTypes := form.MustGetOptions(ctx, conn, "select campsite_type_id::text, name from campsite_type where active") return &campsiteForm{ Active: &form.Checkbox{ Name: "active", Checked: true, }, CampsiteType: &form.Select{ Name: "description", Options: campsiteTypes, }, Label: &form.Input{ Name: "label", }, } } func (f *campsiteForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, label string) error { f.CurrentLabel = label row := conn.QueryRow(ctx, ` select campsite_id , array[campsite_type_id::text] , label , active from campsite where company_id = $1 and label = $2`, company.ID, label) return row.Scan(&f.ID, &f.CampsiteType.Selected, &f.Label.Val, &f.Active.Checked) } func (f *campsiteForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Active.FillValue(r) f.CampsiteType.FillValue(r) f.Label.FillValue(r) return nil } func (f *campsiteForm) Valid(l *locale.Locale) bool { v := form.NewValidator(l) v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid.")) v.CheckRequired(f.Label, l.GettextNoop("Label can not be empty.")) return v.AllOK } func (f *campsiteForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "campsite/form.gohtml", f) }