/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package amenity import ( "context" "net/http" "strconv" "github.com/jackc/pgx/v4" "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" ) func (h *AdminHandler) featuresHandler(user *auth.User, company *auth.Company, conn *database.Conn, label string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: serveFeatureIndex(w, r, user, company, conn, label) case http.MethodPost: addFeature(w, r, user, company, conn, label) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } case "new": switch r.Method { case http.MethodGet: f := newFeatureForm(r.Context(), company, conn, label) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "order": switch r.Method { case http.MethodPost: orderFeatures(w, r, user, company, conn, label) default: httplib.MethodNotAllowed(w, r, http.MethodPost) } default: id, err := strconv.Atoi(head) if err != nil { http.NotFound(w, r) return } f := newFeatureForm(r.Context(), company, conn, label) if err := f.FillFromDatabase(r.Context(), conn, id); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } h.featureHandler(user, company, conn, f).ServeHTTP(w, r) } }) } func (h *AdminHandler) featureHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var head string 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: editFeature(w, r, user, company, conn, f) case http.MethodDelete: deleteFeature(w, r, user, conn, f.Label, f.ID) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) } default: http.NotFound(w, r) } }) } func serveFeatureIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { features, err := collectFeatureEntries(r.Context(), conn, company, label) if err != nil { panic(err) } page := &featureIndex{ Label: label, Features: features, } page.MustRender(w, r, user, company) } func collectFeatureEntries(ctx context.Context, conn *database.Conn, company *auth.Company, label string) ([]*featureEntry, error) { rows, err := conn.Query(ctx, ` select amenity_feature_id , '/admin/amenities/' || amenity.label || '/features/' || amenity_feature_id , feature.icon_name , feature.name from amenity_feature as feature join amenity using (amenity_id) where amenity.label = $1 and amenity.company_id = $2 order by feature.position, feature.name `, label, company.ID) if err != nil { return nil, err } defer rows.Close() var features []*featureEntry for rows.Next() { f := &featureEntry{} if err = rows.Scan(&f.ID, &f.URL, &f.Icon, &f.Name); err != nil { return nil, err } features = append(features, f) } return features, nil } type featureEntry struct { ID int URL string Icon string Name string } type featureIndex struct { Label string Features []*featureEntry } func (page *featureIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "amenity/feature/index.gohtml", page) } func addFeature(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { f := newFeatureForm(r.Context(), company, conn, label) processFeatureForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { var err error f.ID, err = tx.AddAmenityFeature(ctx, company.ID, label, f.Icon.String(), f.Name[f.DefaultLang].Val) if err != nil { return err } return translateFeatures(ctx, tx, company, f) }) } func translateFeatures(ctx context.Context, tx *database.Tx, company *auth.Company, f *featureForm) error { for lang := range company.Locales { l := lang.String() if l == f.DefaultLang { continue } if err := tx.TranslateAmenityFeature(ctx, f.ID, lang, f.Name[l].Val); err != nil { return err } } return nil } func editFeature(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm) { processFeatureForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { if _, err := tx.EditAmenityFeature(ctx, f.ID, f.Icon.String(), f.Name[f.DefaultLang].Val); err != nil { return err } return translateFeatures(ctx, tx, company, f) }) } func processFeatureForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm, act func(ctx context.Context, tx *database.Tx) error) { if ok, err := form.Handle(f, w, r, user); err != nil { return } else if !ok { f.MustRender(w, r, user, company) return } tx := conn.MustBegin(r.Context()) defer tx.Rollback(r.Context()) if err := act(r.Context(), tx); err == nil { tx.MustCommit(r.Context()) } else { panic(err) } httplib.Redirect(w, r, "/admin/amenities/"+f.Label+"/features", http.StatusSeeOther) } func deleteFeature(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, label string, id int) { if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if err := conn.RemoveAmenityFeature(r.Context(), id); err != nil { panic(err) } httplib.Redirect(w, r, "/admin/amenities/"+label+"/features", http.StatusSeeOther) } type featureForm struct { DefaultLang string ID int Label string Icon *form.Select Name form.I18nInput } func newFeatureForm(ctx context.Context, company *auth.Company, conn *database.Conn, label string) *featureForm { return &featureForm{ DefaultLang: company.DefaultLanguage.String(), Label: label, Icon: &form.Select{ Name: "icon", Options: form.MustGetOptions(ctx, conn, "select icon_name, icon_name from icon order by 1"), }, Name: form.NewI18nInput(company.Locales, "name"), } } func (f *featureForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error { f.ID = id var name database.RecordArray row := conn.QueryRow(ctx, ` select array[icon_name] , feature.name , array_agg((lang_tag, i18n.name)) from amenity_feature as feature left join amenity_feature_i18n as i18n using (amenity_feature_id) where amenity_feature_id = $1 group by icon_name , feature.name `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, id) if err := row.Scan(&f.Icon.Selected, &f.Name[f.DefaultLang].Val, &name); err != nil { return err } if err := f.Name.FillArray(name); err != nil { return err } return nil } func (f *featureForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Icon.FillValue(r) f.Name.FillValue(r) return nil } func (f *featureForm) Valid(l *locale.Locale) bool { v := form.NewValidator(l) v.CheckSelectedOptions(f.Icon, l.GettextNoop("Selected icon is not valid.")) if v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty.")) { v.CheckMinLength(f.Name[f.DefaultLang], 1, l.GettextNoop("Name must have at least one letter.")) } return v.AllOK } func (f *featureForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "amenity/feature/form.gohtml", f) } func orderFeatures(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { if err := r.ParseForm(); 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 } input := r.PostForm["feature_id"] if len(input) > 0 { var ids []int for _, s := range input { if id, err := strconv.Atoi(s); err == nil { ids = append(ids, id) } else { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } } if err := conn.OrderAmenityFeatures(r.Context(), ids); err != nil { panic(err) } } serveFeatureIndex(w, r, user, company, conn, label) }