Allow updating bookings
I need to retrieve the values from the database and put them in the form, like all other forms, but in this case the processing is done as if it were a new form, because everything comes from the query string and there is no need to do any extra work then. Had to move the <footer> from the fields.gohtml to form.gohtml because then it could not know that it was editing an existing booking. Had to move the <fieldset> out too, in order to give it an ID and make it htmx’s target, or it would replace the form, causing even more problems —the button would disappear then—. The target **must** be in <form> because it is needed for tis children’s hx-get and for its own hx-put.
This commit is contained in:
parent
30e87c309e
commit
c9e8165f83
|
@ -0,0 +1,92 @@
|
|||
-- Deploy camper:edit_booking_from_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: booking__payment_fields
|
||||
-- requires: booking__stay
|
||||
-- requires: booking_option
|
||||
-- requires: payment
|
||||
-- requires: payment__acsi_card
|
||||
-- requires: payment_option
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function edit_booking_from_payment(booking_slug uuid, payment_slug uuid) returns integer as
|
||||
$$
|
||||
declare
|
||||
bid integer;
|
||||
begin
|
||||
with p as (
|
||||
select company_id
|
||||
, campsite_type_id
|
||||
, daterange(arrival_date, departure_date) as stay
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
from payment
|
||||
where payment.slug = payment_slug
|
||||
)
|
||||
update booking
|
||||
set company_id = p.company_id
|
||||
, campsite_type_id = p.campsite_type_id
|
||||
, stay = p.stay
|
||||
, subtotal_nights = p.subtotal_nights
|
||||
, number_adults = p.number_adults
|
||||
, subtotal_adults = p.subtotal_adults
|
||||
, number_teenagers = p.number_teenagers
|
||||
, subtotal_teenagers = p.subtotal_teenagers
|
||||
, number_children = p.number_children
|
||||
, subtotal_children = p.subtotal_children
|
||||
, number_dogs = p.number_dogs
|
||||
, subtotal_dogs = p.subtotal_dogs
|
||||
, subtotal_tourist_tax = p.subtotal_tourist_tax
|
||||
, total = p.total
|
||||
, currency_code = p.currency_code
|
||||
, zone_preferences = p.zone_preferences
|
||||
, acsi_card = p.acsi_card
|
||||
from p
|
||||
where slug = booking_slug
|
||||
returning booking_id into bid;
|
||||
|
||||
delete from booking_option
|
||||
where booking_id = bid;
|
||||
|
||||
insert into booking_option
|
||||
( booking_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
)
|
||||
select bid
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
from payment_option
|
||||
join payment using (payment_id)
|
||||
where payment.slug = payment_slug
|
||||
;
|
||||
|
||||
return bid;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_booking_from_payment(uuid, uuid) from public;
|
||||
grant execute on function edit_booking_from_payment(uuid, uuid) to employee;
|
||||
grant execute on function edit_booking_from_payment(uuid, uuid) to admin;
|
||||
|
||||
commit;
|
|
@ -21,6 +21,7 @@ import (
|
|||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
|
@ -48,13 +49,69 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
|||
case "new":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveAdminBookingForm(w, r, user, company, conn, 0, "/admin/bookings/new")
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
default:
|
||||
if !uuid.Valid(head) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
h.bookingHandler(user, company, conn, head).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func serveAdminBookingForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, id int, url string) {
|
||||
f, err := newAdminBookingForm(r, conn, company, user.Locale)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.ID = id
|
||||
f.URL = url
|
||||
f.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) bookingHandler(user *auth.User, company *auth.Company, conn *database.Conn, slug string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if err := r.ParseForm(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(r.Form) > 0 {
|
||||
// Act as if it was a new form, because everything needed to render form fields is
|
||||
// already passed as in request query.
|
||||
id, err := conn.GetInt(r.Context(), "select booking_id from booking where slug = $1", slug)
|
||||
if err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
serveAdminBookingForm(w, r, user, company, conn, id, "/admin/bookings/"+slug)
|
||||
return
|
||||
}
|
||||
f := newEmptyAdminBookingForm(r.Context(), conn, company, user.Locale)
|
||||
if err := f.FillFromDatabase(r.Context(), conn, company, slug, user.Locale); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
case http.MethodPut:
|
||||
updateBooking(w, r, user, company, conn, slug)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
|
@ -150,12 +207,19 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
|
|||
type adminBookingForm struct {
|
||||
*bookingForm
|
||||
ID int
|
||||
URL string
|
||||
Campsites []*CampsiteEntry
|
||||
selected []int
|
||||
Months []*Month
|
||||
Error error
|
||||
}
|
||||
|
||||
func newEmptyAdminBookingForm(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) *adminBookingForm {
|
||||
return &adminBookingForm{
|
||||
bookingForm: newEmptyBookingForm(ctx, conn, company, l),
|
||||
}
|
||||
}
|
||||
|
||||
func newAdminBookingForm(r *http.Request, conn *database.Conn, company *auth.Company, l *locale.Locale) (*adminBookingForm, error) {
|
||||
inner, err := newBookingForm(r, company, conn, l)
|
||||
if err != nil {
|
||||
|
@ -171,17 +235,26 @@ func newAdminBookingForm(r *http.Request, conn *database.Conn, company *auth.Com
|
|||
}
|
||||
// Dates and Campsite are valid
|
||||
if inner.Guests != nil {
|
||||
arrivalDate, _ := time.Parse(database.ISODateFormat, inner.Dates.ArrivalDate.Val)
|
||||
from := arrivalDate.AddDate(0, 0, -1)
|
||||
departureDate, _ := time.Parse(database.ISODateFormat, inner.Dates.DepartureDate.Val)
|
||||
to := departureDate.AddDate(0, 0, 2)
|
||||
f.Months = CollectMonths(from, to)
|
||||
f.Campsites, err = CollectCampsiteEntries(r.Context(), company, conn, from, to, inner.CampsiteType.String())
|
||||
if err != nil {
|
||||
selected := r.Form["campsite"]
|
||||
if err = f.FetchCampsites(r.Context(), conn, company, selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *adminBookingForm) FetchCampsites(ctx context.Context, conn *database.Conn, company *auth.Company, selected []string) error {
|
||||
arrivalDate, _ := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
|
||||
from := arrivalDate.AddDate(0, 0, -1)
|
||||
departureDate, _ := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
|
||||
to := departureDate.AddDate(0, 0, 2)
|
||||
f.Months = CollectMonths(from, to)
|
||||
var err error
|
||||
f.Campsites, err = CollectCampsiteEntries(ctx, company, conn, from, to, f.CampsiteType.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selected := r.Form["campsite"]
|
||||
for _, s := range selected {
|
||||
ID, _ := strconv.Atoi(s)
|
||||
for _, c := range f.Campsites {
|
||||
|
@ -192,8 +265,8 @@ func newAdminBookingForm(r *http.Request, conn *database.Conn, company *auth.Com
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findSubtotal(ID int, cart *bookingCart) string {
|
||||
|
@ -218,48 +291,15 @@ func (f *adminBookingForm) MustRender(w http.ResponseWriter, r *http.Request, us
|
|||
}
|
||||
|
||||
func addBooking(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
f, err := newAdminBookingForm(r, conn, company, user.Locale)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
tx := conn.MustBegin(r.Context())
|
||||
if err := performAddBooking(r.Context(), tx, f); err == nil {
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.Rollback(r.Context()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
httplib.Redirect(w, r, "/admin/bookings", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func performAddBooking(ctx context.Context, tx *database.Tx, f *adminBookingForm) error {
|
||||
var bookingID int
|
||||
processAdminBookingForm(w, r, user, company, conn, 0, func(ctx context.Context, tx *database.Tx, f *adminBookingForm) error {
|
||||
var err error
|
||||
bookingID, err = tx.AddBookingFromPayment(ctx, f.PaymentSlug.Val)
|
||||
f.ID, err = tx.AddBookingFromPayment(ctx, f.PaymentSlug.Val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.EditBooking(
|
||||
ctx,
|
||||
bookingID,
|
||||
f.ID,
|
||||
f.Customer.FullName.Val,
|
||||
f.Customer.Address.Val,
|
||||
f.Customer.PostalCode.Val,
|
||||
|
@ -271,6 +311,71 @@ func performAddBooking(ctx context.Context, tx *database.Tx, f *adminBookingForm
|
|||
"confirmed",
|
||||
f.selected,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func updateBooking(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
|
||||
var bookingID int
|
||||
var bookingStatus string
|
||||
var langTag string
|
||||
row := conn.QueryRow(r.Context(), "select booking_id, booking_status, lang_tag from booking where slug = $1", slug)
|
||||
if err := row.Scan(&bookingID, &bookingStatus, &langTag); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
processAdminBookingForm(w, r, user, company, conn, bookingID, func(ctx context.Context, tx *database.Tx, f *adminBookingForm) error {
|
||||
var err error
|
||||
_, err = tx.EditBookingFromPayment(ctx, slug, f.PaymentSlug.Val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.EditBooking(
|
||||
ctx,
|
||||
f.ID,
|
||||
f.Customer.FullName.Val,
|
||||
f.Customer.Address.Val,
|
||||
f.Customer.PostalCode.Val,
|
||||
f.Customer.City.Val,
|
||||
f.Customer.Country.String(),
|
||||
f.Customer.Email.Val,
|
||||
f.Customer.Phone.Val,
|
||||
language.Make(langTag),
|
||||
bookingStatus,
|
||||
f.selected,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func processAdminBookingForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, bookingID int, act func(ctx context.Context, tx *database.Tx, f *adminBookingForm) error) {
|
||||
f, err := newAdminBookingForm(r, conn, company, user.Locale)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := user.VerifyCSRFToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
f.ID = bookingID
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
tx := conn.MustBegin(r.Context())
|
||||
defer tx.Rollback(r.Context())
|
||||
if err := act(r.Context(), tx, f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tx.MustCommit(r.Context())
|
||||
httplib.Redirect(w, r, "/admin/bookings", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (f *adminBookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
|
||||
|
@ -327,7 +432,7 @@ func (f *adminBookingForm) Valid(ctx context.Context, conn *database.Conn, l *lo
|
|||
f.Error = errors.New(l.Gettext("You must select at least one accommodation."))
|
||||
v.AllOK = false
|
||||
} else if f.Dates.ArrivalDate.Error == nil && f.Dates.DepartureDate.Error == nil {
|
||||
if available, err := datesAvailable(ctx, conn, f.Dates, f.selected); err != nil {
|
||||
if available, err := datesAvailable(ctx, conn, f.ID, f.Dates, f.selected); err != nil {
|
||||
return false, err
|
||||
} else if !available {
|
||||
f.Error = errors.New(l.Gettext("The selected accommodations have no available openings in the requested dates."))
|
||||
|
@ -338,6 +443,162 @@ func (f *adminBookingForm) Valid(ctx context.Context, conn *database.Conn, l *lo
|
|||
return v.AllOK, nil
|
||||
}
|
||||
|
||||
func datesAvailable(ctx context.Context, conn *database.Conn, dates *DateFields, selectedCampsites []int) (bool, error) {
|
||||
return conn.GetBool(ctx, "select not exists (select 1 from camper.booking_campsite where campsite_id = any ($3) and stay && daterange($1::date, $2::date))", dates.ArrivalDate, dates.DepartureDate, selectedCampsites)
|
||||
func datesAvailable(ctx context.Context, conn *database.Conn, bookingID int, dates *DateFields, selectedCampsites []int) (bool, error) {
|
||||
return conn.GetBool(ctx, `
|
||||
select not exists (
|
||||
select 1
|
||||
from camper.booking_campsite
|
||||
where booking_id <> $1
|
||||
and campsite_id = any ($4)
|
||||
and stay && daterange($2::date, $3::date)
|
||||
)
|
||||
`,
|
||||
bookingID,
|
||||
dates.ArrivalDate,
|
||||
dates.DepartureDate,
|
||||
selectedCampsites,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *adminBookingForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, slug string, l *locale.Locale) error {
|
||||
f.Cart = &bookingCart{Draft: &paymentDraft{}}
|
||||
f.Customer = newBookingCustomerFields(ctx, conn, l)
|
||||
var arrivalDate string
|
||||
var departureDate string
|
||||
var acsiCard bool
|
||||
var zonePreferences string
|
||||
var selected []string
|
||||
row := conn.QueryRow(ctx, `
|
||||
select booking_id
|
||||
, '/admin/bookings/' || booking.slug
|
||||
, array[campsite_type.slug::text]
|
||||
, lower(booking.stay)::text
|
||||
, upper(booking.stay)::text
|
||||
, upper(booking.stay) - lower(booking.stay)
|
||||
, to_price(subtotal_nights, decimal_digits)
|
||||
, number_adults
|
||||
, to_price(subtotal_adults, decimal_digits)
|
||||
, number_teenagers
|
||||
, to_price(subtotal_teenagers, decimal_digits)
|
||||
, number_children
|
||||
, to_price(subtotal_children, decimal_digits)
|
||||
, number_dogs
|
||||
, to_price(subtotal_dogs, decimal_digits)
|
||||
, to_price(subtotal_tourist_tax, decimal_digits)
|
||||
, to_price(total, decimal_digits)
|
||||
, acsi_card
|
||||
, holder_name
|
||||
, coalesce(address, '')
|
||||
, coalesce(postal_code, '')
|
||||
, coalesce(city, '')
|
||||
, array[coalesce(country_code::text, '')]
|
||||
, coalesce(email::text, '')
|
||||
, coalesce(phone::text, '')
|
||||
, zone_preferences
|
||||
, array_agg(coalesce(campsite_id::text, ''))
|
||||
from booking
|
||||
join campsite_type using (campsite_type_id)
|
||||
left join campsite_type_pet_cost as pet using (campsite_type_id)
|
||||
left join booking_campsite using (booking_id)
|
||||
join currency using (currency_code)
|
||||
where booking.slug = $1
|
||||
group by booking_id
|
||||
, campsite_type.slug
|
||||
, booking.stay
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, acsi_card
|
||||
, holder_name
|
||||
, address
|
||||
, postal_code
|
||||
, city
|
||||
, country_code
|
||||
, email
|
||||
, phone
|
||||
, zone_preferences
|
||||
, decimal_digits
|
||||
`, slug)
|
||||
if err := row.Scan(
|
||||
&f.ID,
|
||||
&f.URL,
|
||||
&f.CampsiteType.Selected,
|
||||
&arrivalDate,
|
||||
&departureDate,
|
||||
&f.Cart.Draft.NumNights,
|
||||
&f.Cart.Draft.Nights,
|
||||
&f.Cart.Draft.NumAdults,
|
||||
&f.Cart.Draft.Adults,
|
||||
&f.Cart.Draft.NumTeenagers,
|
||||
&f.Cart.Draft.Teenagers,
|
||||
&f.Cart.Draft.NumChildren,
|
||||
&f.Cart.Draft.Children,
|
||||
&f.Cart.Draft.NumDogs,
|
||||
&f.Cart.Draft.Dogs,
|
||||
&f.Cart.Draft.TouristTax,
|
||||
&f.Cart.Draft.Total,
|
||||
&acsiCard,
|
||||
&f.Customer.FullName.Val,
|
||||
&f.Customer.Address.Val,
|
||||
&f.Customer.PostalCode.Val,
|
||||
&f.Customer.City.Val,
|
||||
&f.Customer.Country.Selected,
|
||||
&f.Customer.Email.Val,
|
||||
&f.Customer.Phone.Val,
|
||||
&zonePreferences,
|
||||
&selected,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
f.Dates, err = NewDateFields(ctx, conn, f.CampsiteType.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Dates.ArrivalDate.Val = arrivalDate
|
||||
f.Dates.DepartureDate.Val = departureDate
|
||||
f.Dates.AdjustValues(l)
|
||||
|
||||
f.Guests, err = newBookingGuestFields(ctx, conn, f.CampsiteType.String(), arrivalDate, departureDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Guests.NumberAdults.Val = strconv.Itoa(f.Cart.Draft.NumAdults)
|
||||
f.Guests.NumberTeenagers.Val = strconv.Itoa(f.Cart.Draft.NumTeenagers)
|
||||
f.Guests.NumberChildren.Val = strconv.Itoa(f.Cart.Draft.NumChildren)
|
||||
if f.Guests.NumberDogs != nil {
|
||||
f.Guests.NumberDogs.Val = strconv.Itoa(f.Cart.Draft.NumDogs)
|
||||
}
|
||||
if f.Guests.ACSICard != nil {
|
||||
f.Guests.ACSICard.Checked = acsiCard
|
||||
}
|
||||
f.Guests.AdjustValues(f.Cart.Draft.NumAdults+f.Cart.Draft.NumTeenagers+f.Cart.Draft.NumChildren, l)
|
||||
|
||||
f.Options, err = newBookingOptionFields(ctx, conn, f.CampsiteType.String(), l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.Options != nil {
|
||||
if f.Options.ZonePreferences != nil {
|
||||
f.Options.ZonePreferences.Val = zonePreferences
|
||||
}
|
||||
if err = f.Options.FillFromDatabase(ctx, conn, f.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = f.FetchCampsites(ctx, conn, company, selected); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -149,20 +149,24 @@ type bookingCustomerFields struct {
|
|||
Agreement *form.Checkbox
|
||||
}
|
||||
|
||||
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{
|
||||
func newEmptyBookingForm(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) *bookingForm {
|
||||
return &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),
|
||||
Options: form.MustGetOptions(ctx, 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),
|
||||
},
|
||||
PaymentSlug: &form.Input{
|
||||
Name: "payment_slug",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 := newEmptyBookingForm(r.Context(), conn, company, l)
|
||||
f.CampsiteType.FillValue(r)
|
||||
f.PaymentSlug.FillValue(r)
|
||||
campsiteType := f.CampsiteType.String()
|
||||
|
@ -270,7 +274,10 @@ func NewDateFields(ctx context.Context, conn *database.Conn, campsiteType string
|
|||
func (f *DateFields) FillValues(r *http.Request, l *locale.Locale) {
|
||||
f.ArrivalDate.FillValue(r)
|
||||
f.DepartureDate.FillValue(r)
|
||||
f.AdjustValues(l)
|
||||
}
|
||||
|
||||
func (f *DateFields) AdjustValues(l *locale.Locale) {
|
||||
if f.ArrivalDate.Val != "" {
|
||||
arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val)
|
||||
if err != nil {
|
||||
|
@ -362,6 +369,10 @@ func (f *bookingGuestFields) FillValues(r *http.Request, l *locale.Locale) {
|
|||
if f.ACSICard != nil {
|
||||
f.ACSICard.FillValue(r)
|
||||
}
|
||||
f.AdjustValues(numGuests, l)
|
||||
}
|
||||
|
||||
func (f *bookingGuestFields) AdjustValues(numGuests int, l *locale.Locale) {
|
||||
if numGuests > f.MaxGuests {
|
||||
if f.OverflowAllowed {
|
||||
f.Overflow = true
|
||||
|
@ -472,6 +483,42 @@ func (f *bookingOptionFields) FillValues(r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func (f *bookingOptionFields) FillFromDatabase(ctx context.Context, conn *database.Conn, bookingID int) error {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select campsite_type_option.campsite_type_option_id
|
||||
, coalesce(units, lower(range))::text
|
||||
, to_price(coalesce(subtotal, 0), decimal_digits)
|
||||
from booking
|
||||
join campsite_type_option using (campsite_type_id)
|
||||
left join booking_option
|
||||
on booking.booking_id = booking_option.booking_id
|
||||
and booking_option.campsite_type_option_id = campsite_type_option.campsite_type_option_id
|
||||
join currency using (currency_code)
|
||||
where booking.booking_id = $1
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var units string
|
||||
var subtotal string
|
||||
if err = rows.Scan(&id, &units, &subtotal); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, option := range f.Options {
|
||||
if option.ID == id {
|
||||
option.Input.Val = units
|
||||
option.Subtotal = subtotal
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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)) {
|
||||
|
|
|
@ -357,6 +357,10 @@ func (tx *Tx) AddBookingFromPayment(ctx context.Context, paymentSlug string) (in
|
|||
return tx.GetInt(ctx, "select add_booking_from_payment($1)", paymentSlug)
|
||||
}
|
||||
|
||||
func (tx *Tx) EditBookingFromPayment(ctx context.Context, bookingSlug string, paymentSlug string) (int, error) {
|
||||
return tx.GetInt(ctx, "select edit_booking_from_payment($1, $2)", bookingSlug, paymentSlug)
|
||||
}
|
||||
|
||||
func (tx *Tx) EditBooking(ctx context.Context, bookingID int, customerName string, customerAddress string, customerPostCode string, customerCity string, customerCountryCode string, customerEmail string, customerPhone string, customerLangTag language.Tag, bookingStatus string, campsiteIDs []int) error {
|
||||
_, err := tx.Exec(ctx, "select edit_booking($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", bookingID, customerName, zeronull.Text(customerAddress), zeronull.Text(customerPostCode), zeronull.Text(customerCity), zeronull.Text(customerCountryCode), zeronull.Text(customerEmail), zeronull.Text(customerPhone), customerLangTag, bookingStatus, campsiteIDs)
|
||||
return err
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:edit_booking_from_payment from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.edit_booking_from_payment(uuid, uuid);
|
||||
|
||||
commit;
|
|
@ -289,3 +289,4 @@ booking__payment_fields [booking positive_integer nonnegative_integer] 2024-04-2
|
|||
booking_option [roles schema_camper booking campsite_type_option positive_integer nonnegative_integer] 2024-04-24T11:14:03Z jordi fita mas <jordi@tandem.blog> # Add booking campsite option relation
|
||||
add_booking_from_payment [roles schema_camper booking booking__payment_fields booking__stay booking_option payment payment__acsi_card payment_customer payment_option] 2024-04-24T11:23:22Z jordi fita mas <jordi@tandem.blog> # Add function to create a pre-booking from a payment
|
||||
edit_booking [roles schema_camper booking booking__payment_fields booking__stay booking_campsite] 2024-04-24T16:27:10Z jordi fita mas <jordi@tandem.blog> # Add function to update a booking
|
||||
edit_booking_from_payment [roles schema_camper booking booking__payment_fields booking__stay booking_option payment payment__acsi_card payment_option] 2024-04-25T17:18:41Z jordi fita mas <jordi@tandem.blog> # Add function to edit a booking from a payment
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
-- Test edit_booking_from_payment
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(12);
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
select has_function('camper', 'edit_booking_from_payment', array['uuid', 'uuid']);
|
||||
select function_lang_is('camper', 'edit_booking_from_payment', array['uuid', 'uuid'], 'plpgsql');
|
||||
select function_returns('camper', 'edit_booking_from_payment', array['uuid', 'uuid'], 'integer');
|
||||
select isnt_definer('camper', 'edit_booking_from_payment', array['uuid', 'uuid']);
|
||||
select volatility_is('camper', 'edit_booking_from_payment', array['uuid', 'uuid'], 'volatile');
|
||||
select function_privs_are('camper', 'edit_booking_from_payment', array ['uuid', 'uuid'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'edit_booking_from_payment', array ['uuid', 'uuid'], 'employee', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'edit_booking_from_payment', array ['uuid', 'uuid'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'edit_booking_from_payment', array ['uuid', 'uuid'], 'authenticator', array[]::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate booking_option cascade;
|
||||
truncate booking cascade;
|
||||
truncate payment_option cascade;
|
||||
truncate payment cascade;
|
||||
truncate campsite_type_option cascade;
|
||||
truncate campsite_type cascade;
|
||||
truncate media cascade;
|
||||
truncate media_content cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
|
||||
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
|
||||
;
|
||||
|
||||
insert into media_content (media_type, bytes)
|
||||
values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into media (media_id, company_id, original_filename, content_hash)
|
||||
values (6, 2, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};'))
|
||||
;
|
||||
|
||||
insert into campsite_type (campsite_type_id, company_id, name, media_id, max_campers, bookable_nights)
|
||||
values (12, 2, 'Wooden lodge', 6, 7, '[1, 7]')
|
||||
, (14, 2, 'Bungalow', 6, 7, '[1, 7]')
|
||||
, (16, 2, 'Plot', 6, 7, '[1, 7]')
|
||||
;
|
||||
|
||||
insert into campsite_type_option (campsite_type_option_id, campsite_type_id, name, range, per_night)
|
||||
values (18, 12, 'Big tent', '[0, 4)', true)
|
||||
, (20, 12, 'Small tent', '[0, 4)', true)
|
||||
, (22, 14, 'Electricity', '[0, 5)', false)
|
||||
, (24, 14, 'Car', '[0, 4)', true)
|
||||
, (26, 16, 'Autocaravan', '[0, 4)', true)
|
||||
;
|
||||
|
||||
insert into payment (payment_id, slug, company_id, campsite_type_id, arrival_date, departure_date, subtotal_nights, number_adults, subtotal_adults, number_teenagers, subtotal_teenagers, number_children, subtotal_children, number_dogs, subtotal_dogs, subtotal_tourist_tax, total, currency_code, zone_preferences, acsi_card, payment_status)
|
||||
values (28, '4ef35e2f-ef98-42d6-a724-913bd761ca8c', 2, 12, '2024-08-28', '2024-09-04', 3200, 2, 10420, 4, 20840, 6, 25080, 3, 2450, 4900, 79160, 'EUR', 'pref I before E', true, 'draft')
|
||||
;
|
||||
|
||||
insert into payment_option (payment_id, campsite_type_option_id, units, subtotal)
|
||||
values (28, 18, 1, 1500)
|
||||
;
|
||||
|
||||
insert into booking (booking_id, slug, company_id, campsite_type_id, stay, holder_name, address, postal_code, city, country_code, email, phone, lang_tag, zone_preferences, subtotal_nights, number_adults, subtotal_adults, number_teenagers, subtotal_teenagers, number_children, subtotal_children, number_dogs, subtotal_dogs, subtotal_tourist_tax, total, acsi_card, currency_code, booking_status)
|
||||
values (30, 'e3c478f1-8895-4cc6-b644-b19b5a553bb9', 2, 14, daterange('2024-08-29', '2024-09-03'), 'First', 'Fake St., 123', '17800', 'Girona', 'ES', 'customer@example.com', '+34 977 97 79 77', 'ca', '', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, false, 'EUR', 'confirmed');
|
||||
|
||||
insert into booking_option (booking_id, campsite_type_option_id, units, subtotal)
|
||||
values (30, 22, 2, 111)
|
||||
, (30, 24, 3, 2222)
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_booking_from_payment('e3c478f1-8895-4cc6-b644-b19b5a553bb9', '4ef35e2f-ef98-42d6-a724-913bd761ca8c') $$,
|
||||
'Should be able to update a booking from a payment'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select booking_id, company_id, campsite_type_id, stay, holder_name, address, postal_code, city, country_code::text, email::text, phone::text, lang_tag, zone_preferences, subtotal_nights::integer, number_adults::integer, subtotal_adults::integer, number_teenagers::integer, subtotal_teenagers::integer, number_children::integer, subtotal_children::integer, number_dogs::integer, subtotal_dogs::integer, subtotal_tourist_tax::integer, total::integer, acsi_card, currency_code::text, booking_status from booking $$,
|
||||
$$ values (30, 2, 12, daterange('2024-08-28', '2024-09-04'), 'First', 'Fake St., 123', '17800', 'Girona', 'ES', 'customer@example.com', '+34 977 97 79 77', 'ca', 'pref I before E', 3200, 2, 10420, 4, 20840, 6, 25080, 3, 2450, 4900, 79160, true, 'EUR', 'confirmed')
|
||||
$$,
|
||||
'Should have updated the booking'
|
||||
);
|
||||
|
||||
select bag_eq (
|
||||
$$ select booking_id, campsite_type_option_id, units, subtotal from booking_option $$,
|
||||
$$ values (30, 18, 1, 1500)
|
||||
$$ ,
|
||||
'Should have updated the booking options too'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify camper:edit_booking_from_payment on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.edit_booking_from_payment(uuid, uuid)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -882,7 +882,7 @@ label[x-show] > span, label[x-show] > br {
|
|||
grid-column: span 2;
|
||||
}
|
||||
|
||||
#booking-form #campsites-booking {
|
||||
#booking-form #campsites-booking, #booking-form .error {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.adminBookingForm*/ -}}
|
||||
{{ CSRFInput }}
|
||||
<fieldset>
|
||||
<input type="hidden" name="{{ .PaymentSlug.Name }}" value="{{ .PaymentSlug.Val }}">
|
||||
{{ if .Error -}}
|
||||
<p class="error">{{ .Error }}</p>
|
||||
{{- end }}
|
||||
{{ with .CampsiteType -}}
|
||||
<label>
|
||||
{{( pgettext "Accommodation" "title" )}}<br>
|
||||
<select name="{{ .Name }}"
|
||||
required
|
||||
data-hx-get="/admin/bookings/new" data-hx-trigger="change"
|
||||
data-hx-get="{{ $.URL }}" data-hx-trigger="change"
|
||||
{{ template "error-attrs" . }}
|
||||
>
|
||||
<option value="">{{( gettext "Choose an accommodation" )}}</option>
|
||||
|
@ -23,7 +24,7 @@
|
|||
{{- end }}
|
||||
{{ with .Dates -}}
|
||||
<fieldset class="booking-period"
|
||||
data-hx-get="/admin/bookings/new"
|
||||
data-hx-get="{{ $.URL }}"
|
||||
data-hx-trigger="change delay:500ms"
|
||||
>
|
||||
{{ with .ArrivalDate -}}
|
||||
|
@ -64,7 +65,7 @@
|
|||
{{ with $guests := .Guests -}}
|
||||
{{ $draft := $.Cart.Draft }}
|
||||
<fieldset class="booking-items"
|
||||
data-hx-get="/admin/bookings/new" data-hx-trigger="change"
|
||||
data-hx-get="{{ $.URL }}" data-hx-trigger="change"
|
||||
>
|
||||
<table>
|
||||
<thead>
|
||||
|
@ -252,7 +253,7 @@
|
|||
</label>
|
||||
{{- end }}
|
||||
{{ with $.Guests.ACSICard -}}
|
||||
<label class="colspan" data-hx-get="/admin/bookings/new" data-hx-trigger="change">
|
||||
<label class="colspan" data-hx-get="{{ $.URL }}" data-hx-trigger="change">
|
||||
<input type="checkbox" name="{{ .Name }}" {{ if .Checked}}checked{{ end }}
|
||||
{{ template "error-attrs" . }}
|
||||
> {{( pgettext "ACSI card? (optional)" "input" )}}<br>
|
||||
|
@ -264,17 +265,10 @@
|
|||
{{ if .Campsites -}}
|
||||
<h3>{{( pgettext "Campsites" "title" )}}</h3>
|
||||
{{ template "grid.gohtml" . }}
|
||||
{{ if .Error -}}
|
||||
<p class="error">{{ .Error }}</p>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</fieldset>
|
||||
<footer>
|
||||
<button type="submit">
|
||||
{{- if .ID -}}
|
||||
{{( pgettext "Update" "action" )}}
|
||||
{{- else -}}
|
||||
{{( pgettext "Add" "action" )}}
|
||||
{{- end -}}
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
{{ define "campsite-heading" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.CampsiteEntry*/ -}}
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{ define "head" -}}
|
||||
<script src="/static/idiomorph-ext@0.3.0.min.js"></script>
|
||||
{{- end }}
|
||||
|
||||
{{ define "breadcrumb" -}}
|
||||
<li><a href="./">{{( pgettext "Bookings" "title" )}}</a></li>
|
||||
{{- end }}
|
||||
|
@ -18,19 +22,28 @@
|
|||
{{ define "content" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.adminBookingForm*/ -}}
|
||||
<h2>{{ template "title" .}}</h2>
|
||||
{{ if .Error -}}
|
||||
<p class="error">{{ .Error }}</p>
|
||||
{{- end }}
|
||||
<form id="booking-form"
|
||||
data-hx-ext="morph"
|
||||
data-hx-swap="morph:innerHTML"
|
||||
data-hx-include="this"
|
||||
data-hx-target="this"
|
||||
data-hx-target="#booking-form-fields"
|
||||
data-hx-replace-url="true"
|
||||
{{- if .ID }} data-hx-put="/admin/bookings/{{ .CurrentLabel }}"
|
||||
{{- if .URL }} data-hx-put="{{ .URL }}"
|
||||
{{- else }} action="/admin/bookings" method="post"
|
||||
{{- end -}}
|
||||
>
|
||||
{{ CSRFInput }}
|
||||
<fieldset id="booking-form-fields">
|
||||
{{ template "fields.gohtml" . }}
|
||||
</fieldset>
|
||||
<footer>
|
||||
<button type="submit">
|
||||
{{- if .ID -}}
|
||||
{{( pgettext "Update" "action" )}}
|
||||
{{- else -}}
|
||||
{{( pgettext "Add" "action" )}}
|
||||
{{- end -}}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
{{- end }}
|
||||
|
|
Loading…
Reference in New Issue