jordi fita mas eeaa3b415e Add amenities section and public page
This is more or less the same as the campsites, as public information
goes, but for buildings and other amenities that the camping provides
that are not campsites.
2024-01-27 22:51:41 +01:00

305 lines
8.8 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* 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)
}