336 lines
10 KiB
Go
336 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 country []string
|
|
row := conn.QueryRow(ctx, "select number_adults + number_teenagers, array[coalesce(country_code, '')] from booking where slug = $1", f.Slug)
|
|
if err = row.Scan(&numberGuests, &country); err != nil {
|
|
return err
|
|
}
|
|
guests := make([]*guestForm, 0, numberGuests)
|
|
for i := 0; i < numberGuests; i++ {
|
|
guests = append(guests, newGuestFormWithOptions(documentTypes, sexes, countries, 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 country []string
|
|
row := conn.QueryRow(ctx, "select array[coalesce(country_code, '')] from booking where slug = $1", slug)
|
|
if err := row.Scan(&country); err != nil {
|
|
return nil, err
|
|
}
|
|
return newGuestFormWithOptions(documentTypes, sexes, countries, country), nil
|
|
}
|
|
|
|
func newGuestFormWithOptions(documentTypes []*form.Option, sexes []*form.Option, countries []*form.Option, 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",
|
|
},
|
|
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)
|
|
}
|