camper/pkg/booking/checkin.go

336 lines
10 KiB
Go
Raw Normal View History

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