jordi fita mas 3aa53cf1a9 “Mockup” for the new booking form
It does nothing but compute the total of a booking, much like it does
for guests.  In fact, i use the same payment relations to do the exact
same computation, otherwise i am afraid i will make a mistake in the
ACSI or such, now or in future version; better if both are exactly the
same.

The idea is that once the user creates the booking, i will delete that
payment, because it makes no sense to keep it in this case; nobody is
going to pay for it.

Had to reuse the grid showing the bookings of campsites because
employees need to select one or more campsites to book, and need to see
which are available.  In this case, i have to filter by campsite type
and use the arrival and departure dates to filter the months, now up to
the day, not just month.

Had to change max width of th and td in the grid to take into account
that now a month could have a single day, for instance, and the month
heading can not stretch the day or booking spans would not be in their
correct positions.

For that, i needed to access campsiteEntry, bookingEntry, and Month from
campsite package, but campsite imports campsite/types, and
campsite/types already imports booking for the BookingDates type.  To
break the cycle, had to move all that to booking and use from campsite;
it is mostly unchanged, except for the granularity of dates up to days
instead of just months.

The design of this form calls for a different way of showing the totals,
because here employees have to see the amount next to the input with
the units, instead of having a footer with the table.  I did not like
the idea of having to query the database for that, therefore i “lifter”
the payment draft into a struct that both public and admin forms use
to show they respective views of the cart.
2024-04-23 21:07:41 +02:00

288 lines
8.3 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package campsite
import (
"context"
"net/http"
"time"
"github.com/jackc/pgx/v4"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/booking"
"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() *AdminHandler {
return &AdminHandler{
types: types.NewAdminHandler(),
}
}
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, company)
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, 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:
editCampsite(w, r, user, company, conn, f)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
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 serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
page := newCampsiteIndex()
if err := page.Parse(r); err != nil {
panic(err)
}
var err error
from := page.From.Date()
to := page.To.Date().AddDate(0, 1, 0)
page.Campsites, err = booking.CollectCampsiteEntries(r.Context(), company, conn, from, to, "")
if err != nil {
panic(err)
}
page.Months = booking.CollectMonths(from, to)
page.MustRender(w, r, user, company)
}
type campsiteIndex struct {
From *form.Month
To *form.Month
Campsites []*booking.CampsiteEntry
Months []*booking.Month
}
func newCampsiteIndex() *campsiteIndex {
now := time.Now()
from := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
to := from.AddDate(0, 3, 0)
return &campsiteIndex{
From: &form.Month{
Name: "from",
Year: from.Year(),
Month: from.Month(),
},
To: &form.Month{
Name: "to",
Year: to.Year(),
Month: to.Month(),
},
}
}
func (page *campsiteIndex) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
page.From.FillValue(r)
page.To.FillValue(r)
return nil
}
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", "booking/grid.gohtml")
}
func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newCampsiteForm(r.Context(), conn, company)
processCampsiteForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
var err error
f.ID, err = tx.AddCampsite(ctx, f.CampsiteType.Int(), f.Label.Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val)
if err != nil {
return err
}
return translateCampsite(ctx, tx, company, f)
})
httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther)
}
func translateCampsite(ctx context.Context, tx *database.Tx, company *auth.Company, f *campsiteForm) error {
for lang := range company.Locales {
l := lang.String()
if l == f.DefaultLang {
continue
}
if err := tx.TranslateCampsite(ctx, f.ID, lang, f.Info1[l].Val, f.Info2[l].Val); err != nil {
return err
}
}
return nil
}
func editCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm) {
processCampsiteForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
if err := tx.EditCampsite(ctx, f.ID, f.CampsiteType.Int(), f.Label.Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val, f.Active.Checked); err != nil {
return err
}
return translateCampsite(ctx, tx, company, f)
})
httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther)
}
func processCampsiteForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm, 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/campsites", http.StatusSeeOther)
}
type campsiteForm struct {
DefaultLang string
ID int
CurrentLabel string
Active *form.Checkbox
CampsiteType *form.Select
Label *form.Input
Info1 form.I18nInput
Info2 form.I18nInput
}
func newCampsiteForm(ctx context.Context, conn *database.Conn, company *auth.Company) *campsiteForm {
campsiteTypes := form.MustGetOptions(ctx, conn, "select campsite_type_id::text, name from campsite_type where active")
return &campsiteForm{
DefaultLang: company.DefaultLanguage.String(),
Active: &form.Checkbox{
Name: "active",
Checked: true,
},
CampsiteType: &form.Select{
Name: "description",
Options: campsiteTypes,
},
Label: &form.Input{
Name: "label",
},
Info1: form.NewI18nInput(company.Locales, "info1"),
Info2: form.NewI18nInput(company.Locales, "info2"),
}
}
func (f *campsiteForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, label string) error {
f.CurrentLabel = label
var info1 database.RecordArray
var info2 database.RecordArray
row := conn.QueryRow(ctx, `
select campsite_id
, array[campsite_type_id::text]
, label
, campsite.info1::text
, campsite.info2::text
, active
, array_agg((lang_tag, i18n.info1::text))
, array_agg((lang_tag, i18n.info2::text))
from campsite
left join campsite_i18n as i18n using (campsite_id)
where company_id = $1
and label = $2
group by campsite_id
, campsite_type_id
, label
, campsite.info1::text
, campsite.info2::text
, active
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID, label)
if err := row.Scan(&f.ID, &f.CampsiteType.Selected, &f.Label.Val, &f.Info1[f.DefaultLang].Val, &f.Info2[f.DefaultLang].Val, &f.Active.Checked, &info1, &info2); 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 *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)
f.Info1.FillValue(r)
f.Info2.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)
}