diff --git a/pkg/booking/cart.go b/pkg/booking/cart.go index 749ab4c..9fb292f 100644 --- a/pkg/booking/cart.go +++ b/pkg/booking/cart.go @@ -2,49 +2,19 @@ package booking import ( "context" - "net/http" "strconv" "time" "github.com/jackc/pgx/v4" - "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" - httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" - "dev.tandem.ws/tandem/camper/pkg/template" ) -func handleBookingCart(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - var head string - head, r.URL.Path = httplib.ShiftPath(r.URL.Path) - - switch head { - case "": - switch r.Method { - case http.MethodGet: - f := newBookingForm(r.Context(), company, conn, user.Locale) - if err := f.Parse(r); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if cart, err := computeCart(r.Context(), conn, f); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } else { - cart.MustRender(w, r, user, company) - } - default: - httplib.MethodNotAllowed(w, r, http.MethodGet) - } - default: - http.NotFound(w, r) - } -} - type bookingCart struct { - Lines []*cartLine - Total string + Lines []*cartLine + Total string + Enabled bool } type cartLine struct { @@ -53,41 +23,43 @@ type cartLine struct { Subtotal string } -func (cart *bookingCart) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { - template.MustRenderPublicNoLayout(w, r, user, company, "booking/cart.gohtml", cart) -} - -func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*bookingCart, error) { +func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) { cart := &bookingCart{ Total: "0.0", } - campsiteType := f.CampsiteType.String() - if campsiteType == "" { + if f.Dates == nil { return cart, nil } - arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val) + arrivalDate, err := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val) if err != nil { return cart, nil } - departureDate, err := time.Parse(database.ISODateFormat, f.DepartureDate.Val) + departureDate, err := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val) if err != nil { return cart, nil } - numAdults, err := strconv.Atoi(f.NumberAdults.Val) + + if f.Guests == nil { + return cart, nil + } + numAdults, err := strconv.Atoi(f.Guests.NumberAdults.Val) if err != nil { return cart, nil } - numTeenagers, err := strconv.Atoi(f.NumberTeenagers.Val) + numTeenagers, err := strconv.Atoi(f.Guests.NumberTeenagers.Val) if err != nil { return cart, nil } - numChildren, err := strconv.Atoi(f.NumberChildren.Val) + numChildren, err := strconv.Atoi(f.Guests.NumberChildren.Val) if err != nil { return cart, nil } optionMap := make(map[int]*campsiteTypeOption) - typeOptions := f.CampsiteTypeOptions[campsiteType] + var typeOptions []*campsiteTypeOption + if f.Options != nil { + typeOptions = f.Options.Options + } optionIDs := make([]int, 0, len(typeOptions)) optionUnits := make([]int, 0, len(typeOptions)) for _, option := range typeOptions { @@ -103,7 +75,7 @@ func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*boo row := conn.QueryRow(ctx, ` with per_person as ( select count(*) as num_nights - , sum(cost_per_night)::integer as nights + , sum(cost_per_night * ceiling(($4::numeric + $5::numeric + $6::numeric) / max_campers::numeric)::integer)::integer as nights , sum(cost_per_adult * $4)::integer as adults , sum(cost_per_teenager * $5)::integer as teenagers , sum(cost_per_child * $6)::integer as children @@ -121,10 +93,10 @@ func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*boo select campsite_type_option_id , sum(cost_per_night * units)::integer as option_cost from generate_series($1, $2, interval '1 day') as date(day) - join season_calendar on season_range @> date.day::date + join season_calendar on season_range @> date.day::date join campsite_type_option_cost using (season_id) join unnest($7::integer[], $8::integer[]) as option_units(campsite_type_option_id, units) using (campsite_type_option_id) - group by campsite_type_option_id + group by campsite_type_option_id union all select -1, 0 ) select num_nights @@ -195,6 +167,7 @@ func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*boo if total != "" { cart.Total = total + cart.Enabled = f.Guests.Error == nil } return cart, nil diff --git a/pkg/booking/public.go b/pkg/booking/public.go index 411c4e3..c883247 100644 --- a/pkg/booking/public.go +++ b/pkg/booking/public.go @@ -9,8 +9,11 @@ import ( "context" "crypto/rand" "encoding/hex" + "errors" "fmt" "net/http" + "strconv" + "time" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" @@ -37,19 +40,17 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da case "": switch r.Method { case http.MethodGet: - page := newPublicPage(r.Context(), company, conn, user.Locale) - _ = r.ParseForm() - page.Form.CampsiteType.FillValue(r) - page.Form.ArrivalDate.FillValue(r) - page.Form.DepartureDate.FillValue(r) + page, err := newPublicPage(r, company, conn, user.Locale) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } page.MustRender(w, r, user, company, conn) case http.MethodPost: makeReservation(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } - case "cart": - handleBookingCart(w, r, user, company, conn) case "success": handleSuccessfulPayment(w, r, user, company, conn) case "failure": @@ -61,8 +62,8 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da } func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - f := newBookingForm(r.Context(), company, conn, user.Locale) - if err := f.Parse(r); err != nil { + f, err := newBookingForm(r, company, conn, user.Locale) + if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -76,17 +77,12 @@ func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, co page.MustRender(w, r, user, company, conn) return } - cart, err := computeCart(r.Context(), conn, f) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } schema := httplib.Protocol(r) authority := httplib.Host(r) request := &redsys.Request{ TransactionType: redsys.TransactionTypeCharge, - Amount: cart.Total, + Amount: f.Cart.Total, OrderNumber: randomOrderNumber(), Product: "Test Booking", SuccessURL: fmt.Sprintf("%s://%s/%s/booking/success", schema, authority, user.Locale.Language), @@ -113,11 +109,14 @@ func randomOrderNumber() string { type publicPage struct { *template.PublicPage Form *bookingForm - Cart *bookingCart } -func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *publicPage { - return newPublicPageWithForm(newBookingForm(ctx, company, conn, l)) +func newPublicPage(r *http.Request, company *auth.Company, conn *database.Conn, l *locale.Locale) (*publicPage, error) { + f, err := newBookingForm(r, company, conn, l) + if err != nil { + return nil, err + } + return newPublicPageWithForm(f), nil } func newPublicPageWithForm(form *bookingForm) *publicPage { @@ -130,32 +129,52 @@ func newPublicPageWithForm(form *bookingForm) *publicPage { func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { p.Setup(r, user, company, conn) var err error - p.Cart, err = computeCart(r.Context(), conn, p.Form) if err != nil { panic(err) } - template.MustRenderPublicFiles(w, r, user, company, p, "booking/form.gohtml", "booking/cart.gohtml") + if httplib.IsHTMxRequest(r) { + template.MustRenderPublicNoLayout(w, r, user, company, "booking/fields.gohtml", p) + } else { + template.MustRenderPublicFiles(w, r, user, company, p, "booking/page.gohtml", "booking/fields.gohtml") + } } type bookingForm struct { - FullName *form.Input - Address *form.Input - PostalCode *form.Input - City *form.Input - Country *form.Select - Email *form.Input - Phone *form.Input - CampsiteType *form.Select - CampsiteTypeOptions map[string][]*campsiteTypeOption - ArrivalDate *form.Input - DepartureDate *form.Input - NumberAdults *form.Input - NumberTeenagers *form.Input - NumberChildren *form.Input - NumberDogs *form.Input - ZonePreferences map[string]*form.Input - ACSICard *form.Checkbox - Agreement *form.Checkbox + CampsiteType *form.Select + Dates *DateFields + Guests *bookingGuestFields + Options *bookingOptionFields + Customer *bookingCustomerFields + Cart *bookingCart +} + +type DateFields struct { + MinNights int + MaxNights int + ArrivalDate *bookingDateInput + DepartureDate *bookingDateInput +} + +type bookingDateInput struct { + *form.Input + MinDate time.Time + MaxDate time.Time +} + +type bookingGuestFields struct { + MaxGuests int + OverflowAllowed bool + NumberAdults *form.Input + NumberTeenagers *form.Input + NumberChildren *form.Input + NumberDogs *form.Input + Error error +} + +type bookingOptionFields struct { + Legend string + ZonePreferences *form.Input + Options []*campsiteTypeOption } type campsiteTypeOption struct { @@ -166,46 +185,331 @@ type campsiteTypeOption struct { Input *form.Input } -func newBookingForm(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *bookingForm { - var typeSelectOptions []*form.Option - zonePreferences := make(map[string]*form.Input) - rows, err := conn.Query(ctx, ` - select type.slug - , coalesce(i18n.name, type.name) as l10n_name - , ask_zone_preferences - from campsite_type as type - left join campsite_type_i18n as i18n - on type.campsite_type_id = i18n.campsite_type_id - and i18n.lang_tag = $1 - where company_id = $2 - and active - order by position, l10n_name -`, l.Language, company.ID) - if err != nil { - panic(err) - } - defer rows.Close() +type bookingCustomerFields struct { + FullName *form.Input + Address *form.Input + PostalCode *form.Input + City *form.Input + Country *form.Select + Email *form.Input + Phone *form.Input + ACSICard *form.Checkbox + Agreement *form.Checkbox +} - for rows.Next() { - option := &form.Option{} - var ask bool - if err = rows.Scan(&option.Value, &option.Label, &ask); err != nil { - panic(err) +func newBookingForm(r *http.Request, company *auth.Company, conn *database.Conn, l *locale.Locale) (*bookingForm, error) { + if err := r.ParseForm(); err != nil { + return nil, err + } + + f := &bookingForm{ + CampsiteType: &form.Select{ + Name: "campsite_type", + Options: form.MustGetOptions(r.Context(), conn, "select type.slug, coalesce(i18n.name, type.name) as l10n_name from campsite_type as type left join campsite_type_i18n as i18n on type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1 where company_id = $2 and active order by position, l10n_name", l.Language, company.ID), + }, + } + f.CampsiteType.FillValue(r) + campsiteType := f.CampsiteType.String() + if campsiteType == "" { + return f, nil + } + if !f.CampsiteType.ValidOptionsSelected() { + f.CampsiteType.Error = errors.New(l.Gettext("Selected campsite type is not valid.")) + return f, nil + } + + var err error + f.Dates, err = NewDateFields(r.Context(), conn, campsiteType) + if err != nil { + return nil, err + } + f.Dates.FillValues(r, l) + if f.Dates.ArrivalDate.Val == "" || f.Dates.ArrivalDate.Error != nil || f.Dates.DepartureDate.Val == "" || f.Dates.DepartureDate.Error != nil { + return f, nil + } + + f.Guests, err = newBookingGuestFields(r.Context(), conn, campsiteType) + if err != nil { + return nil, err + } + f.Guests.FillValues(r, l) + + f.Options, err = newBookingOptionFields(r.Context(), conn, campsiteType, l) + if err != nil { + return nil, err + } + if f.Options != nil { + f.Options.FillValues(r) + } + + f.Customer = newBookingCustomerFields(r.Context(), conn, l) + f.Customer.FillValues(r) + + f.Cart, err = newBookingCart(r.Context(), conn, f, campsiteType) + if err != nil { + return nil, err + } + + return f, nil +} + +func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { + v := form.NewValidator(l) + + if f.Dates == nil { + return false, errors.New("no booking date fields") + } + if f.Guests == nil { + return false, errors.New("no guests fields") + } + if f.Customer == nil { + return false, errors.New("no customer fields") + } + + v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid.")) + f.Dates.Valid(v, l) + f.Guests.Valid(v, l) + f.Options.Valid(v, l) + if f.Options != nil { + if err := f.Customer.Valid(ctx, conn, v, l); err != nil { + return false, err } - typeSelectOptions = append(typeSelectOptions, option) - if ask { - zonePreferences[option.Value] = &form.Input{ - Name: "zone_preferences_" + option.Value, + } + return v.AllOK, nil +} + +func NewDateFields(ctx context.Context, conn *database.Conn, campsiteType string) (*DateFields, error) { + row := conn.QueryRow(ctx, ` + select lower(bookable_nights), + upper(bookable_nights) - 1, + greatest(min(lower(season_range)), current_timestamp::date), + max(upper(season_range)) + from campsite_type + join campsite_type_cost using (campsite_type_id) + join season_calendar using (season_id) + where campsite_type.slug = $1 + and season_range >> daterange(date_trunc('year', current_timestamp)::date, date_trunc('year', current_timestamp)::date + 1) + group by bookable_nights; + `, campsiteType) + f := &DateFields{ + ArrivalDate: &bookingDateInput{ + Input: &form.Input{Name: "arrival_date"}, + }, + DepartureDate: &bookingDateInput{ + Input: &form.Input{Name: "departure_date"}, + }, + } + if err := row.Scan(&f.MinNights, &f.MaxNights, &f.ArrivalDate.MinDate, &f.DepartureDate.MaxDate); err != nil { + return nil, err + } + f.ArrivalDate.MaxDate = f.DepartureDate.MaxDate.AddDate(0, 0, -f.MinNights) + f.DepartureDate.MinDate = f.ArrivalDate.MinDate.AddDate(0, 0, f.MinNights) + + return f, nil +} + +func (f *DateFields) FillValues(r *http.Request, l *locale.Locale) { + f.ArrivalDate.FillValue(r) + f.DepartureDate.FillValue(r) + + if f.ArrivalDate.Val != "" { + arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val) + if err != nil { + f.ArrivalDate.Error = errors.New(l.Gettext("Arrival date must be a valid date.")) + return + } + f.DepartureDate.MinDate = arrivalDate.AddDate(0, 0, f.MinNights) + maxDate := arrivalDate.AddDate(0, 0, f.MaxNights) + if maxDate.Before(f.DepartureDate.MaxDate) { + f.DepartureDate.MaxDate = maxDate + } + + if f.DepartureDate.Val == "" { + f.DepartureDate.Val = f.DepartureDate.MinDate.Format(database.ISODateFormat) + } else { + departureDate, err := time.Parse(database.ISODateFormat, f.DepartureDate.Val) + if err != nil { + f.DepartureDate.Error = errors.New(l.Gettext("Departure date must be a valid date.")) + return + } + if departureDate.Before(f.DepartureDate.MinDate) { + f.DepartureDate.Val = f.DepartureDate.MinDate.Format(database.ISODateFormat) + } else if departureDate.After(f.DepartureDate.MaxDate) { + f.DepartureDate.Val = f.DepartureDate.MaxDate.Format(database.ISODateFormat) } } } - if rows.Err() != nil { - panic(rows.Err()) +} + +func (f *DateFields) Valid(v *form.Validator, l *locale.Locale) { + var validBefore bool + if v.CheckRequired(f.ArrivalDate.Input, l.GettextNoop("Arrival date can not be empty")) { + if v.CheckValidDate(f.ArrivalDate.Input, l.GettextNoop("Arrival date must be a valid date.")) { + if v.CheckMinDate(f.ArrivalDate.Input, f.ArrivalDate.MinDate, fmt.Sprintf(l.Gettext("Arrival date must be %s or after."), f.ArrivalDate.MinDate.Format(database.ISODateFormat))) { + v.CheckMaxDate(f.ArrivalDate.Input, f.ArrivalDate.MaxDate, fmt.Sprintf(l.Gettext("Arrival date must be %s or before."), f.ArrivalDate.MaxDate.Format(database.ISODateFormat))) + } + } + } + if v.CheckRequired(f.DepartureDate.Input, l.GettextNoop("Departure date can not be empty")) { + if v.CheckValidDate(f.DepartureDate.Input, l.GettextNoop("Departure date must be a valid date.")) && validBefore { + if v.CheckMinDate(f.DepartureDate.Input, f.DepartureDate.MinDate, fmt.Sprintf(l.Gettext("Departure date must be %s or after."), f.DepartureDate.MinDate.Format(database.ISODateFormat))) { + v.CheckMaxDate(f.DepartureDate.Input, f.DepartureDate.MaxDate, fmt.Sprintf(l.Gettext("Departure date must be %s or before."), f.DepartureDate.MaxDate.Format(database.ISODateFormat))) + } + } + } +} + +func newBookingGuestFields(ctx context.Context, conn *database.Conn, campsiteType string) (*bookingGuestFields, error) { + f := &bookingGuestFields{ + NumberAdults: &form.Input{Name: "number_adults"}, + NumberTeenagers: &form.Input{Name: "number_teenagers"}, + NumberChildren: &form.Input{Name: "number_children"}, + } + row := conn.QueryRow(ctx, ` + select max_campers + , overflow_allowed + , dogs_allowed + from campsite_type + where slug = $1 + `, campsiteType) + var dogsAllowed bool + if err := row.Scan(&f.MaxGuests, &f.OverflowAllowed, &dogsAllowed); err != nil { + return nil, err + } + if dogsAllowed { + f.NumberDogs = &form.Input{Name: "number_dogs"} + } + return f, nil +} + +func (f *bookingGuestFields) FillValues(r *http.Request, l *locale.Locale) { + numGuests := 0 + numGuests += fillNumericField(f.NumberAdults, r, 1) + numGuests += fillNumericField(f.NumberTeenagers, r, 0) + numGuests += fillNumericField(f.NumberChildren, r, 0) + if f.NumberDogs != nil { + fillNumericField(f.NumberDogs, r, 0) + } + if !f.OverflowAllowed && numGuests > f.MaxGuests { + f.Error = fmt.Errorf(l.Gettext("There can be at most %d guests in this accommodation."), f.MaxGuests) + } +} + +func fillNumericField(input *form.Input, r *http.Request, min int) int { + input.FillValue(r) + if input.Val == "" { + input.Val = strconv.Itoa(min) + return min + } + val, err := strconv.Atoi(input.Val) + if err != nil { + input.Val = strconv.Itoa(min) + val = min + } + return val +} + +func (f *bookingGuestFields) Valid(v *form.Validator, l *locale.Locale) { + if v.CheckRequired(f.NumberAdults, l.GettextNoop("Number of adults can not be empty")) { + if v.CheckValidInteger(f.NumberAdults, l.GettextNoop("Number of adults must be an integer.")) { + v.CheckMinInteger(f.NumberAdults, 1, l.GettextNoop("There must be at least one adult.")) + } + } + if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of teenagers can not be empty")) { + if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of teenagers must be an integer.")) { + v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of teenagers can not be negative.")) + } + } + if v.CheckRequired(f.NumberChildren, l.GettextNoop("Number of children can not be empty")) { + if v.CheckValidInteger(f.NumberChildren, l.GettextNoop("Number of children must be an integer.")) { + v.CheckMinInteger(f.NumberChildren, 0, l.GettextNoop("Number of children can not be negative.")) + } + } + if v.CheckRequired(f.NumberDogs, l.GettextNoop("Number of dogs can not be empty")) { + if v.CheckValidInteger(f.NumberDogs, l.GettextNoop("Number of dogs must be an integer.")) { + v.CheckMinInteger(f.NumberDogs, 0, l.GettextNoop("Number of dogs can not be negative.")) + } + } +} + +func newBookingOptionFields(ctx context.Context, conn *database.Conn, campsiteType string, l *locale.Locale) (*bookingOptionFields, error) { + f := &bookingOptionFields{} + + row := conn.QueryRow(ctx, ` + select coalesce(i18n.name, campsite_type.name) + , ask_zone_preferences + from campsite_type + left join campsite_type_i18n as i18n on i18n.campsite_type_id = campsite_type.campsite_type_id and i18n.lang_tag = $1 + where slug = $2 + `, l.Language, campsiteType) + var askZonePreferences bool + if err := row.Scan(&f.Legend, &askZonePreferences); err != nil { + return nil, err + } + if askZonePreferences { + f.ZonePreferences = &form.Input{Name: "zone_preferences"} } - return &bookingForm{ - CampsiteTypeOptions: mustGetCampsiteTypeOptions(ctx, conn, company, l), - ZonePreferences: zonePreferences, + rows, err := conn.Query(ctx, ` + select option.campsite_type_option_id + , 'campsite_type_option_' || option.campsite_type_option_id + , coalesce(i18n.name, option.name) as l10_name + , lower(range)::text + , lower(range) + , upper(range) + from campsite_type_option as option + join campsite_type using (campsite_type_id) + left join campsite_type_option_i18n as i18n on i18n.campsite_type_option_id = option.campsite_type_option_id and i18n.lang_tag = $1 + where slug = $2 + and campsite_type.active + order by option.position, l10_name +`, l.Language, campsiteType) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + option := &campsiteTypeOption{ + Input: &form.Input{}, + } + if err = rows.Scan(&option.ID, &option.Input.Name, &option.Label, &option.Input.Val, &option.Min, &option.Max); err != nil { + return nil, err + } + f.Options = append(f.Options, option) + } + if rows.Err() != nil { + return nil, rows.Err() + } + + if f.ZonePreferences == nil && len(f.Options) == 0 { + return nil, nil + } + return f, nil +} + +func (f *bookingOptionFields) FillValues(r *http.Request) { + if f.ZonePreferences != nil { + f.ZonePreferences.FillValue(r) + } + for _, option := range f.Options { + fillNumericField(option.Input, r, option.Min) + } +} + +func (f *bookingOptionFields) Valid(v *form.Validator, l *locale.Locale) { + for _, option := range f.Options { + if v.CheckRequired(option.Input, fmt.Sprintf(l.Gettext("%s can not be empty"), option.Label)) { + if v.CheckValidInteger(option.Input, fmt.Sprintf(l.Gettext("%s must be an integer."), option.Label)) { + if v.CheckMinInteger(option.Input, option.Min, fmt.Sprintf(l.Gettext("%s must be %d or greater."), option.Label, option.Min)) { + v.CheckMaxInteger(option.Input, option.Max, fmt.Sprintf(l.Gettext("%s must be at most %d."), option.Label, option.Max)) + } + } + } + } +} + +func newBookingCustomerFields(ctx context.Context, conn *database.Conn, l *locale.Locale) *bookingCustomerFields { + return &bookingCustomerFields{ FullName: &form.Input{ Name: "full_name", }, @@ -228,32 +532,6 @@ func newBookingForm(ctx context.Context, company *auth.Company, conn *database.C Phone: &form.Input{ Name: "phone", }, - CampsiteType: &form.Select{ - Name: "campsite_type", - Options: typeSelectOptions, - }, - ArrivalDate: &form.Input{ - Name: "arrival_date", - }, - DepartureDate: &form.Input{ - Name: "departure_date", - }, - NumberAdults: &form.Input{ - Name: "number_adults", - Val: "1", - }, - NumberTeenagers: &form.Input{ - Name: "number_teenagers", - Val: "0", - }, - NumberChildren: &form.Input{ - Name: "number_children", - Val: "0", - }, - NumberDogs: &form.Input{ - Name: "number_dogs", - Val: "0", - }, ACSICard: &form.Checkbox{ Name: "acsi_card", }, @@ -263,47 +541,7 @@ func newBookingForm(ctx context.Context, company *auth.Company, conn *database.C } } -func mustGetCampsiteTypeOptions(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) map[string][]*campsiteTypeOption { - rows, err := conn.Query(ctx, ` - select option.campsite_type_option_id - , 'campsite_type_option_' || option.campsite_type_option_id - , slug - , coalesce(i18n.name, option.name) as l10_name - , lower(range)::text - , lower(range) - , upper(range) - from campsite_type_option as option - join campsite_type using (campsite_type_id) - left join campsite_type_option_i18n as i18n on i18n.campsite_type_option_id = option.campsite_type_option_id and i18n.lang_tag = $1 - where company_id = $2 - and campsite_type.active - order by option.position, l10_name -`, l.Language, company.ID) - if err != nil { - panic(err) - } - defer rows.Close() - options := make(map[string][]*campsiteTypeOption) - for rows.Next() { - var slug string - option := &campsiteTypeOption{ - Input: &form.Input{}, - } - if err = rows.Scan(&option.ID, &option.Input.Name, &slug, &option.Label, &option.Input.Val, &option.Min, &option.Max); err != nil { - panic(err) - } - options[slug] = append(options[slug], option) - } - if rows.Err() != nil { - panic(rows.Err()) - } - return options -} - -func (f *bookingForm) Parse(r *http.Request) error { - if err := r.ParseForm(); err != nil { - return err - } +func (f *bookingCustomerFields) FillValues(r *http.Request) { f.FullName.FillValue(r) f.Address.FillValue(r) f.PostalCode.FillValue(r) @@ -311,29 +549,11 @@ func (f *bookingForm) Parse(r *http.Request) error { f.Country.FillValue(r) f.Email.FillValue(r) f.Phone.FillValue(r) - f.CampsiteType.FillValue(r) - f.ArrivalDate.FillValue(r) - f.DepartureDate.FillValue(r) - f.NumberAdults.FillValue(r) - f.NumberTeenagers.FillValue(r) - f.NumberChildren.FillValue(r) - f.NumberDogs.FillValue(r) f.ACSICard.FillValue(r) f.Agreement.FillValue(r) - for _, preferences := range f.ZonePreferences { - preferences.FillValue(r) - } - for _, options := range f.CampsiteTypeOptions { - for _, option := range options { - option.Input.FillValue(r) - } - } - return nil } -func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { - v := form.NewValidator(l) - +func (f *bookingCustomerFields) Valid(ctx context.Context, conn *database.Conn, v *form.Validator, l *locale.Locale) error { var country string if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) { country = f.Country.Selected[0] @@ -345,7 +565,7 @@ func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale. if country != "" && f.PostalCode.Val != "" { if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postal code is not valid.")); err != nil { - return false, err + return err } } if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) { @@ -353,53 +573,10 @@ func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale. } if v.CheckRequired(f.Phone, l.GettextNoop("Phone can not be empty.")) { if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil { - return false, err - } - } - var validBefore bool - v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid.")) - if v.CheckRequired(f.ArrivalDate, l.GettextNoop("Arrival date can not be empty")) { - if v.CheckValidDate(f.ArrivalDate, l.GettextNoop("Arrival date must be a valid date.")) { - validBefore = true - } - } - if v.CheckRequired(f.DepartureDate, l.GettextNoop("Departure date can not be empty")) { - if v.CheckValidDate(f.DepartureDate, l.GettextNoop("Departure date must be a valid date.")) && validBefore { - v.CheckDateAfter(f.DepartureDate, f.ArrivalDate, l.GettextNoop("The departure date must be after the arrival date.")) + return err } } - if v.CheckRequired(f.NumberAdults, l.GettextNoop("Number of adults can not be empty")) { - if v.CheckValidInteger(f.NumberAdults, l.GettextNoop("Number of adults must be an integer.")) { - v.CheckMinInteger(f.NumberAdults, 1, l.GettextNoop("There must be at least one adult.")) - } - } - if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of teenagers can not be empty")) { - if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of teenagers must be an integer.")) { - v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of teenagers can not be negative.")) - } - } - if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of children can not be empty")) { - if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of children must be an integer.")) { - v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of children can not be negative.")) - } - } - if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of dogs can not be empty")) { - if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of dogs must be an integer.")) { - v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of dogs can not be negative.")) - } - } v.Check(f.Agreement, f.Agreement.Checked, l.GettextNoop("It is mandatory to agree to the reservation conditions.")) - for _, options := range f.CampsiteTypeOptions { - for _, option := range options { - if v.CheckRequired(option.Input, fmt.Sprintf(l.Gettext("%s can not be empty"), option.Label)) { - if v.CheckValidInteger(option.Input, fmt.Sprintf(l.Gettext("%s must be an integer."), option.Label)) { - if v.CheckMinInteger(option.Input, option.Min, fmt.Sprintf(l.Gettext("%s must be %d or greater."), option.Label, option.Min)) { - v.CheckMaxInteger(option.Input, option.Max, fmt.Sprintf(l.Gettext("%s must be at most %d."), option.Label, option.Max)) - } - } - } - } - } - return v.AllOK, nil + return nil } diff --git a/pkg/campsite/types/public.go b/pkg/campsite/types/public.go index 2cf4fc0..2674609 100644 --- a/pkg/campsite/types/public.go +++ b/pkg/campsite/types/public.go @@ -7,14 +7,15 @@ package types import ( "context" - "github.com/jackc/pgx/v4" gotemplate "html/template" "net/http" "time" + "github.com/jackc/pgx/v4" "golang.org/x/text/language" "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/booking" "dev.tandem.ws/tandem/camper/pkg/carousel" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" @@ -54,6 +55,21 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da default: httplib.MethodNotAllowed(w, r, http.MethodGet) } + case "dates": + switch r.Method { + case http.MethodGet: + bookingDates, err := booking.NewDateFields(r.Context(), conn, typeUuid) + if err != nil { + panic(err) + } + if err := r.ParseForm(); err != nil { + panic(err) + } + bookingDates.FillValues(r, user.Locale) + template.MustRenderPublicNoLayout(w, r, user, company, "campsite/dates.gohtml", bookingDates) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } default: http.NotFound(w, r) } @@ -78,8 +94,7 @@ type publicPage struct { AdditionalInfo gotemplate.HTML CheckIn string CheckOut string - MinNights int - MaxNights int + BookingDates *booking.DateFields } type typePrice struct { @@ -124,8 +139,6 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co , coalesce(i18n.additional_info, campsite_type.additional_info)::text as l10n_description , coalesce(i18n.check_in, campsite_type.check_in)::text as l10n_check_in , coalesce(i18n.check_out, campsite_type.check_out)::text as l10n_check_out - , lower(bookable_nights) - , upper(bookable_nights) - 1 , dogs_allowed , '3.50' as dogs_prices from campsite_type @@ -142,8 +155,6 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co &page.AdditionalInfo, &page.CheckIn, &page.CheckOut, - &page.MinNights, - &page.MaxNights, &page.DogsAllowed, &page.DogsPrice, ); err != nil { @@ -152,6 +163,10 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co if err = conn.QueryRow(ctx, "select to_price(tourist_tax, $1) from company where company_id = $2", company.DecimalDigits, company.ID).Scan(&page.TouristTax); err != nil { return nil, err } + page.BookingDates, err = booking.NewDateFields(ctx, conn, slug) + if err != nil { + return nil, err + } page.Prices, err = collectPrices(ctx, conn, loc.Language, slug, page.Name) if err != nil { return nil, err @@ -280,5 +295,5 @@ func collectFeatures(ctx context.Context, conn *database.Conn, language language func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { p.Setup(r, user, company, conn) - template.MustRenderPublicFiles(w, r, user, company, p, "campsite/type.gohtml", "campsite/calendar.gohtml") + template.MustRenderPublicFiles(w, r, user, company, p, "campsite/type.gohtml", "campsite/calendar.gohtml", "campsite/dates.gohtml") } diff --git a/pkg/form/select.go b/pkg/form/select.go index 03ff0c4..792c531 100644 --- a/pkg/form/select.go +++ b/pkg/form/select.go @@ -48,7 +48,7 @@ func (s *Select) FillValue(r *http.Request) { s.Selected = r.Form[s.Name] } -func (s *Select) validOptionsSelected() bool { +func (s *Select) ValidOptionsSelected() bool { for _, selected := range s.Selected { if !s.isValidOption(selected) { return false diff --git a/pkg/form/validator.go b/pkg/form/validator.go index 0673852..b7a24e4 100644 --- a/pkg/form/validator.go +++ b/pkg/form/validator.go @@ -130,12 +130,22 @@ func (v *Validator) CheckDateAfter(field *Input, beforeField *Input, message str return v.Check(field, date.After(before), message) } +func (v *Validator) CheckMinDate(field *Input, min time.Time, message string) bool { + date, _ := time.Parse(database.ISODateFormat, field.Val) + return v.Check(field, !date.Before(min), message) +} + +func (v *Validator) CheckMaxDate(field *Input, max time.Time, message string) bool { + date, _ := time.Parse(database.ISODateFormat, field.Val) + return v.Check(field, !date.After(max), message) +} + func (v *Validator) CheckPasswordConfirmation(password *Input, confirm *Input, message string) bool { return v.Check(confirm, password.Val == confirm.Val, message) } func (v *Validator) CheckSelectedOptions(field *Select, message string) bool { - return v.Check(field, field.validOptionsSelected(), message) + return v.Check(field, field.ValidOptionsSelected(), message) } func (v *Validator) CheckImageFile(field *File, message string) bool { diff --git a/pkg/template/render.go b/pkg/template/render.go index 9cef89e..5682dc4 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -107,11 +107,8 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ "formatDate": func(time time.Time) template.HTML { return template.HTML(`") }, - "today": func() string { - return time.Now().Format(database.ISODateFormat) - }, - "tomorrow": func() string { - return time.Now().AddDate(0, 0, 1).Format(database.ISODateFormat) + "formatDateAttr": func(time time.Time) string { + return time.Format(database.ISODateFormat) }, "queryEscape": func(s string) string { return url.QueryEscape(s) diff --git a/po/ca.po b/po/ca.po index b70cee5..1111d32 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2024-02-04 06:23+0100\n" +"POT-Creation-Date: 2024-02-10 03:29+0100\n" "PO-Revision-Date: 2024-02-06 10:04+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -95,7 +95,7 @@ msgid "The campsite offers many different services." msgstr "El càmping disposa de diversos serveis." #: web/templates/public/amenity.gohtml:39 -#: web/templates/public/campsite/type.gohtml:113 +#: web/templates/public/campsite/type.gohtml:106 #: web/templates/public/campsite/page.gohtml:39 msgctxt "title" msgid "Features" @@ -154,89 +154,79 @@ msgstr "A menys d’una hora de Girona, a una de La Bis msgid "Discover" msgstr "Descobreix" -#: web/templates/public/campsite/type.gohtml:41 -msgctxt "input" -msgid "Check-in Date" -msgstr "Data d’entrada" - -#: web/templates/public/campsite/type.gohtml:47 -msgctxt "input" -msgid "Check-out Date" -msgstr "Data de sortida" - -#: web/templates/public/campsite/type.gohtml:56 -#: web/templates/public/booking/cart.gohtml:25 +#: web/templates/public/campsite/type.gohtml:49 +#: web/templates/public/booking/fields.gohtml:240 msgctxt "action" msgid "Book" msgstr "Reserva" -#: web/templates/public/campsite/type.gohtml:64 +#: web/templates/public/campsite/type.gohtml:57 #: web/templates/admin/season/index.gohtml:54 msgctxt "title" msgid "Calendar" msgstr "Calendari" -#: web/templates/public/campsite/type.gohtml:75 +#: web/templates/public/campsite/type.gohtml:68 #: web/templates/admin/campsite/type/form.gohtml:143 #: web/templates/admin/campsite/type/option/form.gohtml:70 msgctxt "title" msgid "Prices" msgstr "Preus" -#: web/templates/public/campsite/type.gohtml:88 +#: web/templates/public/campsite/type.gohtml:81 msgid "%s: %s/night" msgstr "%s: %s/nit" -#: web/templates/public/campsite/type.gohtml:90 +#: web/templates/public/campsite/type.gohtml:83 msgid "%s/night" msgstr "%s/nit" -#: web/templates/public/campsite/type.gohtml:95 +#: web/templates/public/campsite/type.gohtml:88 msgid "*Minimum %d nights per stay" msgstr "*Mínim %d nits per estada" -#: web/templates/public/campsite/type.gohtml:100 +#: web/templates/public/campsite/type.gohtml:93 msgid "10 % VAT included." msgstr "IVA del 10 % inclòs." -#: web/templates/public/campsite/type.gohtml:101 +#: web/templates/public/campsite/type.gohtml:94 msgid "Tourist tax: %s/night per person aged 17 or older." msgstr "Impost turístic: %s/nit per persona major de 16 anys." -#: web/templates/public/campsite/type.gohtml:103 +#: web/templates/public/campsite/type.gohtml:96 msgid "Dogs: %s/night, tied, accompanied, and minimal barking." msgstr "Gossos: %s/nit, lligats, acompanyats i el mínim de lladrucs." -#: web/templates/public/campsite/type.gohtml:105 +#: web/templates/public/campsite/type.gohtml:98 msgid "No dogs allowed." msgstr "No es permeten gossos." -#: web/templates/public/campsite/type.gohtml:124 +#: web/templates/public/campsite/type.gohtml:117 msgctxt "title" msgid "Info" msgstr "Informació" -#: web/templates/public/campsite/type.gohtml:128 +#: web/templates/public/campsite/type.gohtml:121 msgctxt "title" msgid "Facilities" msgstr "Equipaments" -#: web/templates/public/campsite/type.gohtml:132 +#: web/templates/public/campsite/type.gohtml:125 msgctxt "title" msgid "Description" msgstr "Descripció" -#: web/templates/public/campsite/type.gohtml:136 +#: web/templates/public/campsite/type.gohtml:129 msgctxt "title" msgid "Additional Information" msgstr "Informació addicional" -#: web/templates/public/campsite/type.gohtml:139 +#: web/templates/public/campsite/type.gohtml:132 msgctxt "time" msgid "Check-in" msgstr "Entrada" -#: web/templates/public/campsite/type.gohtml:143 +#: web/templates/public/campsite/type.gohtml:136 msgctxt "time" msgid "Check-out" msgstr "Sortida" @@ -283,6 +273,18 @@ msgctxt "day" msgid "Sun" msgstr "dg" +#: web/templates/public/campsite/dates.gohtml:4 +#: web/templates/public/booking/fields.gohtml:26 +msgctxt "input" +msgid "Arrival date" +msgstr "Data d’arribada" + +#: web/templates/public/campsite/dates.gohtml:15 +#: web/templates/public/booking/fields.gohtml:37 +msgctxt "input" +msgid "Departure date" +msgstr "Data de sortida" + #: web/templates/public/surroundings.gohtml:30 msgctxt "title" msgid "What to Do Outside the Campsite?" @@ -516,8 +518,7 @@ msgid "Campsites" msgstr "Allotjaments" #: web/templates/public/layout.gohtml:70 -#: web/templates/public/booking/form.gohtml:7 -#: web/templates/public/booking/form.gohtml:16 +#: web/templates/public/booking/page.gohtml:7 msgctxt "title" msgid "Booking" msgstr "Reserva" @@ -536,119 +537,113 @@ msgstr "Obertura" msgid "RTC #%s" msgstr "Núm. RTC %s" -#: web/templates/public/booking/form.gohtml:29 +#: web/templates/public/booking/fields.gohtml:13 msgctxt "title" msgid "Accommodation" msgstr "Allotjaments" -#: web/templates/public/booking/form.gohtml:43 +#: web/templates/public/booking/fields.gohtml:23 msgctxt "title" msgid "Booking Period" msgstr "Període de reserva" -#: web/templates/public/booking/form.gohtml:46 -msgctxt "input" -msgid "Arrival date" -msgstr "Data d’arribada" - -#: web/templates/public/booking/form.gohtml:57 -msgctxt "input" -msgid "Departure date" -msgstr "Data de sortida" - -#: web/templates/public/booking/form.gohtml:72 +#: web/templates/public/booking/fields.gohtml:50 msgctxt "title" msgid "Guests" msgstr "Hostes" -#: web/templates/public/booking/form.gohtml:76 +#: web/templates/public/booking/fields.gohtml:54 msgctxt "input" msgid "Adults aged 17 or older" msgstr "Adults de 17 anys o més" -#: web/templates/public/booking/form.gohtml:86 +#: web/templates/public/booking/fields.gohtml:65 msgctxt "input" msgid "Teenagers from 11 to 16 years old" msgstr "Adolescents d’entre 11 i 16 anys" -#: web/templates/public/booking/form.gohtml:96 +#: web/templates/public/booking/fields.gohtml:76 msgctxt "input" msgid "Children from 2 to 10 years old" msgstr "Nens d’entre 2 i 10 anys)" -#: web/templates/public/booking/form.gohtml:106 +#: web/templates/public/booking/fields.gohtml:91 msgctxt "input" msgid "Dogs" msgstr "Gossos" -#: web/templates/public/booking/form.gohtml:127 +#: web/templates/public/booking/fields.gohtml:100 +msgid "Note: This accommodation does not allow dogs." +msgstr "Nota: A aquest allotjament no s’hi permeten gossos." + +#: web/templates/public/booking/fields.gohtml:110 msgctxt "input" msgid "Area preferences (optional)" msgstr "Preferències d’àrea (opcional)" -#: web/templates/public/booking/form.gohtml:129 +#: web/templates/public/booking/fields.gohtml:112 msgid "Campground map" msgstr "Mapa del càmping" -#: web/templates/public/booking/form.gohtml:156 +#: web/templates/public/booking/fields.gohtml:135 msgctxt "title" msgid "Customer Details" msgstr "Detalls del client" -#: web/templates/public/booking/form.gohtml:159 +#: web/templates/public/booking/fields.gohtml:138 msgctxt "input" msgid "Full name" msgstr "Nom i cognoms" -#: web/templates/public/booking/form.gohtml:168 +#: web/templates/public/booking/fields.gohtml:147 msgctxt "input" msgid "Address (optional)" msgstr "Adreça (opcional)" -#: web/templates/public/booking/form.gohtml:177 +#: web/templates/public/booking/fields.gohtml:156 msgctxt "input" msgid "Postcode (optional)" msgstr "Codi postal (opcional)" -#: web/templates/public/booking/form.gohtml:186 +#: web/templates/public/booking/fields.gohtml:165 msgctxt "input" msgid "Town or village (optional)" msgstr "Població (opcional)" -#: web/templates/public/booking/form.gohtml:195 +#: web/templates/public/booking/fields.gohtml:174 #: web/templates/admin/taxDetails.gohtml:101 msgctxt "input" msgid "Country" msgstr "País" -#: web/templates/public/booking/form.gohtml:198 +#: web/templates/public/booking/fields.gohtml:177 msgid "Choose a country" msgstr "Esculli un país" -#: web/templates/public/booking/form.gohtml:206 +#: web/templates/public/booking/fields.gohtml:185 #: web/templates/admin/login.gohtml:27 web/templates/admin/profile.gohtml:38 #: web/templates/admin/taxDetails.gohtml:53 msgctxt "input" msgid "Email" msgstr "Correu-e" -#: web/templates/public/booking/form.gohtml:215 +#: web/templates/public/booking/fields.gohtml:194 #: web/templates/admin/taxDetails.gohtml:45 msgctxt "input" msgid "Phone" msgstr "Telèfon" -#: web/templates/public/booking/form.gohtml:226 +#: web/templates/public/booking/fields.gohtml:205 msgctxt "input" msgid "ACSI card? (optional)" msgstr "Targeta ACSI? (opcional)" -#: web/templates/public/booking/form.gohtml:233 +#: web/templates/public/booking/fields.gohtml:212 msgctxt "input" msgid "I have read and I accept %[1]sthe reservation conditions%[2]s" msgstr "He llegit i accepto %[1]sles condicions de reserves%[2]s" -#: web/templates/public/booking/cart.gohtml:14 +#: web/templates/public/booking/fields.gohtml:229 msgctxt "cart" msgid "Total" msgstr "Total" @@ -1973,12 +1968,12 @@ msgid "Slide image must be an image media type." msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge." #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:217 -#: pkg/booking/public.go:351 +#: pkg/booking/public.go:571 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:218 -#: pkg/booking/public.go:352 +#: pkg/booking/public.go:572 msgid "This email is not valid. It should be like name@domain.com." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com." @@ -2150,22 +2145,23 @@ msgstr "El preu per nen ha de ser un número decimal." msgid "Price per child must be zero or greater." msgstr "El preu per nen ha de ser com a mínim zero." -#: pkg/campsite/types/public.go:229 +#: pkg/campsite/types/public.go:244 msgctxt "header" msgid "Adults" msgstr "Adults" -#: pkg/campsite/types/public.go:235 +#: pkg/campsite/types/public.go:250 msgctxt "header" msgid "Teenagers (aged 11 to 16)" msgstr "Adolescents (entre 11 i 16 anys)" -#: pkg/campsite/types/public.go:241 +#: pkg/campsite/types/public.go:256 msgctxt "header" msgid "Children (aged 2 to 10)" msgstr "Mainada (entre 2 i 10 anys)" -#: pkg/campsite/admin.go:275 pkg/booking/public.go:360 +#: pkg/campsite/admin.go:275 pkg/booking/public.go:217 +#: pkg/booking/public.go:269 msgid "Selected campsite type is not valid." msgstr "El tipus d’allotjament escollit no és vàlid." @@ -2321,7 +2317,7 @@ msgstr "No podeu deixar l’adreça de l’enllaç en blanc." msgid "This web address is not valid. It should be like https://domain.com/." msgstr "Aquesta adreça web no és vàlida. Hauria de ser similar a https://domini.com/." -#: pkg/company/admin.go:200 pkg/booking/public.go:338 +#: pkg/company/admin.go:200 pkg/booking/public.go:558 msgid "Selected country is not valid." msgstr "El país escollit no és vàlid." @@ -2341,11 +2337,11 @@ msgstr "No podeu deixar el NIF en blanc." msgid "This VAT number is not valid." msgstr "Aquest NIF no és vàlid." -#: pkg/company/admin.go:212 pkg/booking/public.go:354 +#: pkg/company/admin.go:212 pkg/booking/public.go:574 msgid "Phone can not be empty." msgstr "No podeu deixar el telèfon en blanc." -#: pkg/company/admin.go:213 pkg/booking/public.go:355 +#: pkg/company/admin.go:213 pkg/booking/public.go:575 msgid "This phone number is not valid." msgstr "Aquest número de telèfon no és vàlid." @@ -2365,7 +2361,7 @@ msgstr "No podeu deixar la província en blanc." msgid "Postal code can not be empty." msgstr "No podeu deixar el codi postal en blanc." -#: pkg/company/admin.go:227 pkg/booking/public.go:347 +#: pkg/company/admin.go:227 pkg/booking/public.go:567 msgid "This postal code is not valid." msgstr "Aquest codi postal no és vàlid." @@ -2405,27 +2401,27 @@ msgstr "No podeu deixar el fitxer del mèdia en blanc." msgid "Filename can not be empty." msgstr "No podeu deixar el nom del fitxer en blanc." -#: pkg/booking/cart.go:171 +#: pkg/booking/cart.go:142 msgctxt "cart" msgid "Night" msgstr "Nit" -#: pkg/booking/cart.go:172 +#: pkg/booking/cart.go:143 msgctxt "cart" msgid "Adult" msgstr "Adult" -#: pkg/booking/cart.go:173 +#: pkg/booking/cart.go:144 msgctxt "cart" msgid "Teenager" msgstr "Adolescent" -#: pkg/booking/cart.go:174 +#: pkg/booking/cart.go:145 msgctxt "cart" msgid "Child" msgstr "Nen" -#: pkg/booking/cart.go:193 +#: pkg/booking/cart.go:164 msgctxt "cart" msgid "Tourist tax" msgstr "Impost turístic" @@ -2487,106 +2483,138 @@ msgstr "La integració escollida no és vàlida." msgid "The merchant key is not valid." msgstr "Aquesta clau del comerç no és vàlid." -#: pkg/booking/public.go:342 -msgid "Full name can not be empty." -msgstr "No podeu deixar el nom i els cognoms en blanc." - -#: pkg/booking/public.go:343 -msgid "Full name must have at least one letter." -msgstr "El nom i els cognoms han de tenir com a mínim una lletra." - -#: pkg/booking/public.go:361 -msgid "Arrival date can not be empty" -msgstr "No podeu deixar la data d’arribada en blanc." - -#: pkg/booking/public.go:362 +#: pkg/booking/public.go:318 pkg/booking/public.go:347 msgid "Arrival date must be a valid date." msgstr "La data d’arribada ha de ser una data vàlida." -#: pkg/booking/public.go:366 -msgid "Departure date can not be empty" -msgstr "No podeu deixar la data de sortida en blanc." - -#: pkg/booking/public.go:367 +#: pkg/booking/public.go:332 pkg/booking/public.go:354 msgid "Departure date must be a valid date." msgstr "La data de sortida ha de ser una data vàlida." -#: pkg/booking/public.go:368 -msgid "The departure date must be after the arrival date." -msgstr "La data de sortida ha de ser posterior a la d’arribada." +#: pkg/booking/public.go:346 +msgid "Arrival date can not be empty" +msgstr "No podeu deixar la data d’arribada en blanc." -#: pkg/booking/public.go:372 +#: pkg/booking/public.go:348 +#, c-format +msgid "Arrival date must be %s or after." +msgstr "La data d’arribada ha de ser igual o posterior a %s." + +#: pkg/booking/public.go:349 +#, c-format +msgid "Arrival date must be %s or before." +msgstr "La data d’arribada ha de ser anterior o igual a %s." + +#: pkg/booking/public.go:353 +msgid "Departure date can not be empty" +msgstr "No podeu deixar la data de sortida en blanc." + +#: pkg/booking/public.go:355 +#, c-format +msgid "Departure date must be %s or after." +msgstr "La data de sortida ha de ser igual o posterior a %s." + +#: pkg/booking/public.go:356 +#, c-format +msgid "Departure date must be %s or before." +msgstr "La data de sortida ha de ser anterior o igual a %s." + +#: pkg/booking/public.go:394 +#, c-format +msgid "There can be at most %d guests in this accommodation." +msgstr "Hi poden haver com a màxim %d convidats a aquest allotjament." + +#: pkg/booking/public.go:413 msgid "Number of adults can not be empty" msgstr "No podeu deixar el número d’adults en blanc." -#: pkg/booking/public.go:373 +#: pkg/booking/public.go:414 msgid "Number of adults must be an integer." msgstr "El número d’adults ha de ser enter." -#: pkg/booking/public.go:374 +#: pkg/booking/public.go:415 msgid "There must be at least one adult." msgstr "Hi ha d’haver com a mínim un adult." -#: pkg/booking/public.go:377 +#: pkg/booking/public.go:418 msgid "Number of teenagers can not be empty" msgstr "No podeu deixar el número d’adolescents en blanc." -#: pkg/booking/public.go:378 +#: pkg/booking/public.go:419 msgid "Number of teenagers must be an integer." msgstr "El número d’adolescents ha de ser enter." -#: pkg/booking/public.go:379 +#: pkg/booking/public.go:420 msgid "Number of teenagers can not be negative." msgstr "El número d’adolescents no pot ser negatiu." -#: pkg/booking/public.go:382 +#: pkg/booking/public.go:423 msgid "Number of children can not be empty" msgstr "No podeu deixar el número de nens en blanc." -#: pkg/booking/public.go:383 +#: pkg/booking/public.go:424 msgid "Number of children must be an integer." msgstr "El número de nens ha de ser enter." -#: pkg/booking/public.go:384 +#: pkg/booking/public.go:425 msgid "Number of children can not be negative." msgstr "El número de nens no pot ser negatiu." -#: pkg/booking/public.go:387 +#: pkg/booking/public.go:428 msgid "Number of dogs can not be empty" msgstr "No podeu deixar el número de gossos en blanc." -#: pkg/booking/public.go:388 +#: pkg/booking/public.go:429 msgid "Number of dogs must be an integer." msgstr "El número de gossos ha de ser enter." -#: pkg/booking/public.go:389 +#: pkg/booking/public.go:430 msgid "Number of dogs can not be negative." msgstr "El número de gossos no pot ser negatiu." -#: pkg/booking/public.go:392 -msgid "It is mandatory to agree to the reservation conditions." -msgstr "És obligatori acceptar les condicions de reserves." - -#: pkg/booking/public.go:395 +#: pkg/booking/public.go:501 #, c-format msgid "%s can not be empty" msgstr "No podeu deixar %s en blanc." -#: pkg/booking/public.go:396 +#: pkg/booking/public.go:502 #, c-format msgid "%s must be an integer." msgstr "%s ha de ser un número enter." -#: pkg/booking/public.go:397 +#: pkg/booking/public.go:503 #, c-format msgid "%s must be %d or greater." msgstr "El valor de %s ha de ser com a mínim %d." -#: pkg/booking/public.go:398 +#: pkg/booking/public.go:504 #, c-format msgid "%s must be at most %d." msgstr "El valor de %s ha de ser com a màxim %d." +#: pkg/booking/public.go:562 +msgid "Full name can not be empty." +msgstr "No podeu deixar el nom i els cognoms en blanc." + +#: pkg/booking/public.go:563 +msgid "Full name must have at least one letter." +msgstr "El nom i els cognoms han de tenir com a mínim una lletra." + +#: pkg/booking/public.go:580 +msgid "It is mandatory to agree to the reservation conditions." +msgstr "És obligatori acceptar les condicions de reserves." + +#~ msgctxt "input" +#~ msgid "Check-in Date" +#~ msgstr "Data d’entrada" + +#~ msgctxt "input" +#~ msgid "Check-out Date" +#~ msgstr "Data de sortida" + +#~ msgid "The departure date must be after the arrival date." +#~ msgstr "La data de sortida ha de ser posterior a la d’arribada." + #~ msgid "Campsite Montagut is an ideal starting point for quiet outings, climbing, swimming in the river and gorges, volcanoes, the Fageda d’en Jordà, cycle tours for all ages…." #~ msgstr "El Càmping Montagut és ideal com a punt de partida d’excursions tranquil·les, escalada, banyar-se en el riu i gorgues, volcans, la Fageda d’en Jordà, sortides amb bicicleta per a tots els nivells…." diff --git a/po/es.po b/po/es.po index c7cb7ec..e3d5867 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2024-02-04 06:23+0100\n" +"POT-Creation-Date: 2024-02-10 03:31+0100\n" "PO-Revision-Date: 2024-02-06 10:04+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -95,7 +95,7 @@ msgid "The campsite offers many different services." msgstr "El camping dispone de varios servicios." #: web/templates/public/amenity.gohtml:39 -#: web/templates/public/campsite/type.gohtml:113 +#: web/templates/public/campsite/type.gohtml:106 #: web/templates/public/campsite/page.gohtml:39 msgctxt "title" msgid "Features" @@ -154,89 +154,79 @@ msgstr "A menos de una hora de Girona, a una de La Bisb msgid "Discover" msgstr "Descubre" -#: web/templates/public/campsite/type.gohtml:41 -msgctxt "input" -msgid "Check-in Date" -msgstr "Fecha de entrada" - -#: web/templates/public/campsite/type.gohtml:47 -msgctxt "input" -msgid "Check-out Date" -msgstr "Fecha de salida" - -#: web/templates/public/campsite/type.gohtml:56 -#: web/templates/public/booking/cart.gohtml:25 +#: web/templates/public/campsite/type.gohtml:49 +#: web/templates/public/booking/fields.gohtml:240 msgctxt "action" msgid "Book" msgstr "Reservar" -#: web/templates/public/campsite/type.gohtml:64 +#: web/templates/public/campsite/type.gohtml:57 #: web/templates/admin/season/index.gohtml:54 msgctxt "title" msgid "Calendar" msgstr "Calendario" -#: web/templates/public/campsite/type.gohtml:75 +#: web/templates/public/campsite/type.gohtml:68 #: web/templates/admin/campsite/type/form.gohtml:143 #: web/templates/admin/campsite/type/option/form.gohtml:70 msgctxt "title" msgid "Prices" msgstr "Precios" -#: web/templates/public/campsite/type.gohtml:88 +#: web/templates/public/campsite/type.gohtml:81 msgid "%s: %s/night" msgstr "%s: %s/noche" -#: web/templates/public/campsite/type.gohtml:90 +#: web/templates/public/campsite/type.gohtml:83 msgid "%s/night" msgstr "%s/noche" -#: web/templates/public/campsite/type.gohtml:95 +#: web/templates/public/campsite/type.gohtml:88 msgid "*Minimum %d nights per stay" msgstr "*Mínimo %d noches por estancia" -#: web/templates/public/campsite/type.gohtml:100 +#: web/templates/public/campsite/type.gohtml:93 msgid "10 % VAT included." msgstr "IVA del 10 % incluido." -#: web/templates/public/campsite/type.gohtml:101 +#: web/templates/public/campsite/type.gohtml:94 msgid "Tourist tax: %s/night per person aged 17 or older." msgstr "Impuesto turístico: %s/noche por persona mayor de 16 años." -#: web/templates/public/campsite/type.gohtml:103 +#: web/templates/public/campsite/type.gohtml:96 msgid "Dogs: %s/night, tied, accompanied, and minimal barking." msgstr "Perros: %s/noche, atados, acompañados y con mínimo de ladrido." -#: web/templates/public/campsite/type.gohtml:105 +#: web/templates/public/campsite/type.gohtml:98 msgid "No dogs allowed." msgstr "No se permiten perros" -#: web/templates/public/campsite/type.gohtml:124 +#: web/templates/public/campsite/type.gohtml:117 msgctxt "title" msgid "Info" msgstr "Información" -#: web/templates/public/campsite/type.gohtml:128 +#: web/templates/public/campsite/type.gohtml:121 msgctxt "title" msgid "Facilities" msgstr "Equipamiento" -#: web/templates/public/campsite/type.gohtml:132 +#: web/templates/public/campsite/type.gohtml:125 msgctxt "title" msgid "Description" msgstr "Descripción" -#: web/templates/public/campsite/type.gohtml:136 +#: web/templates/public/campsite/type.gohtml:129 msgctxt "title" msgid "Additional Information" msgstr "Información adicional" -#: web/templates/public/campsite/type.gohtml:139 +#: web/templates/public/campsite/type.gohtml:132 msgctxt "time" msgid "Check-in" msgstr "Entrada" -#: web/templates/public/campsite/type.gohtml:143 +#: web/templates/public/campsite/type.gohtml:136 msgctxt "time" msgid "Check-out" msgstr "Salida" @@ -283,6 +273,18 @@ msgctxt "day" msgid "Sun" msgstr "do" +#: web/templates/public/campsite/dates.gohtml:4 +#: web/templates/public/booking/fields.gohtml:26 +msgctxt "input" +msgid "Arrival date" +msgstr "Fecha de llegada" + +#: web/templates/public/campsite/dates.gohtml:15 +#: web/templates/public/booking/fields.gohtml:37 +msgctxt "input" +msgid "Departure date" +msgstr "Fecha de salida" + #: web/templates/public/surroundings.gohtml:30 msgctxt "title" msgid "What to Do Outside the Campsite?" @@ -516,8 +518,7 @@ msgid "Campsites" msgstr "Alojamientos" #: web/templates/public/layout.gohtml:70 -#: web/templates/public/booking/form.gohtml:7 -#: web/templates/public/booking/form.gohtml:16 +#: web/templates/public/booking/page.gohtml:7 msgctxt "title" msgid "Booking" msgstr "Reserva" @@ -536,119 +537,113 @@ msgstr "Apertura" msgid "RTC #%s" msgstr " RTC %s" -#: web/templates/public/booking/form.gohtml:29 +#: web/templates/public/booking/fields.gohtml:13 msgctxt "title" msgid "Accommodation" msgstr "Alojamientos" -#: web/templates/public/booking/form.gohtml:43 +#: web/templates/public/booking/fields.gohtml:23 msgctxt "title" msgid "Booking Period" msgstr "Periodo de reserva" -#: web/templates/public/booking/form.gohtml:46 -msgctxt "input" -msgid "Arrival date" -msgstr "Fecha de llegada" - -#: web/templates/public/booking/form.gohtml:57 -msgctxt "input" -msgid "Departure date" -msgstr "Fecha de salida" - -#: web/templates/public/booking/form.gohtml:72 +#: web/templates/public/booking/fields.gohtml:50 msgctxt "title" msgid "Guests" msgstr "Huéspedes" -#: web/templates/public/booking/form.gohtml:76 +#: web/templates/public/booking/fields.gohtml:54 msgctxt "input" msgid "Adults aged 17 or older" msgstr "Adultos de 17 años o más" -#: web/templates/public/booking/form.gohtml:86 +#: web/templates/public/booking/fields.gohtml:65 msgctxt "input" msgid "Teenagers from 11 to 16 years old" msgstr "Adolescentes de 11 a 16 años" -#: web/templates/public/booking/form.gohtml:96 +#: web/templates/public/booking/fields.gohtml:76 msgctxt "input" msgid "Children from 2 to 10 years old" msgstr "Niños de 2 a 10 años" -#: web/templates/public/booking/form.gohtml:106 +#: web/templates/public/booking/fields.gohtml:91 msgctxt "input" msgid "Dogs" msgstr "Perros" -#: web/templates/public/booking/form.gohtml:127 +#: web/templates/public/booking/fields.gohtml:100 +msgid "Note: This accommodation does not allow dogs." +msgstr "Nota: En este alojamiento no\n" "Language-Team: French \n" @@ -95,7 +95,7 @@ msgid "The campsite offers many different services." msgstr "Le camping propose de nombreux services différents." #: web/templates/public/amenity.gohtml:39 -#: web/templates/public/campsite/type.gohtml:113 +#: web/templates/public/campsite/type.gohtml:106 #: web/templates/public/campsite/page.gohtml:39 msgctxt "title" msgid "Features" @@ -154,89 +154,79 @@ msgstr "À moins d’une heure de Gérone, un de La Bis msgid "Discover" msgstr "Découvrir" -#: web/templates/public/campsite/type.gohtml:41 -msgctxt "input" -msgid "Check-in Date" -msgstr "Date d'arrivée" - -#: web/templates/public/campsite/type.gohtml:47 -msgctxt "input" -msgid "Check-out Date" -msgstr "Date de départ" - -#: web/templates/public/campsite/type.gohtml:56 -#: web/templates/public/booking/cart.gohtml:25 +#: web/templates/public/campsite/type.gohtml:49 +#: web/templates/public/booking/fields.gohtml:240 msgctxt "action" msgid "Book" msgstr "Réserver" -#: web/templates/public/campsite/type.gohtml:64 +#: web/templates/public/campsite/type.gohtml:57 #: web/templates/admin/season/index.gohtml:54 msgctxt "title" msgid "Calendar" msgstr "Calendrier" -#: web/templates/public/campsite/type.gohtml:75 +#: web/templates/public/campsite/type.gohtml:68 #: web/templates/admin/campsite/type/form.gohtml:143 #: web/templates/admin/campsite/type/option/form.gohtml:70 msgctxt "title" msgid "Prices" msgstr "Prix" -#: web/templates/public/campsite/type.gohtml:88 +#: web/templates/public/campsite/type.gohtml:81 msgid "%s: %s/night" msgstr "%s : %s/nuit" -#: web/templates/public/campsite/type.gohtml:90 +#: web/templates/public/campsite/type.gohtml:83 msgid "%s/night" msgstr "%s/nuit" -#: web/templates/public/campsite/type.gohtml:95 +#: web/templates/public/campsite/type.gohtml:88 msgid "*Minimum %d nights per stay" msgstr "*Minimum %d nuits par séjour" -#: web/templates/public/campsite/type.gohtml:100 +#: web/templates/public/campsite/type.gohtml:93 msgid "10 % VAT included." msgstr "10 % TVA incluse." -#: web/templates/public/campsite/type.gohtml:101 +#: web/templates/public/campsite/type.gohtml:94 msgid "Tourist tax: %s/night per person aged 17 or older." msgstr "Taxe touristique: %s/nuit par personne de plus de 16 ans." -#: web/templates/public/campsite/type.gohtml:103 +#: web/templates/public/campsite/type.gohtml:96 msgid "Dogs: %s/night, tied, accompanied, and minimal barking." msgstr "Chiens : %s/nuit, attachés, accompagnés et aboiements minimes." -#: web/templates/public/campsite/type.gohtml:105 +#: web/templates/public/campsite/type.gohtml:98 msgid "No dogs allowed." msgstr "Chiens interdits." -#: web/templates/public/campsite/type.gohtml:124 +#: web/templates/public/campsite/type.gohtml:117 msgctxt "title" msgid "Info" msgstr "Info" -#: web/templates/public/campsite/type.gohtml:128 +#: web/templates/public/campsite/type.gohtml:121 msgctxt "title" msgid "Facilities" msgstr "Installations" -#: web/templates/public/campsite/type.gohtml:132 +#: web/templates/public/campsite/type.gohtml:125 msgctxt "title" msgid "Description" msgstr "Description" -#: web/templates/public/campsite/type.gohtml:136 +#: web/templates/public/campsite/type.gohtml:129 msgctxt "title" msgid "Additional Information" msgstr "Informations Complémentaires" -#: web/templates/public/campsite/type.gohtml:139 +#: web/templates/public/campsite/type.gohtml:132 msgctxt "time" msgid "Check-in" msgstr "Arrivée" -#: web/templates/public/campsite/type.gohtml:143 +#: web/templates/public/campsite/type.gohtml:136 msgctxt "time" msgid "Check-out" msgstr "Départ" @@ -283,6 +273,18 @@ msgctxt "day" msgid "Sun" msgstr "Dim." +#: web/templates/public/campsite/dates.gohtml:4 +#: web/templates/public/booking/fields.gohtml:26 +msgctxt "input" +msgid "Arrival date" +msgstr "Date d’arrivée" + +#: web/templates/public/campsite/dates.gohtml:15 +#: web/templates/public/booking/fields.gohtml:37 +msgctxt "input" +msgid "Departure date" +msgstr "Date de depart" + #: web/templates/public/surroundings.gohtml:30 msgctxt "title" msgid "What to Do Outside the Campsite?" @@ -516,8 +518,7 @@ msgid "Campsites" msgstr "Locatifs" #: web/templates/public/layout.gohtml:70 -#: web/templates/public/booking/form.gohtml:7 -#: web/templates/public/booking/form.gohtml:16 +#: web/templates/public/booking/page.gohtml:7 msgctxt "title" msgid "Booking" msgstr "Réservation" @@ -536,119 +537,113 @@ msgstr "Ouverture" msgid "RTC #%s" msgstr "# RTC %s" -#: web/templates/public/booking/form.gohtml:29 +#: web/templates/public/booking/fields.gohtml:13 msgctxt "title" msgid "Accommodation" msgstr "Hébergement" -#: web/templates/public/booking/form.gohtml:43 +#: web/templates/public/booking/fields.gohtml:23 msgctxt "title" msgid "Booking Period" msgstr "Période de réservation" -#: web/templates/public/booking/form.gohtml:46 -msgctxt "input" -msgid "Arrival date" -msgstr "Date d’arrivée" - -#: web/templates/public/booking/form.gohtml:57 -msgctxt "input" -msgid "Departure date" -msgstr "Date de depart" - -#: web/templates/public/booking/form.gohtml:72 +#: web/templates/public/booking/fields.gohtml:50 msgctxt "title" msgid "Guests" msgstr "Personnes logeant" -#: web/templates/public/booking/form.gohtml:76 +#: web/templates/public/booking/fields.gohtml:54 msgctxt "input" msgid "Adults aged 17 or older" msgstr "Adultes âgés 17 ans ou plus" -#: web/templates/public/booking/form.gohtml:86 +#: web/templates/public/booking/fields.gohtml:65 msgctxt "input" msgid "Teenagers from 11 to 16 years old" msgstr "Adolescents de 11 à 16 ans" -#: web/templates/public/booking/form.gohtml:96 +#: web/templates/public/booking/fields.gohtml:76 msgctxt "input" msgid "Children from 2 to 10 years old" msgstr "Enfants de 2 à 10 ans" -#: web/templates/public/booking/form.gohtml:106 +#: web/templates/public/booking/fields.gohtml:91 msgctxt "input" msgid "Dogs" msgstr "Chiens" -#: web/templates/public/booking/form.gohtml:127 +#: web/templates/public/booking/fields.gohtml:100 +msgid "Note: This accommodation does not allow dogs." +msgstr "Remarque: Dans cet hébergement les chiens ne sont pas autorisés." + +#: web/templates/public/booking/fields.gohtml:110 msgctxt "input" msgid "Area preferences (optional)" msgstr "Préférences de zone (facultatif)" -#: web/templates/public/booking/form.gohtml:129 +#: web/templates/public/booking/fields.gohtml:112 msgid "Campground map" msgstr "Plan du camping" -#: web/templates/public/booking/form.gohtml:156 +#: web/templates/public/booking/fields.gohtml:135 msgctxt "title" msgid "Customer Details" msgstr "Détails du client" -#: web/templates/public/booking/form.gohtml:159 +#: web/templates/public/booking/fields.gohtml:138 msgctxt "input" msgid "Full name" msgstr "Nom et prénom" -#: web/templates/public/booking/form.gohtml:168 +#: web/templates/public/booking/fields.gohtml:147 msgctxt "input" msgid "Address (optional)" msgstr "Adresse (Facultatif)" -#: web/templates/public/booking/form.gohtml:177 +#: web/templates/public/booking/fields.gohtml:156 msgctxt "input" msgid "Postcode (optional)" msgstr "Code postal (Facultatif)" -#: web/templates/public/booking/form.gohtml:186 +#: web/templates/public/booking/fields.gohtml:165 msgctxt "input" msgid "Town or village (optional)" msgstr "Ville (Facultatif)" -#: web/templates/public/booking/form.gohtml:195 +#: web/templates/public/booking/fields.gohtml:174 #: web/templates/admin/taxDetails.gohtml:101 msgctxt "input" msgid "Country" msgstr "Pays" -#: web/templates/public/booking/form.gohtml:198 +#: web/templates/public/booking/fields.gohtml:177 msgid "Choose a country" msgstr "Choisissez un pays" -#: web/templates/public/booking/form.gohtml:206 +#: web/templates/public/booking/fields.gohtml:185 #: web/templates/admin/login.gohtml:27 web/templates/admin/profile.gohtml:38 #: web/templates/admin/taxDetails.gohtml:53 msgctxt "input" msgid "Email" msgstr "E-mail" -#: web/templates/public/booking/form.gohtml:215 +#: web/templates/public/booking/fields.gohtml:194 #: web/templates/admin/taxDetails.gohtml:45 msgctxt "input" msgid "Phone" msgstr "Téléphone" -#: web/templates/public/booking/form.gohtml:226 +#: web/templates/public/booking/fields.gohtml:205 msgctxt "input" msgid "ACSI card? (optional)" msgstr "Carte ACSI ? (Facultatif)" -#: web/templates/public/booking/form.gohtml:233 +#: web/templates/public/booking/fields.gohtml:212 msgctxt "input" msgid "I have read and I accept %[1]sthe reservation conditions%[2]s" msgstr "J’ai lu et j’accepte %[1]sles conditions de réservation%[2]s" -#: web/templates/public/booking/cart.gohtml:14 +#: web/templates/public/booking/fields.gohtml:229 msgctxt "cart" msgid "Total" msgstr "Totale" @@ -1973,12 +1968,12 @@ msgid "Slide image must be an image media type." msgstr "L’image de la diapositive doit être de type média d’image." #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:217 -#: pkg/booking/public.go:351 +#: pkg/booking/public.go:571 msgid "Email can not be empty." msgstr "L’e-mail ne peut pas être vide." #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:218 -#: pkg/booking/public.go:352 +#: pkg/booking/public.go:572 msgid "This email is not valid. It should be like name@domain.com." msgstr "Cette adresse e-mail n’est pas valide. Il devrait en être name@domain.com." @@ -2150,22 +2145,23 @@ msgstr "Le prix par enfant doit être un nombre décimal." msgid "Price per child must be zero or greater." msgstr "Le prix par enfant doit être égal ou supérieur." -#: pkg/campsite/types/public.go:229 +#: pkg/campsite/types/public.go:244 msgctxt "header" msgid "Adults" msgstr "Adultes" -#: pkg/campsite/types/public.go:235 +#: pkg/campsite/types/public.go:250 msgctxt "header" msgid "Teenagers (aged 11 to 16)" msgstr "Adolescents (de 11 à 16 anys)" -#: pkg/campsite/types/public.go:241 +#: pkg/campsite/types/public.go:256 msgctxt "header" msgid "Children (aged 2 to 10)" msgstr "Enfants (de 2 à 10 anys)" -#: pkg/campsite/admin.go:275 pkg/booking/public.go:360 +#: pkg/campsite/admin.go:275 pkg/booking/public.go:217 +#: pkg/booking/public.go:269 msgid "Selected campsite type is not valid." msgstr "Le type d’emplacement sélectionné n’est pas valide." @@ -2321,7 +2317,7 @@ msgstr "L’addresse du lien ne peut pas être vide." msgid "This web address is not valid. It should be like https://domain.com/." msgstr "Cette adresse web n’est pas valide. Il devrait en être https://domain.com/." -#: pkg/company/admin.go:200 pkg/booking/public.go:338 +#: pkg/company/admin.go:200 pkg/booking/public.go:558 msgid "Selected country is not valid." msgstr "Le pays sélectionné n’est pas valide." @@ -2341,11 +2337,11 @@ msgstr "Le numéro de TVA ne peut pas être vide." msgid "This VAT number is not valid." msgstr "Ce numéro de TVA n’est pas valide." -#: pkg/company/admin.go:212 pkg/booking/public.go:354 +#: pkg/company/admin.go:212 pkg/booking/public.go:574 msgid "Phone can not be empty." msgstr "Le téléphone ne peut pas être vide." -#: pkg/company/admin.go:213 pkg/booking/public.go:355 +#: pkg/company/admin.go:213 pkg/booking/public.go:575 msgid "This phone number is not valid." msgstr "Ce numéro de téléphone n’est pas valide." @@ -2365,7 +2361,7 @@ msgstr "La province ne peut pas être vide." msgid "Postal code can not be empty." msgstr "Le code postal ne peut pas être vide." -#: pkg/company/admin.go:227 pkg/booking/public.go:347 +#: pkg/company/admin.go:227 pkg/booking/public.go:567 msgid "This postal code is not valid." msgstr "Ce code postal n’est pas valide." @@ -2405,27 +2401,27 @@ msgstr "Le fichier téléchargé ne peut pas être vide." msgid "Filename can not be empty." msgstr "Le nom de fichier ne peut pas être vide." -#: pkg/booking/cart.go:171 +#: pkg/booking/cart.go:142 msgctxt "cart" msgid "Night" msgstr "Nuit" -#: pkg/booking/cart.go:172 +#: pkg/booking/cart.go:143 msgctxt "cart" msgid "Adult" msgstr "Adulte" -#: pkg/booking/cart.go:173 +#: pkg/booking/cart.go:144 msgctxt "cart" msgid "Teenager" msgstr "Adolescent" -#: pkg/booking/cart.go:174 +#: pkg/booking/cart.go:145 msgctxt "cart" msgid "Child" msgstr "Enfant" -#: pkg/booking/cart.go:193 +#: pkg/booking/cart.go:164 msgctxt "cart" msgid "Tourist tax" msgstr "Taxe touristique" @@ -2487,106 +2483,138 @@ msgstr "L’intégration sélectionnée n’est pas valide." msgid "The merchant key is not valid." msgstr "La clé marchand n’est pas valide." -#: pkg/booking/public.go:342 -msgid "Full name can not be empty." -msgstr "Le nom complet ne peut pas être vide." - -#: pkg/booking/public.go:343 -msgid "Full name must have at least one letter." -msgstr "Le nom complet doit comporter au moins une lettre." - -#: pkg/booking/public.go:361 -msgid "Arrival date can not be empty" -msgstr "La date d’arrivée ne peut pas être vide" - -#: pkg/booking/public.go:362 +#: pkg/booking/public.go:318 pkg/booking/public.go:347 msgid "Arrival date must be a valid date." msgstr "La date d’arrivée doit être une date valide." -#: pkg/booking/public.go:366 -msgid "Departure date can not be empty" -msgstr "La date de départ ne peut pas être vide" - -#: pkg/booking/public.go:367 +#: pkg/booking/public.go:332 pkg/booking/public.go:354 msgid "Departure date must be a valid date." msgstr "La date de départ doit être une date valide." -#: pkg/booking/public.go:368 -msgid "The departure date must be after the arrival date." -msgstr "La date de départ doit être postérieure à la date d’arrivée." +#: pkg/booking/public.go:346 +msgid "Arrival date can not be empty" +msgstr "La date d’arrivée ne peut pas être vide" -#: pkg/booking/public.go:372 +#: pkg/booking/public.go:348 +#, c-format +msgid "Arrival date must be %s or after." +msgstr "La date d’arrivée doit être égale ou postérieure à %s." + +#: pkg/booking/public.go:349 +#, c-format +msgid "Arrival date must be %s or before." +msgstr "La date d’arrivée doit être antérieure ou égale à %s." + +#: pkg/booking/public.go:353 +msgid "Departure date can not be empty" +msgstr "La date de départ ne peut pas être vide" + +#: pkg/booking/public.go:355 +#, c-format +msgid "Departure date must be %s or after." +msgstr "La date de départ doit être égale ou postérieure à %s." + +#: pkg/booking/public.go:356 +#, c-format +msgid "Departure date must be %s or before." +msgstr "La date de départ doit être antérieure ou égale à %s." + +#: pkg/booking/public.go:394 +#, c-format +msgid "There can be at most %d guests in this accommodation." +msgstr "Il peut y avoir au plus %d invités dans cet hébergement." + +#: pkg/booking/public.go:413 msgid "Number of adults can not be empty" msgstr "Le nombre d’adultes ne peut pas être vide." -#: pkg/booking/public.go:373 +#: pkg/booking/public.go:414 msgid "Number of adults must be an integer." msgstr "Le nombre d’adultes doit être un entier." -#: pkg/booking/public.go:374 +#: pkg/booking/public.go:415 msgid "There must be at least one adult." msgstr "Il doit y avoir au moins un adulte." -#: pkg/booking/public.go:377 +#: pkg/booking/public.go:418 msgid "Number of teenagers can not be empty" msgstr "Le nombre d’adolescents ne peut pas être vide." -#: pkg/booking/public.go:378 +#: pkg/booking/public.go:419 msgid "Number of teenagers must be an integer." msgstr "Le nombre d’adolescents doit être un entier." -#: pkg/booking/public.go:379 +#: pkg/booking/public.go:420 msgid "Number of teenagers can not be negative." msgstr "Le nombre d’adolescents ne peut pas être négatif." -#: pkg/booking/public.go:382 +#: pkg/booking/public.go:423 msgid "Number of children can not be empty" msgstr "Le nombre d’enfants ne peut pas être vide." -#: pkg/booking/public.go:383 +#: pkg/booking/public.go:424 msgid "Number of children must be an integer." msgstr "Le nombre d’enfants doit être un entier." -#: pkg/booking/public.go:384 +#: pkg/booking/public.go:425 msgid "Number of children can not be negative." msgstr "Le nombre d’enfants ne peut pas être négatif." -#: pkg/booking/public.go:387 +#: pkg/booking/public.go:428 msgid "Number of dogs can not be empty" msgstr "Le nombre de chiens ne peut pas être vide." -#: pkg/booking/public.go:388 +#: pkg/booking/public.go:429 msgid "Number of dogs must be an integer." msgstr "Le nombre de chiens nuits être un entier." -#: pkg/booking/public.go:389 +#: pkg/booking/public.go:430 msgid "Number of dogs can not be negative." msgstr "Le nombre de chiens ne peut pas être négatif." -#: pkg/booking/public.go:392 -msgid "It is mandatory to agree to the reservation conditions." -msgstr "Il est obligatoire d’accepter les conditions de réservation." - -#: pkg/booking/public.go:395 +#: pkg/booking/public.go:501 #, c-format msgid "%s can not be empty" msgstr "%s ne peut pas être vide" -#: pkg/booking/public.go:396 +#: pkg/booking/public.go:502 #, c-format msgid "%s must be an integer." msgstr "%s doit être un entier." -#: pkg/booking/public.go:397 +#: pkg/booking/public.go:503 #, c-format msgid "%s must be %d or greater." msgstr "%s doit être %d ou plus." -#: pkg/booking/public.go:398 +#: pkg/booking/public.go:504 #, c-format msgid "%s must be at most %d." msgstr "%s doit être tout au plus %d." +#: pkg/booking/public.go:562 +msgid "Full name can not be empty." +msgstr "Le nom complet ne peut pas être vide." + +#: pkg/booking/public.go:563 +msgid "Full name must have at least one letter." +msgstr "Le nom complet doit comporter au moins une lettre." + +#: pkg/booking/public.go:580 +msgid "It is mandatory to agree to the reservation conditions." +msgstr "Il est obligatoire d’accepter les conditions de réservation." + +#~ msgctxt "input" +#~ msgid "Check-in Date" +#~ msgstr "Date d'arrivée" + +#~ msgctxt "input" +#~ msgid "Check-out Date" +#~ msgstr "Date de départ" + +#~ msgid "The departure date must be after the arrival date." +#~ msgstr "La date de départ doit être postérieure à la date d’arrivée." + #~ msgid "Campsite Montagut is an ideal starting point for quiet outings, climbing, swimming in the river and gorges, volcanoes, the Fageda d’en Jordà, cycle tours for all ages…." #~ msgstr "Le camping Montagut est un point de départ idéal pour des sorties tranquilles, de l’escalade, des baignades dans la rivière et les gorges, les volcans, la Fageda dâen Jordã, des randonnées à vélo pour tous les âges…." diff --git a/web/static/booking-dates.js b/web/static/booking-dates.js deleted file mode 100644 index 3d1a40b..0000000 --- a/web/static/booking-dates.js +++ /dev/null @@ -1,58 +0,0 @@ -(function () { - 'use strict'; - - function updateDepartureDate(arrivalDateField) { - const arrivalDate = new Date(arrivalDateField.value); - if (isNaN(arrivalDate.getTime())) { - return; - } - const departureDateField = document.querySelector('[name="departure_date"]'); - if (!departureDateField) { - return; - } - - function updateDepartureDate(date) { - departureDateField.value = date; - departureDateField.dispatchEvent(new Event('input', {bubbles: true})); - } - - const minNights = Math.max(1, parseInt(departureDateField.dataset.minNights, 10) || 0); - arrivalDate.setUTCDate(arrivalDate.getUTCDate() + minNights); - const minDate = formatDate(arrivalDate); - - departureDateField.setAttribute('min', minDate); - const departureDate = new Date(departureDateField.value); - const validDepartureDate = !isNaN(departureDate.getTime()) - if (!validDepartureDate || departureDate < arrivalDate) { - updateDepartureDate(minDate); - } - - const maxNights = parseInt(departureDateField.dataset.maxNights, 10) || 0; - if (maxNights > 0) { - arrivalDate.setUTCDate(arrivalDate.getUTCDate() + maxNights - minNights); - const maxDate = formatDate(arrivalDate); - departureDateField.setAttribute('max', maxDate); - if (validDepartureDate && departureDate >= arrivalDate) { - updateDepartureDate(maxDate); - } - } - } - - function formatDate(date) { - return `${date.getFullYear()}-${zeroPad(date.getMonth() + 1)}-${zeroPad(date.getDate())}`; - } - - function zeroPad(num) { - return `${num < 10 ? '0' : ''}${num}` - } - - const arrivalDateField = document.querySelector('[name="arrival_date"]'); - if (!arrivalDateField) { - return; - } - arrivalDateField.addEventListener('change', function (event) { - updateDepartureDate(event.target); - }); - - updateDepartureDate(arrivalDateField); -})(); diff --git a/web/static/public.css b/web/static/public.css index e4c846a..5867016 100644 --- a/web/static/public.css +++ b/web/static/public.css @@ -673,8 +673,8 @@ dl, .nature > div, .outside_activities > div, .campsite_info { @media (max-width: 48rem) { .campsite_type_title { - padding-bottom: 6rem; - } + padding-bottom: 6rem; + } .carousel { overflow: unset; @@ -1290,6 +1290,10 @@ button { color: var(--contrast); } +button[disabled] { + background-color: var(--contrast-3); + border-color: var(--contrast-3); +} /* radio buttins + checkbox */ @@ -1371,14 +1375,6 @@ input[type="checkbox"]:focus { flex: .33; position: sticky; top: 13rem; - opacity: 0; - visibility: hidden; - transition: opacity .5s ease; -} - -#booking > footer.is-visible { - opacity: 1; - visibility: visible; } #booking br { diff --git a/web/templates/public/booking/cart.gohtml b/web/templates/public/booking/cart.gohtml deleted file mode 100644 index 9dc7b17..0000000 --- a/web/templates/public/booking/cart.gohtml +++ /dev/null @@ -1,25 +0,0 @@ - -{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.bookingCart */ -}} -
- {{ range .Lines -}} -
-
{{ .Units}} x {{( pgettext .Concept "cart" )}}
-
{{ formatPrice .Subtotal }}
-
- {{- end }} -
-
{{( pgettext "Total" "cart" )}}
-
{{ formatPrice .Total }}
-
-
-
- - VISA - MasterCard - Maestro - -
- diff --git a/web/templates/public/booking/fields.gohtml b/web/templates/public/booking/fields.gohtml new file mode 100644 index 0000000..20fe55e --- /dev/null +++ b/web/templates/public/booking/fields.gohtml @@ -0,0 +1,244 @@ + +{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.publicPage*/ -}} +{{ with .Form -}} +
+
+ {{( pgettext "Accommodation" "title" )}} + {{ range .CampsiteType.Options -}} +
+ {{- end }} + {{ template "error-message" .CampsiteType }} +
+ {{ with .Dates -}} +
+ {{( pgettext "Booking Period" "title" )}} + {{ with .ArrivalDate -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .DepartureDate -}} + + {{ template "error-message" . }} + {{- end }} +
+ {{- end }} + {{ with $guests := .Guests -}} +
+ {{( pgettext "Guests" "title" )}} + + {{ with .NumberAdults -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .NumberTeenagers -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .NumberChildren -}} + + {{ template "error-message" . }} + {{- end }} + {{ if .Error -}} +

{{ .Error }}

+ {{- end }} + {{ if .NumberDogs -}} + {{ with .NumberDogs -}} + + {{ template "error-message" . }} + {{- end }} + {{- else -}} + {{( gettext "Note: This accommodation does not allow dogs.") | raw }} + {{- end }} +
+ {{- end }} + {{ with .Options -}} +
+ {{ .Legend }} + {{ with .ZonePreferences -}} + + {{ template "error-message" . }} + {{- end }} + {{ range .Options -}} + + {{ template "error-message" .Input }} + {{- end }} +
+ {{- end }} + {{ with .Customer -}} +
+ {{( pgettext "Customer Details" "title" )}} + {{ with .FullName -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Address -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .PostalCode -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .City -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Country -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Email -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Phone -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .ACSICard -}} +
+ {{ template "error-message" . }} + {{- end }} + {{ with .Agreement -}} +
+ {{ template "error-message" . }} + {{- end }} +
+ {{- end }} +
+ {{ with .Cart -}} +
+
+ {{ range .Lines -}} +
+
{{ .Units}} x {{( pgettext .Concept "cart" )}}
+
{{ formatPrice .Subtotal }}
+
+ {{- end }} +
+
{{( pgettext "Total" "cart" )}}
+
{{ formatPrice .Total }}
+
+
+
+ + VISA + MasterCard + Maestro + +
+ +
+ {{- end }} +{{- end }} diff --git a/web/templates/public/booking/form.gohtml b/web/templates/public/booking/form.gohtml deleted file mode 100644 index c5360d7..0000000 --- a/web/templates/public/booking/form.gohtml +++ /dev/null @@ -1,245 +0,0 @@ - -{{ define "title" -}} - {{( pgettext "Booking" "title" )}} -{{- end }} - -{{ define "head" -}} - {{ template "alpineScript" }} -{{- end }} - -{{ define "content" -}} - {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.publicPage*/ -}} -

{{( pgettext "Booking" "title" )}}

- {{ with .Form -}} -
-
-
- {{( pgettext "Accommodation" "title" )}} - {{ range .CampsiteType.Options -}} -
- {{- end }} - {{ template "error-message" .CampsiteType }} -
-
- {{( pgettext "Booking Period" "title" )}} - {{ with .ArrivalDate -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .DepartureDate -}} - - {{ template "error-message" . }} - {{- end }} -
-
- {{( pgettext "Guests" "title" )}} - - {{ with .NumberAdults -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .NumberTeenagers -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .NumberChildren -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .NumberDogs -}} - - {{ template "error-message" . }} - {{- end }} -
- {{ range $campsiteType := .CampsiteType.Options -}} - {{ $options := index $.Form.CampsiteTypeOptions .Value }} - {{ $zonePreferences := index $.Form.ZonePreferences .Value }} - {{ if or $options $zonePreferences }} -
- {{ .Label }} - {{ with $zonePreferences -}} - - {{ template "error-message" . }} - {{- end }} - {{ range $options -}} - - {{ template "error-message" .Input }} - {{- end }} -
- {{- end }} - {{- end }} -
- {{( pgettext "Customer Details" "title" )}} - {{ with .FullName -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .Address -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .PostalCode -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .City -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .Country -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .Email -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .Phone -}} - - {{ template "error-message" . }} - {{- end }} - {{ with .ACSICard -}} -
- {{ template "error-message" . }} - {{- end }} - {{ with .Agreement -}} -
- {{ template "error-message" . }} - {{- end }} -
-
-
- {{ template "cart.gohtml" $.Cart }} -
-
- {{- end }} - -{{- end }} diff --git a/web/templates/public/booking/page.gohtml b/web/templates/public/booking/page.gohtml new file mode 100644 index 0000000..af215a0 --- /dev/null +++ b/web/templates/public/booking/page.gohtml @@ -0,0 +1,20 @@ + +{{ define "title" -}} + {{( pgettext "Booking" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.publicPage*/ -}} +

{{ template "title" . }}

+
+ {{ template "fields.gohtml" . }} +
+{{- end }} diff --git a/web/templates/public/campsite/dates.gohtml b/web/templates/public/campsite/dates.gohtml new file mode 100644 index 0000000..8578bca --- /dev/null +++ b/web/templates/public/campsite/dates.gohtml @@ -0,0 +1,23 @@ +{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.DateFields*/ -}} +{{ with .ArrivalDate -}} + + {{ template "error-message" . }} +{{- end }} +{{ with .DepartureDate -}} + + {{ template "error-message" . }} +{{- end }} diff --git a/web/templates/public/campsite/type.gohtml b/web/templates/public/campsite/type.gohtml index 27da167..1357cb0 100644 --- a/web/templates/public/campsite/type.gohtml +++ b/web/templates/public/campsite/type.gohtml @@ -34,23 +34,16 @@ {{- end }} -
+ -
- - +
+ {{ template "dates.gohtml" .BookingDates }}