768 lines
22 KiB
Go
768 lines
22 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package booking
|
|
|
|
import (
|
|
"context"
|
|
"dev.tandem.ws/tandem/camper/pkg/ods"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
|
"dev.tandem.ws/tandem/camper/pkg/form"
|
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
|
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
|
)
|
|
|
|
type AdminHandler struct {
|
|
}
|
|
|
|
func NewAdminHandler() *AdminHandler {
|
|
return &AdminHandler{}
|
|
}
|
|
|
|
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) 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:
|
|
serveBookingIndex(w, r, user, company, conn)
|
|
case http.MethodPost:
|
|
addBooking(w, r, user, company, conn)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
|
}
|
|
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, http.MethodPut)
|
|
}
|
|
case "check-in":
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
serveCheckInForm(w, r, user, company, conn, slug)
|
|
case http.MethodPost:
|
|
checkInBooking(w, r, user, company, conn, slug)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
|
}
|
|
case "guest":
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
serveGuestForm(w, r, user, company, conn, slug)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
}
|
|
|
|
func serveBookingIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
|
filters := newFilterForm(r.Context(), conn, company, user.Locale)
|
|
if err := filters.Parse(r); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language, filters)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
page := &bookingIndex{
|
|
Bookings: filters.buildCursor(bookings),
|
|
Filters: filters,
|
|
}
|
|
page.MustRender(w, r, user, company)
|
|
}
|
|
|
|
func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag, filters *filterForm) ([]*bookingEntry, error) {
|
|
where, args := filters.BuildQuery([]interface{}{lang.String()})
|
|
rows, err := conn.Query(ctx, fmt.Sprintf(`
|
|
select booking_id
|
|
, left(slug::text, 10)
|
|
, '/admin/bookings/' || slug
|
|
, lower(stay)
|
|
, upper(stay)
|
|
, holder_name
|
|
, booking.booking_status
|
|
, coalesce(i18n.name, status.name)
|
|
from booking
|
|
join booking_status as status using (booking_status)
|
|
left join booking_status_i18n as i18n on status.booking_status = i18n.booking_status and i18n.lang_tag = $1
|
|
where (%s)
|
|
order by lower(stay) desc
|
|
, booking_id desc
|
|
LIMIT %d
|
|
`, where, filters.perPage+1), args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var entries []*bookingEntry
|
|
for rows.Next() {
|
|
entry := &bookingEntry{}
|
|
if err = rows.Scan(&entry.ID, &entry.Reference, &entry.URL, &entry.ArrivalDate, &entry.DepartureDate, &entry.HolderName, &entry.Status, &entry.StatusLabel); err != nil {
|
|
return nil, err
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
type bookingEntry struct {
|
|
ID int
|
|
Reference string
|
|
URL string
|
|
ArrivalDate time.Time
|
|
DepartureDate time.Time
|
|
HolderName string
|
|
Status string
|
|
StatusLabel string
|
|
}
|
|
|
|
type bookingIndex struct {
|
|
Bookings []*bookingEntry
|
|
Filters *filterForm
|
|
}
|
|
|
|
func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
|
switch r.URL.Query().Get("format") {
|
|
case "ods":
|
|
columns := []string{
|
|
"Reference",
|
|
"Arrival Date",
|
|
"Departure Date",
|
|
"Holder Name",
|
|
"Status",
|
|
}
|
|
table, err := ods.WriteTable(page.Bookings, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
|
|
if err := ods.WriteCellString(sb, entry.Reference); err != nil {
|
|
return err
|
|
}
|
|
ods.WriteCellDate(sb, entry.ArrivalDate)
|
|
ods.WriteCellDate(sb, entry.DepartureDate)
|
|
if err := ods.WriteCellString(sb, entry.HolderName); err != nil {
|
|
return err
|
|
}
|
|
if err := ods.WriteCellString(sb, entry.StatusLabel); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ods.MustWriteResponse(w, table, user.Locale.Pgettext("bookings.ods", "filename"))
|
|
default:
|
|
if httplib.IsHTMxRequest(r) && page.Filters.pagination {
|
|
template.MustRenderAdminNoLayout(w, r, user, company, "booking/results.gohtml", page)
|
|
} else {
|
|
template.MustRenderAdminFiles(w, r, user, company, page, "booking/index.gohtml", "booking/results.gohtml")
|
|
}
|
|
}
|
|
}
|
|
|
|
type filterForm struct {
|
|
locale *locale.Locale
|
|
company *auth.Company
|
|
perPage int
|
|
pagination bool
|
|
HolderName *form.Input
|
|
BookingStatus *form.Select
|
|
FromDate *form.Input
|
|
ToDate *form.Input
|
|
Cursor *form.Input
|
|
}
|
|
|
|
func newFilterForm(ctx context.Context, conn *database.Conn, company *auth.Company, locale *locale.Locale) *filterForm {
|
|
return &filterForm{
|
|
locale: locale,
|
|
company: company,
|
|
perPage: 25,
|
|
HolderName: &form.Input{
|
|
Name: "holder_name",
|
|
},
|
|
BookingStatus: &form.Select{
|
|
Name: "booking_status",
|
|
Options: mustGetBookingStatusOptions(ctx, conn, locale),
|
|
},
|
|
FromDate: &form.Input{
|
|
Name: "from_date",
|
|
},
|
|
ToDate: &form.Input{
|
|
Name: "to_date",
|
|
},
|
|
Cursor: &form.Input{
|
|
Name: "cursor",
|
|
},
|
|
}
|
|
}
|
|
|
|
func mustGetBookingStatusOptions(ctx context.Context, conn *database.Conn, locale *locale.Locale) []*form.Option {
|
|
return form.MustGetOptions(ctx, conn, `
|
|
select booking_status.booking_status
|
|
, isi18n.name
|
|
from booking_status
|
|
join booking_status_i18n isi18n using(booking_status)
|
|
where isi18n.lang_tag = $1
|
|
and booking_status <> 'created'
|
|
order by booking_status`, locale.Language)
|
|
}
|
|
|
|
func (f *filterForm) Parse(r *http.Request) error {
|
|
if err := r.ParseForm(); err != nil {
|
|
return err
|
|
}
|
|
f.HolderName.FillValue(r)
|
|
f.BookingStatus.FillValue(r)
|
|
f.FromDate.FillValue(r)
|
|
f.ToDate.FillValue(r)
|
|
f.Cursor.FillValue(r)
|
|
f.pagination = f.Cursor.Val != ""
|
|
return nil
|
|
}
|
|
|
|
func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
|
var where []string
|
|
appendWhere := func(expression string, value interface{}) {
|
|
args = append(args, value)
|
|
where = append(where, fmt.Sprintf(expression, len(args)))
|
|
}
|
|
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
|
|
if value != "" {
|
|
if conv == nil {
|
|
appendWhere(expression, value)
|
|
} else {
|
|
appendWhere(expression, conv(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
appendWhere("booking.company_id = $%d", f.company.ID)
|
|
maybeAppendWhere("booking.holder_name ILIKE $%d", f.HolderName.Val, func(v string) interface{} {
|
|
return "%" + v + "%"
|
|
})
|
|
if len(f.BookingStatus.Selected) == 0 {
|
|
where = append(where, "booking.booking_status <> 'created'")
|
|
} else {
|
|
maybeAppendWhere("booking.booking_status = $%d", f.BookingStatus.String(), nil)
|
|
}
|
|
maybeAppendWhere("lower(stay) >= $%d", f.FromDate.Val, nil)
|
|
maybeAppendWhere("lower(stay) <= $%d", f.ToDate.Val, nil)
|
|
|
|
if f.Cursor.Val != "" {
|
|
params := strings.Split(f.Cursor.Val, ";")
|
|
if len(params) == 2 {
|
|
where = append(where, fmt.Sprintf("(lower(stay), booking_id) < ($%d, $%d)", len(args)+1, len(args)+2))
|
|
args = append(args, params[0])
|
|
args = append(args, params[1])
|
|
}
|
|
}
|
|
|
|
return strings.Join(where, ") AND ("), args
|
|
}
|
|
|
|
func (f *filterForm) buildCursor(bookings []*bookingEntry) []*bookingEntry {
|
|
if len(bookings) <= f.perPage {
|
|
f.Cursor.Val = ""
|
|
return bookings
|
|
}
|
|
bookings = bookings[:f.perPage]
|
|
last := bookings[f.perPage-1]
|
|
f.Cursor.Val = fmt.Sprintf("%s;%d", last.ArrivalDate.Format(database.ISODateFormat), last.ID)
|
|
return bookings
|
|
}
|
|
|
|
func (f *filterForm) HasValue() bool {
|
|
return f.HolderName.Val != "" ||
|
|
(len(f.BookingStatus.Selected) > 0 && f.BookingStatus.Selected[0] != "") ||
|
|
f.FromDate.Val != "" ||
|
|
f.ToDate.Val != ""
|
|
}
|
|
|
|
type adminBookingForm struct {
|
|
*bookingForm
|
|
ID int
|
|
URL string
|
|
Status 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 {
|
|
return nil, err
|
|
}
|
|
if inner.Options != nil {
|
|
for _, option := range inner.Options.Options {
|
|
option.Subtotal = findSubtotal(option.ID, inner.Cart)
|
|
}
|
|
}
|
|
f := &adminBookingForm{
|
|
bookingForm: inner,
|
|
}
|
|
// Dates and Campsite are valid
|
|
if inner.Guests != 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
|
|
}
|
|
|
|
for _, s := range selected {
|
|
ID, _ := strconv.Atoi(s)
|
|
for _, c := range f.Campsites {
|
|
if c.ID == ID {
|
|
f.selected = append(f.selected, c.ID)
|
|
c.Selected = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func findSubtotal(ID int, cart *bookingCart) string {
|
|
none := "0.0"
|
|
if cart == nil || cart.Draft == nil {
|
|
return none
|
|
}
|
|
for _, option := range cart.Draft.Options {
|
|
if option.ID == ID {
|
|
return option.Subtotal
|
|
}
|
|
}
|
|
return none
|
|
}
|
|
|
|
func (f *adminBookingForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
|
if httplib.IsHTMxRequest(r) {
|
|
template.MustRenderAdminNoLayoutFiles(w, r, user, company, f, "booking/fields.gohtml", "booking/grid.gohtml")
|
|
} else {
|
|
template.MustRenderAdminFiles(w, r, user, company, f, "booking/form.gohtml", "booking/fields.gohtml", "booking/grid.gohtml")
|
|
}
|
|
}
|
|
|
|
func addBooking(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
|
processAdminBookingForm(w, r, user, company, conn, 0, func(ctx context.Context, tx *database.Tx, f *adminBookingForm) error {
|
|
var err error
|
|
f.ID, err = tx.AddBookingFromPayment(ctx, 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("und"),
|
|
"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
|
|
}
|
|
if bookingStatus == "created" {
|
|
bookingStatus = "confirmed"
|
|
}
|
|
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) {
|
|
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")
|
|
}
|
|
if f.Cart == nil {
|
|
return false, errors.New("no booking cart")
|
|
}
|
|
|
|
v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid."))
|
|
f.Dates.Valid(v, l)
|
|
f.Guests.Valid(v, l)
|
|
if f.Options != nil {
|
|
f.Options.Valid(v, l)
|
|
}
|
|
|
|
var country string
|
|
if f.Customer.Country.ValidOptionsSelected() {
|
|
country = f.Customer.Country.Selected[0]
|
|
}
|
|
|
|
if v.CheckRequired(f.Customer.FullName, l.GettextNoop("Full name can not be empty.")) {
|
|
v.CheckMinLength(f.Customer.FullName, 1, l.GettextNoop("Full name must have at least one letter."))
|
|
}
|
|
|
|
if f.Customer.PostalCode.Val != "" {
|
|
if country == "" {
|
|
v.Check(f.Customer.PostalCode, false, l.GettextNoop("Country can not be empty to validate the postcode."))
|
|
} else if _, err := v.CheckValidPostalCode(ctx, conn, f.Customer.PostalCode, country, l.GettextNoop("This postcode is not valid.")); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
if f.Customer.Email.Val != "" {
|
|
v.CheckValidEmail(f.Customer.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
|
|
}
|
|
if f.Customer.Phone.Val != "" {
|
|
if country == "" {
|
|
v.Check(f.Customer.Phone, false, l.GettextNoop("Country can not be empty to validate the phone."))
|
|
} else if _, err := v.CheckValidPhone(ctx, conn, f.Customer.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
if len(f.selected) == 0 {
|
|
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.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."))
|
|
v.AllOK = false
|
|
}
|
|
}
|
|
|
|
return v.AllOK, nil
|
|
}
|
|
|
|
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
|
|
, booking_status
|
|
, 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.Status,
|
|
&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
|
|
}
|