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) }