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.
305 lines
8.8 KiB
Go
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)
|
|
}
|