jordi fita mas 7edf3a3ed1 Pre-fill all guests with the holder’s address
Most will be families living at the same address.  And, if they are not,
it is far easier to replace the incorrect address with the actual,
rather than write the same address to all family members under the same
household.
2024-04-29 17:49:38 +02:00

339 lines
10 KiB
Go

package booking
import (
"context"
"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"
"github.com/jackc/pgx/v4"
"math"
"net/http"
"time"
)
func serveCheckInForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
f := newCheckinForm(slug)
if err := f.FillFromDatabase(r.Context(), conn, user.Locale); err != nil {
panic(err)
}
f.MustRender(w, r, user, company)
}
func serveGuestForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
f, err := newGuestForm(r.Context(), conn, user.Locale, slug)
if err != nil {
panic(err)
}
f.MustRender(w, r, user, company)
}
func checkInBooking(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
f := newCheckinForm(slug)
if err := f.Parse(r, user, conn); 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
}
if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
panic(err)
} else if !ok {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
f.MustRender(w, r, user, company)
}
guests := make([]*database.CheckedInGuest, 0, len(f.Guests))
for _, g := range f.Guests {
guests = append(guests, g.checkedInGuest())
}
if err := conn.CheckInGuests(r.Context(), f.Slug, guests); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/bookings", http.StatusSeeOther)
}
type checkInForm struct {
Slug string
Guests []*guestForm
}
func newCheckinForm(slug string) *checkInForm {
return &checkInForm{
Slug: slug,
}
}
func (f *checkInForm) FillFromDatabase(ctx context.Context, conn *database.Conn, l *locale.Locale) error {
documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
sexes := mustGetSexOptions(ctx, conn, l)
countries := form.MustGetCountryOptions(ctx, conn, l)
rows, err := conn.Query(ctx, `
select array[id_document_type_id]
, id_document_number
, coalesce(id_document_issue_date::text, '')
, given_name
, first_surname
, second_surname
, array[sex_id]
, birthdate::text
, array[guest.country_code::text]
, coalesce(guest.phone::text, '')
, guest.address
from booking_guest as guest
join booking using (booking_id)
where slug = $1
`, f.Slug)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
guest := newGuestFormWithOptions(documentTypes, sexes, countries, "", nil)
if err := guest.FillFromRow(rows); err != nil {
return err
}
f.Guests = append(f.Guests, guest)
}
if len(f.Guests) == 0 {
var numberGuests int
var address string
var country []string
row := conn.QueryRow(ctx, "select number_adults + number_teenagers, coalesce(address, ''), array[coalesce(country_code, '')] from booking where slug = $1", f.Slug)
if err = row.Scan(&numberGuests, &address, &country); err != nil {
return err
}
guests := make([]*guestForm, 0, numberGuests)
for i := 0; i < numberGuests; i++ {
guests = append(guests, newGuestFormWithOptions(documentTypes, sexes, countries, address, country))
}
f.Guests = guests
}
return nil
}
func mustGetSexOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*form.Option {
return form.MustGetOptions(ctx, conn, "select sex.sex_id::text, coalesce(i18n.name, sex.name) as l10n_name from sex left join sex_i18n as i18n on sex.sex_id = i18n.sex_id and i18n.lang_tag = $1 order by l10n_name", l.Language)
}
func (f *checkInForm) Parse(r *http.Request, user *auth.User, conn *database.Conn) error {
if err := r.ParseForm(); err != nil {
return err
}
documentTypes := form.MustGetDocumentTypeOptions(r.Context(), conn, user.Locale)
sexes := mustGetSexOptions(r.Context(), conn, user.Locale)
countries := form.MustGetCountryOptions(r.Context(), conn, user.Locale)
guest := newGuestFormWithOptions(documentTypes, sexes, countries, "", nil)
count := guest.count(r)
f.Guests = make([]*guestForm, 0, count)
guest.FillValueIndex(r, 0)
f.Guests = append(f.Guests, guest)
for i := 1; i < count; i++ {
guest = newGuestFormWithOptions(documentTypes, sexes, countries, "", nil)
guest.FillValueIndex(r, i)
f.Guests = append(f.Guests, guest)
}
return nil
}
func (f *checkInForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
allOK := true
for _, g := range f.Guests {
if ok, err := g.Valid(ctx, conn, l); err != nil {
return false, err
} else if !ok {
allOK = false
}
}
return allOK, nil
}
func (f *checkInForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdminFiles(w, r, user, company, f, "booking/checkin.gohtml", "booking/guest.gohtml")
}
type guestForm struct {
IDDocumentType *form.Select
IDDocumentNumber *form.Input
IDDocumentDate *form.Input
GivenName *form.Input
FirstSurname *form.Input
SecondSurname *form.Input
Sex *form.Select
Birthdate *form.Input
Country *form.Select
Address *form.Input
Phone *form.Input
}
func newGuestForm(ctx context.Context, conn *database.Conn, l *locale.Locale, slug string) (*guestForm, error) {
documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
sexes := mustGetSexOptions(ctx, conn, l)
countries := form.MustGetCountryOptions(ctx, conn, l)
var address string
var country []string
row := conn.QueryRow(ctx, "select coalesce(address, ''), array[coalesce(country_code, '')] from booking where slug = $1", slug)
if err := row.Scan(&address, &country); err != nil {
return nil, err
}
return newGuestFormWithOptions(documentTypes, sexes, countries, address, country), nil
}
func newGuestFormWithOptions(documentTypes []*form.Option, sexes []*form.Option, countries []*form.Option, address string, selectedCountry []string) *guestForm {
return &guestForm{
IDDocumentType: &form.Select{
Name: "id_document_type",
Options: documentTypes,
},
IDDocumentNumber: &form.Input{
Name: "id_document_number",
},
IDDocumentDate: &form.Input{
Name: "id_document_date",
},
GivenName: &form.Input{
Name: "given_name",
},
FirstSurname: &form.Input{
Name: "first_surname",
},
SecondSurname: &form.Input{
Name: "second_surname",
},
Sex: &form.Select{
Name: "sex",
Options: sexes,
},
Birthdate: &form.Input{
Name: "birthdate",
},
Country: &form.Select{
Name: "country",
Options: countries,
Selected: selectedCountry,
},
Address: &form.Input{
Name: "address",
Val: address,
},
Phone: &form.Input{
Name: "phone",
},
}
}
func (f *guestForm) count(r *http.Request) int {
keys := []string{f.IDDocumentType.Name, f.IDDocumentNumber.Name, f.IDDocumentDate.Name, f.GivenName.Name, f.FirstSurname.Name, f.SecondSurname.Name, f.Sex.Name, f.Birthdate.Name, f.Country.Name, f.Address.Name, f.Phone.Name}
min := math.MaxInt
for _, key := range keys {
l := len(r.Form[key])
if len(r.Form[key]) < min {
min = l
}
}
return min
}
func (f *guestForm) FillValueIndex(r *http.Request, idx int) {
f.IDDocumentType.FillValueIndex(r, idx)
f.IDDocumentNumber.FillValueIndex(r, idx)
f.IDDocumentDate.FillValueIndex(r, idx)
f.GivenName.FillValueIndex(r, idx)
f.FirstSurname.FillValueIndex(r, idx)
f.SecondSurname.FillValueIndex(r, idx)
f.Sex.FillValueIndex(r, idx)
f.Birthdate.FillValueIndex(r, idx)
f.Country.FillValueIndex(r, idx)
f.Address.FillValueIndex(r, idx)
f.Phone.FillValueIndex(r, idx)
}
func (f *guestForm) FillFromRow(row pgx.Rows) error {
return row.Scan(
&f.IDDocumentType.Selected,
&f.IDDocumentNumber.Val,
&f.IDDocumentDate.Val,
&f.GivenName.Val,
&f.FirstSurname.Val,
&f.SecondSurname.Val,
&f.Sex.Selected,
&f.Birthdate.Val,
&f.Country.Selected,
&f.Phone.Val,
&f.Address.Val,
)
}
func (f *guestForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
v := form.NewValidator(l)
today := time.Now()
yesterday := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
v.CheckSelectedOptions(f.IDDocumentType, l.GettextNoop("Selected ID document type is not valid."))
v.CheckRequired(f.IDDocumentNumber, l.GettextNoop("ID document number can not be empty."))
if f.IDDocumentDate.Val != "" {
if v.CheckValidDate(f.IDDocumentDate, l.GettextNoop("ID document issue date must be a valid date.")) {
v.CheckMaxDate(f.IDDocumentDate, yesterday, l.Gettext("ID document issue date must be in the past."))
}
}
v.CheckRequired(f.GivenName, l.GettextNoop("Full name can not be empty."))
v.CheckRequired(f.FirstSurname, l.GettextNoop("Full name can not be empty."))
v.CheckSelectedOptions(f.Sex, l.GettextNoop("Selected sex is not valid."))
if v.CheckRequired(f.Birthdate, l.GettextNoop("Birthdate can not be empty")) {
if v.CheckValidDate(f.Birthdate, l.GettextNoop("Birthdate must be a valid date.")) {
v.CheckMaxDate(f.Birthdate, yesterday, l.Gettext("Birthdate must be in the past."))
}
}
var country string
if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) {
country = f.Country.Selected[0]
}
if f.Phone.Val != "" && country != "" {
if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
return false, err
}
}
return v.AllOK, nil
}
func (f *guestForm) checkedInGuest() *database.CheckedInGuest {
birthdate, err := time.Parse(database.ISODateFormat, f.Birthdate.Val)
if err != nil {
panic(err)
}
issueDate, err := time.Parse(database.ISODateFormat, f.IDDocumentDate.Val)
if err != nil {
issueDate = time.Time{}
}
return &database.CheckedInGuest{
IDDocumentType: f.IDDocumentType.String(),
IDDocumentNumber: f.IDDocumentNumber.Val,
IDDocumentIssueDate: issueDate,
GivenName: f.GivenName.Val,
FirstSurname: f.FirstSurname.Val,
SecondSurname: f.SecondSurname.Val,
Sex: f.Sex.String(),
Birthdate: birthdate,
CountryCode: f.Country.String(),
Phone: f.Phone.Val,
Address: f.Address.Val,
}
}
func (f *guestForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdminNoLayout(w, r, user, company, "booking/guest.gohtml", f)
}