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.
339 lines
10 KiB
Go
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)
|
|
}
|