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

290 lines
8.1 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package amenity
import (
"context"
"net/http"
"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"
)
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 := newAmenityForm(company)
f.MustRender(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
case "":
switch r.Method {
case http.MethodGet:
serveAmenityIndex(w, r, user, company, conn)
case http.MethodPost:
addAmenity(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
}
default:
f := newAmenityForm(company)
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:
editAmenity(w, r, user, company, conn, f)
case http.MethodDelete:
deleteAmenity(w, r, user, conn, f.ID)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete)
}
case "slides":
h.carouselHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r)
case "features":
h.featuresHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
}
}
}
func serveAmenityIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
amenities, err := collectAmenityEntries(r.Context(), company, conn)
if err != nil {
panic(err)
}
page := &amenityIndex{
Amenities: amenities,
}
page.MustRender(w, r, user, company)
}
func collectAmenityEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*amenityEntry, error) {
rows, err := conn.Query(ctx, `
select label
, name
, active
from amenity
where company_id = $1
order by label`, company.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var amenities []*amenityEntry
for rows.Next() {
entry := &amenityEntry{}
if err = rows.Scan(&entry.Label, &entry.Name, &entry.Active); err != nil {
return nil, err
}
amenities = append(amenities, entry)
}
return amenities, nil
}
type amenityEntry struct {
Label string
Name string
Active bool
}
type amenityIndex struct {
Amenities []*amenityEntry
}
func (page *amenityIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "amenity/index.gohtml", page)
}
func addAmenity(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newAmenityForm(company)
processAmenityForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
var err error
f.ID, err = tx.AddAmenity(ctx, company.ID, f.Label.Val, f.Name[f.DefaultLang].Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val)
if err != nil {
return err
}
return translateAmenity(ctx, tx, company, f)
})
httplib.Redirect(w, r, "/admin/amenities", http.StatusSeeOther)
}
func translateAmenity(ctx context.Context, tx *database.Tx, company *auth.Company, f *amenityForm) error {
for lang := range company.Locales {
l := lang.String()
if l == f.DefaultLang {
continue
}
if err := tx.TranslateAmenity(ctx, f.ID, lang, f.Name[l].Val, f.Info1[l].Val, f.Info2[l].Val); err != nil {
return err
}
}
return nil
}
func editAmenity(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *amenityForm) {
processAmenityForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
if err := tx.EditAmenity(ctx, f.ID, f.Label.Val, f.Name[f.DefaultLang].Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val, f.Active.Checked); err != nil {
return err
}
return translateAmenity(ctx, tx, company, f)
})
httplib.Redirect(w, r, "/admin/amenities", http.StatusSeeOther)
}
func processAmenityForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *amenityForm, 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 {
panic(err)
}
tx.MustCommit(r.Context())
httplib.Redirect(w, r, "/admin/amenities", http.StatusSeeOther)
}
func deleteAmenity(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, id int) {
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if err := conn.RemoveAmenity(r.Context(), id); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/amenities/", http.StatusSeeOther)
}
type amenityForm struct {
DefaultLang string
ID int
CurrentLabel string
Active *form.Checkbox
Label *form.Input
Name form.I18nInput
Info1 form.I18nInput
Info2 form.I18nInput
}
func newAmenityForm(company *auth.Company) *amenityForm {
return &amenityForm{
DefaultLang: company.DefaultLanguage.String(),
Active: &form.Checkbox{
Name: "active",
Checked: true,
},
Label: &form.Input{
Name: "label",
},
Name: form.NewI18nInput(company.Locales, "name"),
Info1: form.NewI18nInput(company.Locales, "info1"),
Info2: form.NewI18nInput(company.Locales, "info2"),
}
}
func (f *amenityForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, label string) error {
f.CurrentLabel = label
var name database.RecordArray
var info1 database.RecordArray
var info2 database.RecordArray
row := conn.QueryRow(ctx, `
select amenity_id
, label
, amenity.name
, amenity.info1::text
, amenity.info2::text
, active
, array_agg((lang_tag, i18n.name))
, array_agg((lang_tag, i18n.info1::text))
, array_agg((lang_tag, i18n.info2::text))
from amenity
left join amenity_i18n as i18n using (amenity_id)
where company_id = $1
and label = $2
group by amenity_id
, label
, amenity.name
, amenity.info1::text
, amenity.info2::text
, active
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID, label)
if err := row.Scan(&f.ID, &f.Label.Val, &f.Name[f.DefaultLang].Val, &f.Info1[f.DefaultLang].Val, &f.Info2[f.DefaultLang].Val, &f.Active.Checked, &name, &info1, &info2); err != nil {
return err
}
if err := f.Name.FillArray(name); err != nil {
return err
}
if err := f.Info1.FillArray(info1); err != nil {
return err
}
if err := f.Info2.FillArray(info2); err != nil {
return err
}
return nil
}
func (f *amenityForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Active.FillValue(r)
f.Label.FillValue(r)
f.Name.FillValue(r)
f.Info1.FillValue(r)
f.Info2.FillValue(r)
return nil
}
func (f *amenityForm) Valid(l *locale.Locale) bool {
v := form.NewValidator(l)
v.CheckRequired(f.Label, l.GettextNoop("Label can not be empty."))
v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty."))
return v.AllOK
}
func (f *amenityForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "amenity/form.gohtml", f)
}