“Mockup” for the new booking form
It does nothing but compute the total of a booking, much like it does for guests. In fact, i use the same payment relations to do the exact same computation, otherwise i am afraid i will make a mistake in the ACSI or such, now or in future version; better if both are exactly the same. The idea is that once the user creates the booking, i will delete that payment, because it makes no sense to keep it in this case; nobody is going to pay for it. Had to reuse the grid showing the bookings of campsites because employees need to select one or more campsites to book, and need to see which are available. In this case, i have to filter by campsite type and use the arrival and departure dates to filter the months, now up to the day, not just month. Had to change max width of th and td in the grid to take into account that now a month could have a single day, for instance, and the month heading can not stretch the day or booking spans would not be in their correct positions. For that, i needed to access campsiteEntry, bookingEntry, and Month from campsite package, but campsite imports campsite/types, and campsite/types already imports booking for the BookingDates type. To break the cycle, had to move all that to booking and use from campsite; it is mostly unchanged, except for the granularity of dates up to days instead of just months. The design of this form calls for a different way of showing the totals, because here employees have to see the amount next to the input with the units, instead of having a footer with the table. I did not like the idea of having to query the database for that, therefore i “lifter” the payment draft into a struct that both public and admin forms use to show they respective views of the cart.
This commit is contained in:
parent
598354e8b7
commit
3aa53cf1a9
|
@ -7,14 +7,16 @@ package booking
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"golang.org/x/text/language"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
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/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,6 +40,17 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
||||||
default:
|
default:
|
||||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||||
}
|
}
|
||||||
|
case "new":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
f, err := newAdminBookingForm(r, conn, company, user.Locale)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
f.MustRender(w, r, user, company)
|
||||||
|
default:
|
||||||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +69,7 @@ func serveBookingIndex(w http.ResponseWriter, r *http.Request, user *auth.User,
|
||||||
func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag) ([]*bookingEntry, error) {
|
func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag) ([]*bookingEntry, error) {
|
||||||
rows, err := conn.Query(ctx, `
|
rows, err := conn.Query(ctx, `
|
||||||
select left(slug::text, 10)
|
select left(slug::text, 10)
|
||||||
, '/admin/booking/' || slug
|
, '/admin/bookings/' || slug
|
||||||
, lower(stay)
|
, lower(stay)
|
||||||
, upper(stay)
|
, upper(stay)
|
||||||
, holder_name
|
, holder_name
|
||||||
|
@ -128,3 +141,59 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
|
||||||
template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page)
|
template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type adminBookingForm struct {
|
||||||
|
*bookingForm
|
||||||
|
ID int
|
||||||
|
Campsites []*CampsiteEntry
|
||||||
|
Months []*Month
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAdminBookingForm(r *http.Request, conn *database.Conn, company *auth.Company, l *locale.Locale) (*adminBookingForm, error) {
|
||||||
|
form, err := newBookingForm(r, company, conn, l)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if form.Options != nil {
|
||||||
|
for _, option := range form.Options.Options {
|
||||||
|
option.Subtotal = findSubtotal(option.ID, form.Cart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f := &adminBookingForm{
|
||||||
|
bookingForm: form,
|
||||||
|
}
|
||||||
|
// Dates and Campsite are valid
|
||||||
|
if form.Guests != nil {
|
||||||
|
arrivalDate, _ := time.Parse(database.ISODateFormat, form.Dates.ArrivalDate.Val)
|
||||||
|
from := arrivalDate.AddDate(0, 0, -1)
|
||||||
|
departureDate, _ := time.Parse(database.ISODateFormat, form.Dates.DepartureDate.Val)
|
||||||
|
to := departureDate.AddDate(0, 0, 2)
|
||||||
|
f.Months = CollectMonths(from, to)
|
||||||
|
f.Campsites, err = CollectCampsiteEntries(r.Context(), company, conn, from, to, form.CampsiteType.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f, 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
package booking
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/season"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Month struct {
|
||||||
|
Year int
|
||||||
|
Month time.Month
|
||||||
|
Name string
|
||||||
|
Days []time.Time
|
||||||
|
Spans []*Span
|
||||||
|
}
|
||||||
|
|
||||||
|
type Span struct {
|
||||||
|
Weekend bool
|
||||||
|
Today bool
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWeekend(t time.Time) bool {
|
||||||
|
switch t.Weekday() {
|
||||||
|
case time.Saturday, time.Sunday:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CollectMonths(from time.Time, to time.Time) []*Month {
|
||||||
|
current := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
now := time.Now()
|
||||||
|
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
var months []*Month
|
||||||
|
for !current.Equal(to) {
|
||||||
|
span := &Span{
|
||||||
|
Weekend: isWeekend(current),
|
||||||
|
Today: current.Equal(today),
|
||||||
|
}
|
||||||
|
month := &Month{
|
||||||
|
Year: current.Year(),
|
||||||
|
Month: current.Month(),
|
||||||
|
Name: season.LongMonthNames[current.Month()-1],
|
||||||
|
Days: make([]time.Time, 0, 31),
|
||||||
|
Spans: make([]*Span, 0, 10),
|
||||||
|
}
|
||||||
|
month.Spans = append(month.Spans, span)
|
||||||
|
for current.Month() == month.Month && !current.Equal(to) {
|
||||||
|
month.Days = append(month.Days, current)
|
||||||
|
if span.Weekend != isWeekend(current) || span.Today != current.Equal(today) {
|
||||||
|
span = &Span{
|
||||||
|
Weekend: isWeekend(current),
|
||||||
|
Today: current.Equal(today),
|
||||||
|
}
|
||||||
|
month.Spans = append(month.Spans, span)
|
||||||
|
}
|
||||||
|
span.Count = span.Count + 1
|
||||||
|
current = current.AddDate(0, 0, 1)
|
||||||
|
}
|
||||||
|
months = append(months, month)
|
||||||
|
}
|
||||||
|
return months
|
||||||
|
}
|
||||||
|
|
||||||
|
type CampsiteEntry struct {
|
||||||
|
Label string
|
||||||
|
Type string
|
||||||
|
Active bool
|
||||||
|
Bookings map[time.Time]*CampsiteBooking
|
||||||
|
}
|
||||||
|
|
||||||
|
type CampsiteBooking struct {
|
||||||
|
Holder string
|
||||||
|
Status string
|
||||||
|
Nights int
|
||||||
|
Begin bool
|
||||||
|
End bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func CollectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time, campsiteType string) ([]*CampsiteEntry, error) {
|
||||||
|
rows, err := conn.Query(ctx, `
|
||||||
|
select campsite.label
|
||||||
|
, campsite_type.name
|
||||||
|
, campsite.active
|
||||||
|
from campsite
|
||||||
|
join campsite_type using (campsite_type_id)
|
||||||
|
where campsite.company_id = $1
|
||||||
|
and ($2::uuid is null or campsite_type.slug = $2::uuid)
|
||||||
|
order by label`, company.ID, database.ZeroNullUUID(campsiteType))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
byLabel := make(map[string]*CampsiteEntry)
|
||||||
|
var campsites []*CampsiteEntry
|
||||||
|
for rows.Next() {
|
||||||
|
entry := &CampsiteEntry{}
|
||||||
|
if err = rows.Scan(&entry.Label, &entry.Type, &entry.Active); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
campsites = append(campsites, entry)
|
||||||
|
byLabel[entry.Label] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := collectCampsiteBookings(ctx, company, conn, from, to, byLabel); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return campsites, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectCampsiteBookings(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time, campsites map[string]*CampsiteEntry) error {
|
||||||
|
rows, err := conn.Query(ctx, `
|
||||||
|
select campsite.label
|
||||||
|
, lower(booking_campsite.stay * daterange($2::date, $3::date))
|
||||||
|
, holder_name
|
||||||
|
, booking_status
|
||||||
|
, upper(booking_campsite.stay * daterange($2::date, $3::date)) - lower(booking_campsite.stay * daterange($2::date, $3::date))
|
||||||
|
, booking_campsite.stay &> daterange($2::date, $3::date)
|
||||||
|
, booking_campsite.stay &< daterange($2::date, ($3 - 1)::date)
|
||||||
|
from booking_campsite
|
||||||
|
join booking using (booking_id)
|
||||||
|
join campsite using (campsite_id)
|
||||||
|
where booking.company_id = $1
|
||||||
|
and booking_campsite.stay && daterange($2::date, $3::date)
|
||||||
|
order by label`, company.ID, from, to)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
entry := &CampsiteBooking{}
|
||||||
|
var label string
|
||||||
|
var date time.Time
|
||||||
|
if err = rows.Scan(&label, &date, &entry.Holder, &entry.Status, &entry.Nights, &entry.Begin, &entry.End); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
campsite := campsites[label]
|
||||||
|
if campsite != nil {
|
||||||
|
if campsite.Bookings == nil {
|
||||||
|
campsite.Bookings = make(map[time.Time]*CampsiteBooking)
|
||||||
|
}
|
||||||
|
campsite.Bookings[date] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type bookingCart struct {
|
type bookingCart struct {
|
||||||
|
Draft *paymentDraft
|
||||||
Lines []*cartLine
|
Lines []*cartLine
|
||||||
Total string
|
Total string
|
||||||
DownPayment string
|
DownPayment string
|
||||||
|
@ -23,52 +24,80 @@ type cartLine struct {
|
||||||
Subtotal string
|
Subtotal string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) {
|
type paymentDraft struct {
|
||||||
cart := &bookingCart{
|
ArrivalDate time.Time
|
||||||
Total: "0.0",
|
DepartureDate time.Time
|
||||||
}
|
NumAdults int
|
||||||
|
NumTeenagers int
|
||||||
|
NumChildren int
|
||||||
|
NumDogs int
|
||||||
|
ZonePreferences string
|
||||||
|
ACSICard bool
|
||||||
|
PaymentID int
|
||||||
|
NumNights int
|
||||||
|
Nights string
|
||||||
|
Adults string
|
||||||
|
Teenagers string
|
||||||
|
Children string
|
||||||
|
Dogs string
|
||||||
|
TouristTax string
|
||||||
|
Total string
|
||||||
|
DownPaymentPercent int
|
||||||
|
DownPayment string
|
||||||
|
Options []*paymentOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentOption struct {
|
||||||
|
ID int
|
||||||
|
Label string
|
||||||
|
Units int
|
||||||
|
Subtotal string
|
||||||
|
}
|
||||||
|
|
||||||
|
func draftPayment(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*paymentDraft, error) {
|
||||||
|
var err error
|
||||||
if f.Dates == nil {
|
if f.Dates == nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
arrivalDate, err := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
|
|
||||||
|
draft := &paymentDraft{}
|
||||||
|
draft.ArrivalDate, err = time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
departureDate, err := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
|
draft.DepartureDate, err = time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Guests == nil {
|
if f.Guests == nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
numAdults, err := strconv.Atoi(f.Guests.NumberAdults.Val)
|
draft.NumAdults, err = strconv.Atoi(f.Guests.NumberAdults.Val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
numTeenagers, err := strconv.Atoi(f.Guests.NumberTeenagers.Val)
|
draft.NumTeenagers, err = strconv.Atoi(f.Guests.NumberTeenagers.Val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
numChildren, err := strconv.Atoi(f.Guests.NumberChildren.Val)
|
draft.NumChildren, err = strconv.Atoi(f.Guests.NumberChildren.Val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
numDogs := 0
|
|
||||||
if f.Guests.NumberDogs != nil {
|
if f.Guests.NumberDogs != nil {
|
||||||
numDogs, err = strconv.Atoi(f.Guests.NumberDogs.Val)
|
draft.NumDogs, err = strconv.Atoi(f.Guests.NumberDogs.Val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
zonePreferences := ""
|
|
||||||
if f.Options != nil && f.Options.ZonePreferences != nil {
|
if f.Options != nil && f.Options.ZonePreferences != nil {
|
||||||
zonePreferences = f.Options.ZonePreferences.Val
|
draft.ZonePreferences = f.Options.ZonePreferences.Val
|
||||||
}
|
}
|
||||||
|
|
||||||
var ACSICard bool
|
|
||||||
if f.Guests.ACSICard != nil {
|
if f.Guests.ACSICard != nil {
|
||||||
ACSICard = f.Guests.ACSICard.Checked
|
draft.ACSICard = f.Guests.ACSICard.Checked
|
||||||
}
|
}
|
||||||
|
|
||||||
optionMap := make(map[int]*campsiteTypeOption)
|
optionMap := make(map[int]*campsiteTypeOption)
|
||||||
|
@ -106,62 +135,37 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
|
||||||
join currency using (currency_code)
|
join currency using (currency_code)
|
||||||
`,
|
`,
|
||||||
database.ZeroNullUUID(f.PaymentSlug.Val),
|
database.ZeroNullUUID(f.PaymentSlug.Val),
|
||||||
arrivalDate,
|
draft.ArrivalDate,
|
||||||
departureDate,
|
draft.DepartureDate,
|
||||||
campsiteType,
|
campsiteType,
|
||||||
numAdults,
|
draft.NumAdults,
|
||||||
numTeenagers,
|
draft.NumTeenagers,
|
||||||
numChildren,
|
draft.NumChildren,
|
||||||
numDogs,
|
draft.NumDogs,
|
||||||
zonePreferences,
|
draft.ZonePreferences,
|
||||||
ACSICard,
|
draft.ACSICard,
|
||||||
database.OptionUnitsArray(optionUnits),
|
database.OptionUnitsArray(optionUnits),
|
||||||
)
|
)
|
||||||
var paymentID int
|
|
||||||
var numNights int
|
|
||||||
var nights string
|
|
||||||
var adults string
|
|
||||||
var teenagers string
|
|
||||||
var children string
|
|
||||||
var dogs string
|
|
||||||
var touristTax string
|
|
||||||
var total string
|
|
||||||
var downPayment string
|
|
||||||
if err = row.Scan(
|
if err = row.Scan(
|
||||||
&f.PaymentSlug.Val,
|
&f.PaymentSlug.Val,
|
||||||
&paymentID,
|
&draft.PaymentID,
|
||||||
&numNights,
|
&draft.NumNights,
|
||||||
&nights,
|
&draft.Nights,
|
||||||
&adults,
|
&draft.Adults,
|
||||||
&teenagers,
|
&draft.Teenagers,
|
||||||
&children,
|
&draft.Children,
|
||||||
&dogs,
|
&draft.Dogs,
|
||||||
&touristTax,
|
&draft.TouristTax,
|
||||||
&total,
|
&draft.Total,
|
||||||
&downPayment,
|
&draft.DownPayment,
|
||||||
&cart.DownPaymentPercent,
|
&draft.DownPaymentPercent,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
if database.ErrorIsNotFound(err) {
|
if database.ErrorIsNotFound(err) {
|
||||||
return cart, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeAddLine := func(units int, subtotal string, concept string) {
|
|
||||||
if units > 0 && subtotal != "" {
|
|
||||||
cart.Lines = append(cart.Lines, &cartLine{
|
|
||||||
Concept: concept,
|
|
||||||
Units: units,
|
|
||||||
Subtotal: subtotal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maybeAddLine(numNights, nights, locale.PgettextNoop("Night", "cart"))
|
|
||||||
maybeAddLine(numAdults, adults, locale.PgettextNoop("Adult", "cart"))
|
|
||||||
maybeAddLine(numTeenagers, teenagers, locale.PgettextNoop("Teenager", "cart"))
|
|
||||||
maybeAddLine(numChildren, children, locale.PgettextNoop("Child", "cart"))
|
|
||||||
maybeAddLine(numDogs, dogs, locale.PgettextNoop("Dog", "cart"))
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, `
|
rows, err := conn.Query(ctx, `
|
||||||
select campsite_type_option_id
|
select campsite_type_option_id
|
||||||
, units
|
, units
|
||||||
|
@ -170,7 +174,8 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
|
||||||
join payment using (payment_id)
|
join payment using (payment_id)
|
||||||
join currency using (currency_code)
|
join currency using (currency_code)
|
||||||
where payment_id = $1
|
where payment_id = $1
|
||||||
`, paymentID)
|
order by campsite_type_option_id
|
||||||
|
`, draft.PaymentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -188,20 +193,62 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
|
||||||
if option == nil {
|
if option == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
maybeAddLine(units, subtotal, option.Label)
|
draft.Options = append(draft.Options, &paymentOption{
|
||||||
|
ID: option.ID,
|
||||||
|
Label: option.Label,
|
||||||
|
Units: units,
|
||||||
|
Subtotal: subtotal,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if rows.Err() != nil {
|
if rows.Err() != nil {
|
||||||
return nil, rows.Err()
|
return nil, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeAddLine(numAdults, touristTax, locale.PgettextNoop("Tourist tax", "cart"))
|
return draft, nil
|
||||||
|
}
|
||||||
|
|
||||||
if total != "0.0" {
|
func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) {
|
||||||
cart.Total = total
|
cart := &bookingCart{
|
||||||
|
Total: "0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
draft, err := draftPayment(ctx, conn, f, campsiteType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if draft == nil {
|
||||||
|
return cart, nil
|
||||||
|
}
|
||||||
|
cart.Draft = draft
|
||||||
|
cart.DownPaymentPercent = draft.DownPaymentPercent
|
||||||
|
|
||||||
|
maybeAddLine := func(units int, subtotal string, concept string) {
|
||||||
|
if units > 0 && subtotal != "" {
|
||||||
|
cart.Lines = append(cart.Lines, &cartLine{
|
||||||
|
Concept: concept,
|
||||||
|
Units: units,
|
||||||
|
Subtotal: subtotal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maybeAddLine(draft.NumNights, draft.Nights, locale.PgettextNoop("Night", "cart"))
|
||||||
|
maybeAddLine(draft.NumAdults, draft.Adults, locale.PgettextNoop("Adult", "cart"))
|
||||||
|
maybeAddLine(draft.NumTeenagers, draft.Teenagers, locale.PgettextNoop("Teenager", "cart"))
|
||||||
|
maybeAddLine(draft.NumChildren, draft.Children, locale.PgettextNoop("Child", "cart"))
|
||||||
|
maybeAddLine(draft.NumDogs, draft.Dogs, locale.PgettextNoop("Dog", "cart"))
|
||||||
|
|
||||||
|
for _, option := range draft.Options {
|
||||||
|
maybeAddLine(option.Units, option.Subtotal, option.Label)
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeAddLine(draft.NumAdults, draft.TouristTax, locale.PgettextNoop("Tourist tax", "cart"))
|
||||||
|
|
||||||
|
if draft.Total != "0.0" {
|
||||||
|
cart.Total = draft.Total
|
||||||
cart.Enabled = f.Guests.Error == nil
|
cart.Enabled = f.Guests.Error == nil
|
||||||
|
|
||||||
if downPayment != total {
|
if draft.DownPayment != draft.Total {
|
||||||
cart.DownPayment = downPayment
|
cart.DownPayment = draft.DownPayment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,11 +130,12 @@ type bookingOptionFields struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type campsiteTypeOption struct {
|
type campsiteTypeOption struct {
|
||||||
ID int
|
ID int
|
||||||
Label string
|
Label string
|
||||||
Min int
|
Min int
|
||||||
Max int
|
Max int
|
||||||
Input *form.Input
|
Input *form.Input
|
||||||
|
Subtotal string
|
||||||
}
|
}
|
||||||
|
|
||||||
type bookingCustomerFields struct {
|
type bookingCustomerFields struct {
|
||||||
|
|
|
@ -13,12 +13,12 @@ import (
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/booking"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/campsite/types"
|
"dev.tandem.ws/tandem/camper/pkg/campsite/types"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/season"
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,106 +95,21 @@ func serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User,
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
page.Campsites, err = collectCampsiteEntries(r.Context(), company, conn, page.From.Date(), page.To.Date())
|
from := page.From.Date()
|
||||||
|
to := page.To.Date().AddDate(0, 1, 0)
|
||||||
|
page.Campsites, err = booking.CollectCampsiteEntries(r.Context(), company, conn, from, to, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
page.Months = collectMonths(page.From.Date(), page.To.Date())
|
page.Months = booking.CollectMonths(from, to)
|
||||||
page.MustRender(w, r, user, company)
|
page.MustRender(w, r, user, company)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time) ([]*campsiteEntry, error) {
|
|
||||||
rows, err := conn.Query(ctx, `
|
|
||||||
select campsite.label
|
|
||||||
, campsite_type.name
|
|
||||||
, campsite.active
|
|
||||||
from campsite
|
|
||||||
join campsite_type using (campsite_type_id)
|
|
||||||
where campsite.company_id = $1
|
|
||||||
order by label`, company.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
byLabel := make(map[string]*campsiteEntry)
|
|
||||||
var campsites []*campsiteEntry
|
|
||||||
for rows.Next() {
|
|
||||||
entry := &campsiteEntry{}
|
|
||||||
if err = rows.Scan(&entry.Label, &entry.Type, &entry.Active); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
campsites = append(campsites, entry)
|
|
||||||
byLabel[entry.Label] = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := collectBookingEntries(ctx, company, conn, from, to, byLabel); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return campsites, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectBookingEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time, campsites map[string]*campsiteEntry) error {
|
|
||||||
lastDay := to.AddDate(0, 1, 0)
|
|
||||||
rows, err := conn.Query(ctx, `
|
|
||||||
select campsite.label
|
|
||||||
, lower(booking_campsite.stay * daterange($2::date, $3::date))
|
|
||||||
, holder_name
|
|
||||||
, booking_status
|
|
||||||
, upper(booking_campsite.stay * daterange($2::date, $3::date)) - lower(booking_campsite.stay * daterange($2::date, $3::date))
|
|
||||||
, booking_campsite.stay &> daterange($2::date, $3::date)
|
|
||||||
, booking_campsite.stay &< daterange($2::date, $3::date)
|
|
||||||
from booking_campsite
|
|
||||||
join booking using (booking_id)
|
|
||||||
join campsite using (campsite_id)
|
|
||||||
where booking.company_id = $1
|
|
||||||
and booking_campsite.stay && daterange($2::date, $3::date)
|
|
||||||
order by label`, company.ID, from, lastDay)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
entry := &bookingEntry{}
|
|
||||||
var label string
|
|
||||||
var date time.Time
|
|
||||||
if err = rows.Scan(&label, &date, &entry.Holder, &entry.Status, &entry.Nights, &entry.Begin, &entry.End); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
campsite := campsites[label]
|
|
||||||
if campsite != nil {
|
|
||||||
if campsite.Bookings == nil {
|
|
||||||
campsite.Bookings = make(map[time.Time]*bookingEntry)
|
|
||||||
}
|
|
||||||
campsite.Bookings[date] = entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type campsiteEntry struct {
|
|
||||||
Label string
|
|
||||||
Type string
|
|
||||||
Active bool
|
|
||||||
Bookings map[time.Time]*bookingEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
type bookingEntry struct {
|
|
||||||
Holder string
|
|
||||||
Status string
|
|
||||||
Nights int
|
|
||||||
Begin bool
|
|
||||||
End bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type campsiteIndex struct {
|
type campsiteIndex struct {
|
||||||
From *form.Month
|
From *form.Month
|
||||||
To *form.Month
|
To *form.Month
|
||||||
Campsites []*campsiteEntry
|
Campsites []*booking.CampsiteEntry
|
||||||
Months []*Month
|
Months []*booking.Month
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCampsiteIndex() *campsiteIndex {
|
func newCampsiteIndex() *campsiteIndex {
|
||||||
|
@ -224,62 +139,8 @@ func (page *campsiteIndex) Parse(r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Month struct {
|
|
||||||
Year int
|
|
||||||
Month time.Month
|
|
||||||
Name string
|
|
||||||
Days []time.Time
|
|
||||||
Spans []*Span
|
|
||||||
}
|
|
||||||
|
|
||||||
type Span struct {
|
|
||||||
Weekend bool
|
|
||||||
Count int
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWeekend(t time.Time) bool {
|
|
||||||
switch t.Weekday() {
|
|
||||||
case time.Saturday, time.Sunday:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectMonths(from time.Time, to time.Time) []*Month {
|
|
||||||
current := time.Date(from.Year(), from.Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
numMonths := (to.Year()-from.Year())*12 + int(to.Month()) - int(from.Month()) + 1
|
|
||||||
var months []*Month
|
|
||||||
for i := 0; i < numMonths; i++ {
|
|
||||||
span := &Span{
|
|
||||||
Weekend: isWeekend(current),
|
|
||||||
}
|
|
||||||
month := &Month{
|
|
||||||
Year: current.Year(),
|
|
||||||
Month: current.Month(),
|
|
||||||
Name: season.LongMonthNames[current.Month()-1],
|
|
||||||
Days: make([]time.Time, 0, 31),
|
|
||||||
Spans: make([]*Span, 0, 10),
|
|
||||||
}
|
|
||||||
month.Spans = append(month.Spans, span)
|
|
||||||
for current.Month() == month.Month {
|
|
||||||
month.Days = append(month.Days, current)
|
|
||||||
if span.Weekend != isWeekend(current) {
|
|
||||||
span = &Span{
|
|
||||||
Weekend: !span.Weekend,
|
|
||||||
}
|
|
||||||
month.Spans = append(month.Spans, span)
|
|
||||||
}
|
|
||||||
span.Count = span.Count + 1
|
|
||||||
current = current.AddDate(0, 0, 1)
|
|
||||||
}
|
|
||||||
months = append(months, month)
|
|
||||||
}
|
|
||||||
return months
|
|
||||||
}
|
|
||||||
|
|
||||||
func (page *campsiteIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
func (page *campsiteIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
template.MustRenderAdminFiles(w, r, user, company, page, "campsite/index.gohtml", "web/templates/campground_map.svg")
|
template.MustRenderAdminFiles(w, r, user, company, page, "campsite/index.gohtml", "booking/grid.gohtml")
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||||
|
|
|
@ -56,6 +56,10 @@ func MustRenderAdminNoLayout(w io.Writer, r *http.Request, user *auth.User, comp
|
||||||
mustRenderLayout(w, user, company, adminTemplateFile, data, filename)
|
mustRenderLayout(w, user, company, adminTemplateFile, data, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MustRenderAdminNoLayoutFiles(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, data interface{}, filenames ...string) {
|
||||||
|
mustRenderLayout(w, user, company, adminTemplateFile, data, filenames...)
|
||||||
|
}
|
||||||
|
|
||||||
func MustRenderPublic(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) {
|
func MustRenderPublic(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) {
|
||||||
layout := "layout.gohtml"
|
layout := "layout.gohtml"
|
||||||
mustRenderLayout(w, user, company, publicTemplateFile, data, layout, filename)
|
mustRenderLayout(w, user, company, publicTemplateFile, data, layout, filename)
|
||||||
|
|
|
@ -772,7 +772,7 @@ label[x-show] > span, label[x-show] > br {
|
||||||
/*<editor-fold desc="Campsites Booking">*/
|
/*<editor-fold desc="Campsites Booking">*/
|
||||||
|
|
||||||
#campsites-booking {
|
#campsites-booking {
|
||||||
height: 90vh;
|
max-height: 90vh;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -784,6 +784,10 @@ label[x-show] > span, label[x-show] > br {
|
||||||
background-color: var(--camper--color--rosy);
|
background-color: var(--camper--color--rosy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#campsites-booking .today {
|
||||||
|
border-left: 2px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
#campsites-booking colgroup {
|
#campsites-booking colgroup {
|
||||||
border-right: 2px solid;
|
border-right: 2px solid;
|
||||||
}
|
}
|
||||||
|
@ -792,6 +796,7 @@ label[x-show] > span, label[x-show] > br {
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
min-width: 2.25ch;
|
min-width: 2.25ch;
|
||||||
max-width: 2.25ch;
|
max-width: 2.25ch;
|
||||||
|
width: 2.25ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
#campsites-booking th {
|
#campsites-booking th {
|
||||||
|
@ -801,6 +806,9 @@ label[x-show] > span, label[x-show] > br {
|
||||||
|
|
||||||
#campsites-booking thead tr:first-child th, #campsites-booking tbody th {
|
#campsites-booking thead tr:first-child th, #campsites-booking tbody th {
|
||||||
padding: 0 .5ch;
|
padding: 0 .5ch;
|
||||||
|
max-width: calc(2.25ch * var(--days));
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#campsites-booking thead tr:last-child th {
|
#campsites-booking thead tr:last-child th {
|
||||||
|
@ -861,3 +869,52 @@ label[x-show] > span, label[x-show] > br {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*</editor-fold>*/
|
/*</editor-fold>*/
|
||||||
|
/*<editor-fold desc="Booking Form">*/
|
||||||
|
|
||||||
|
#booking-form > fieldset {
|
||||||
|
display: grid;
|
||||||
|
gap: .5em;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form fieldset fieldset, #booking-form .colspan {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form #campsites-booking {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form :is(label, fieldset fieldset),
|
||||||
|
#booking-form .booking-items table {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form :is(input:not([type='checkbox']), select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form :is(.customer-details, .booking-period) {
|
||||||
|
display: grid;
|
||||||
|
gap: .5em;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form .booking-items td {
|
||||||
|
padding: .3125rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form .booking-items br {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form .booking-items td:first-child input {
|
||||||
|
width: 6ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booking-form h3 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*</editor-fold>*/
|
||||||
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
|
SPDX-FileCopyrightText: 2023 Oriol Carbonell <info@oriolcarbonell.cat>
|
||||||
|
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 }}">
|
||||||
|
{{ with .CampsiteType -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Accommodation" "title" )}}<br>
|
||||||
|
<select name="{{ .Name }}"
|
||||||
|
required
|
||||||
|
data-hx-get="/admin/bookings/new" data-hx-trigger="change"
|
||||||
|
{{ template "error-attrs" . }}
|
||||||
|
>
|
||||||
|
<option value="">{{( gettext "Choose an accomodation" )}}</option>
|
||||||
|
{{ template "list-options" . }}
|
||||||
|
</select><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Dates -}}
|
||||||
|
<fieldset class="booking-period"
|
||||||
|
data-hx-get="/admin/bookings/new"
|
||||||
|
data-hx-trigger="change delay:500ms"
|
||||||
|
>
|
||||||
|
{{ with .ArrivalDate -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Arrival date" "input" )}}<br>
|
||||||
|
<input type="date" required
|
||||||
|
min="{{ formatDateAttr .MinDate }}"
|
||||||
|
max="{{ formatDateAttr .MaxDate }}"
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .DepartureDate -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Departure date" "input" )}}<br>
|
||||||
|
<input type="date" required
|
||||||
|
min="{{ formatDateAttr .MinDate }}"
|
||||||
|
max="{{ formatDateAttr .MaxDate }}"
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
</fieldset>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Options -}}
|
||||||
|
{{ with .ZonePreferences -}}
|
||||||
|
<label>{{( pgettext "Area preferences (optional)" "input" )}}<br>
|
||||||
|
<input type="text"
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
<a href="/{{ currentLocale }}/campground?zones" target="_blank">{{( gettext "Campground map" )}}</a><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{ with $guests := .Guests -}}
|
||||||
|
{{ $draft := $.Cart.Draft }}
|
||||||
|
<fieldset class="booking-items"
|
||||||
|
data-hx-get="/admin/bookings/new" data-hx-trigger="change"
|
||||||
|
>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{( pgettext "Units" "header" )}}</th>
|
||||||
|
<th scope="col">{{( pgettext "Decription" "header" )}}</th>
|
||||||
|
<th scope="col" class="numeric">{{( pgettext "Total" "header" )}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ $draft.NumNights }}</td>
|
||||||
|
<td>{{( pgettext "Night" "cart" )}}</td>
|
||||||
|
<td class="numeric">{{ formatPrice $draft.Nights }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ with .NumberAdults -}}
|
||||||
|
<tr>
|
||||||
|
<td><input id="adults" type="number" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}"
|
||||||
|
min="1"{{if not $guests.OverflowAllowed }} max="{{ $guests.MaxGuests }}"{{ end }}
|
||||||
|
{{ template "error-attrs" . }}
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label for="adults">{{( pgettext "Adults aged 17 or older" "input" )}}</label><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</td>
|
||||||
|
<td class="numeric">{{ formatPrice $draft.Adults }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .NumberTeenagers -}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input id="teenagers" type="number" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}"
|
||||||
|
min="0"{{if not $guests.OverflowAllowed }} max="{{ $guests.MaxGuests | dec }}"{{ end }}
|
||||||
|
{{ template "error-attrs" . }}
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label for="teenagers">{{( pgettext "Teenagers from 11 to 16 years old" "input" )}}</label><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</td>
|
||||||
|
<td class="numeric">{{ formatPrice $draft.Teenagers }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .NumberChildren -}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input id="children" type="number" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}"
|
||||||
|
min="0"{{if not $guests.OverflowAllowed }} max="{{ $guests.MaxGuests | dec }}"{{ end }}
|
||||||
|
{{ template "error-attrs" . }}
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label for="children">{{( pgettext "Children from 2 to 10 years old" "input" )}}</label><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</td>
|
||||||
|
<td class="numeric">{{ formatPrice $draft.Children }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .NumberDogs -}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input id="dogs" type="number" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" min="0"
|
||||||
|
{{ template "error-attrs" . }}
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label for="dogs">{{( pgettext "Dogs" "input" )}}</label><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</td>
|
||||||
|
<td class="numeric">{{ formatPrice $draft.Dogs }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ with $.Options -}}
|
||||||
|
{{ range .Options -}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input id="{{ .Input.Name }}" type="number" required
|
||||||
|
name="{{ .Input.Name }}" value="{{ .Input.Val }}"
|
||||||
|
min="{{ .Min }}" max="{{ .Max }}"
|
||||||
|
{{ template "error-attrs" .Input }}
|
||||||
|
>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label for="{{ .Input.Name }}">{{ .Label }}</label><br>
|
||||||
|
{{ template "error-message" .Input }}
|
||||||
|
</td>
|
||||||
|
<td class="numeric">{{ formatPrice .Subtotal }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ $draft.NumAdults }}</td>
|
||||||
|
<td>{{( pgettext "Tourist tax" "cart" )}}</td>
|
||||||
|
<td class="numeric">{{ formatPrice $draft.TouristTax }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" colspan="2" class="numeric">{{( pgettext "Total" "header" )}}</th>
|
||||||
|
<td class="numeric">{{ formatPrice $draft.Total }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
{{ if not .NumberDogs -}}
|
||||||
|
<small>{{( gettext "Note: This accommodation does <strong>not</strong> allow dogs.") | raw }}</small>
|
||||||
|
{{- end }}
|
||||||
|
{{ if .Error -}}
|
||||||
|
<p class="error">{{ .Error }}</p>
|
||||||
|
{{- end }}
|
||||||
|
</fieldset>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Customer -}}
|
||||||
|
<fieldset class="customer-details">
|
||||||
|
<legend>{{( pgettext "Customer Details" "title" )}}</legend>
|
||||||
|
{{ with .FullName -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Full name" "input" )}}<br>
|
||||||
|
<input type="text" required minlength="2"
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Country -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Country" "input" )}}<br>
|
||||||
|
<select name="{{ .Name }}"
|
||||||
|
required
|
||||||
|
{{ template "error-attrs" . }}
|
||||||
|
>
|
||||||
|
<option>{{( gettext "Choose a country" )}}</option>
|
||||||
|
{{ template "list-options" . }}
|
||||||
|
</select><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Address -}}
|
||||||
|
<label class="colspan">
|
||||||
|
{{( pgettext "Address" "input" )}}<br>
|
||||||
|
<input type="text" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .PostalCode -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Postcode" "input" )}}<br>
|
||||||
|
<input type="text" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .City -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Town or village" "input" )}}<br>
|
||||||
|
<input type="text"
|
||||||
|
required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Email -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Email" "input" )}}<br>
|
||||||
|
<input type="email" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Phone -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Phone" "input" )}}<br>
|
||||||
|
<input type="tel" required
|
||||||
|
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
|
||||||
|
><br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
{{ with $.Guests.ACSICard -}}
|
||||||
|
<label class="colspan" data-hx-get="/admin/bookings/new" data-hx-trigger="change">
|
||||||
|
<input type="checkbox" name="{{ .Name }}" {{ if .Checked}}checked{{ end }}
|
||||||
|
{{ template "error-attrs" . }}
|
||||||
|
> {{( pgettext "ACSI card? (optional)" "input" )}}<br>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
</label>
|
||||||
|
{{- end }}
|
||||||
|
</fieldset>
|
||||||
|
{{- end }}
|
||||||
|
{{ if .Campsites -}}
|
||||||
|
<h3>{{( pgettext "Campsites" "title" )}}</h3>
|
||||||
|
{{ template "grid.gohtml" . }}
|
||||||
|
{{- end }}
|
||||||
|
</fieldset>
|
||||||
|
<footer>
|
||||||
|
<button type="submit">
|
||||||
|
{{- if .ID -}}
|
||||||
|
{{( pgettext "Update" "action" )}}
|
||||||
|
{{- else -}}
|
||||||
|
{{( pgettext "Add" "action" )}}
|
||||||
|
{{- end -}}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{{ define "campsite-heading" -}}
|
||||||
|
<label><input type="checkbox"> {{ .Label }}</label>
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
{{ define "title" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.adminBookingForm*/ -}}
|
||||||
|
{{ if .ID }}
|
||||||
|
{{( pgettext "Edit Booking" "title" )}}
|
||||||
|
{{ else }}
|
||||||
|
{{( pgettext "New Booking" "title" )}}
|
||||||
|
{{ end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "breadcrumb" -}}
|
||||||
|
<li><a href="./">{{( pgettext "Bookings" "title" )}}</a></li>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "content" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.adminBookingForm*/ -}}
|
||||||
|
<h2>{{ template "title" .}}</h2>
|
||||||
|
<form id="booking-form"
|
||||||
|
data-hx-ext="morph"
|
||||||
|
data-hx-swap="morph:innerHTML"
|
||||||
|
data-hx-include="this"
|
||||||
|
data-hx-target="this"
|
||||||
|
data-hx-replace-url="true"
|
||||||
|
{{- if .ID }} data-hx-put="/admin/bookings/{{ .CurrentLabel }}"
|
||||||
|
{{- else }} action="/admin/bookings" method="post"
|
||||||
|
{{- end -}}
|
||||||
|
>
|
||||||
|
{{ template "fields.gohtml" . }}
|
||||||
|
</form>
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,50 @@
|
||||||
|
<div id="campsites-booking">
|
||||||
|
<table>
|
||||||
|
<colgroup></colgroup>
|
||||||
|
{{ range .Months }}
|
||||||
|
<colgroup>
|
||||||
|
{{ range .Spans }}
|
||||||
|
<col span="{{ .Count }}" class="{{ if .Today}}today {{ end }}{{ if .Weekend }}weekend{{ else }}weekday{{ end }}">
|
||||||
|
{{- end }}
|
||||||
|
</colgroup>
|
||||||
|
{{- end }}
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" rowspan="2">{{( pgettext "Label" "header" )}}</th>
|
||||||
|
{{ range .Months }}
|
||||||
|
<th scope="col" style="--days: {{ len .Days }}" colspan="{{ len .Days }}">{{ pgettext .Name "month" }} {{ .Year }}</th>
|
||||||
|
{{- end }}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{{ range .Months }}
|
||||||
|
{{ range .Days }}
|
||||||
|
<th scope="col">{{ .Day }}</th>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range $campsite := .Campsites -}}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
{{ template "campsite-heading" . }}
|
||||||
|
</th>
|
||||||
|
{{ range $.Months }}
|
||||||
|
{{ range $day := .Days }}
|
||||||
|
{{ with index $campsite.Bookings $day -}}
|
||||||
|
<td class="booking-{{ .Status }}">
|
||||||
|
<div class="booking-status"
|
||||||
|
style="--booking-nights: {{ .Nights }}; --booking-begin: {{ if .Begin }}1{{ else }}0{{ end }}; --booking-end: {{ if .End }}1{{ else }}0{{ end }}"
|
||||||
|
title="{{ .Holder }}"
|
||||||
|
>{{ .Holder }}</div>
|
||||||
|
</td>
|
||||||
|
{{- else -}}
|
||||||
|
<td></td>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
|
@ -17,60 +17,7 @@
|
||||||
|
|
||||||
<h2>{{( pgettext "Campsites" "title" )}}</h2>
|
<h2>{{( pgettext "Campsites" "title" )}}</h2>
|
||||||
{{ if .Campsites -}}
|
{{ if .Campsites -}}
|
||||||
<div id="campsites-booking">
|
{{ template "grid.gohtml" . }}
|
||||||
<table>
|
|
||||||
<colgroup></colgroup>
|
|
||||||
{{ range .Months }}
|
|
||||||
<colgroup>
|
|
||||||
{{ range .Spans }}
|
|
||||||
<col span="{{ .Count }}" class="{{ if .Weekend }}weekend{{ else }}weekday{{ end }}">
|
|
||||||
{{- end }}
|
|
||||||
</colgroup>
|
|
||||||
{{- end }}
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" rowspan="2">{{( pgettext "Label" "header" )}}</th>
|
|
||||||
{{ range .Months }}
|
|
||||||
<th scope="col" colspan="{{ len .Days }}">{{ pgettext .Name "month" }} {{ .Year }}</th>
|
|
||||||
{{- end }}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
{{ range .Months }}
|
|
||||||
{{ range .Days }}
|
|
||||||
<th scope="col">{{ .Day }}</th>
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ range $campsite := .Campsites -}}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
{{- if isAdmin -}}
|
|
||||||
<a href="/admin/campsites/{{ .Label }}">{{ .Label }}</a>
|
|
||||||
{{- else -}}
|
|
||||||
{{ .Label }}
|
|
||||||
{{- end -}}
|
|
||||||
</th>
|
|
||||||
{{ range $.Months }}
|
|
||||||
{{ range $day := .Days }}
|
|
||||||
{{ with index $campsite.Bookings $day -}}
|
|
||||||
<td class="booking-{{ .Status }}">
|
|
||||||
<div class="booking-status"
|
|
||||||
style="--booking-nights: {{ .Nights }}; --booking-begin: {{ if .Begin }}1{{ else }}0{{ end }}; --booking-end: {{ if .End }}1{{ else }}0{{ end }}"
|
|
||||||
title="{{ .Holder }}"
|
|
||||||
>{{ .Holder }}</div>
|
|
||||||
</td>
|
|
||||||
{{- else -}}
|
|
||||||
<td></td>
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
</tr>
|
|
||||||
{{- end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<form id="booking-filter">
|
<form id="booking-filter">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{{ with .From -}}
|
{{ with .From -}}
|
||||||
|
@ -96,3 +43,11 @@
|
||||||
<p>{{( gettext "No campsites added yet." )}}</p>
|
<p>{{( gettext "No campsites added yet." )}}</p>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "campsite-heading" -}}
|
||||||
|
{{- if isAdmin -}}
|
||||||
|
<a href="/admin/campsites/{{ .Label }}">{{ .Label }}</a>
|
||||||
|
{{- else -}}
|
||||||
|
{{ .Label }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end }}
|
||||||
|
|
Loading…
Reference in New Issue