From 3aa53cf1a945a54a87586d127a77520df94fb497 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 23 Apr 2024 21:07:41 +0200 Subject: [PATCH] =?UTF-8?q?=E2=80=9CMockup=E2=80=9D=20for=20the=20new=20bo?= =?UTF-8?q?oking=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pkg/booking/admin.go | 73 +++++- pkg/booking/campsite.go | 155 ++++++++++++ pkg/booking/cart.go | 197 +++++++++------ pkg/booking/public.go | 11 +- pkg/campsite/admin.go | 155 +----------- pkg/template/render.go | 4 + web/static/camper.css | 59 ++++- web/templates/admin/booking/fields.gohtml | 283 ++++++++++++++++++++++ web/templates/admin/booking/form.gohtml | 33 +++ web/templates/admin/booking/grid.gohtml | 50 ++++ web/templates/admin/campsite/index.gohtml | 63 +---- 11 files changed, 799 insertions(+), 284 deletions(-) create mode 100644 pkg/booking/campsite.go create mode 100644 web/templates/admin/booking/fields.gohtml create mode 100644 web/templates/admin/booking/form.gohtml create mode 100644 web/templates/admin/booking/grid.gohtml diff --git a/pkg/booking/admin.go b/pkg/booking/admin.go index 64201dd..ced2d89 100644 --- a/pkg/booking/admin.go +++ b/pkg/booking/admin.go @@ -7,14 +7,16 @@ package booking import ( "context" - "golang.org/x/text/language" "net/http" "strings" "time" + "golang.org/x/text/language" + "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/template" ) @@ -38,6 +40,17 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat default: 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: 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) { rows, err := conn.Query(ctx, ` select left(slug::text, 10) - , '/admin/booking/' || slug + , '/admin/bookings/' || slug , lower(stay) , upper(stay) , 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) } } + +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") + } +} diff --git a/pkg/booking/campsite.go b/pkg/booking/campsite.go new file mode 100644 index 0000000..8a0d490 --- /dev/null +++ b/pkg/booking/campsite.go @@ -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 +} diff --git a/pkg/booking/cart.go b/pkg/booking/cart.go index 6f61583..d8920bd 100644 --- a/pkg/booking/cart.go +++ b/pkg/booking/cart.go @@ -10,6 +10,7 @@ import ( ) type bookingCart struct { + Draft *paymentDraft Lines []*cartLine Total string DownPayment string @@ -23,52 +24,80 @@ type cartLine struct { Subtotal string } -func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) { - cart := &bookingCart{ - Total: "0.0", - } +type paymentDraft struct { + ArrivalDate time.Time + 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 { - 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 { - 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 { - return cart, nil + return nil, 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 { - 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 { - 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 { - return cart, nil + return nil, nil } - numDogs := 0 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 { - return cart, nil + return nil, nil } } - zonePreferences := "" + 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 { - ACSICard = f.Guests.ACSICard.Checked + draft.ACSICard = f.Guests.ACSICard.Checked } 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) `, database.ZeroNullUUID(f.PaymentSlug.Val), - arrivalDate, - departureDate, + draft.ArrivalDate, + draft.DepartureDate, campsiteType, - numAdults, - numTeenagers, - numChildren, - numDogs, - zonePreferences, - ACSICard, + draft.NumAdults, + draft.NumTeenagers, + draft.NumChildren, + draft.NumDogs, + draft.ZonePreferences, + draft.ACSICard, 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( &f.PaymentSlug.Val, - &paymentID, - &numNights, - &nights, - &adults, - &teenagers, - &children, - &dogs, - &touristTax, - &total, - &downPayment, - &cart.DownPaymentPercent, + &draft.PaymentID, + &draft.NumNights, + &draft.Nights, + &draft.Adults, + &draft.Teenagers, + &draft.Children, + &draft.Dogs, + &draft.TouristTax, + &draft.Total, + &draft.DownPayment, + &draft.DownPaymentPercent, ); err != nil { if database.ErrorIsNotFound(err) { - return cart, nil + return nil, nil } 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, ` select campsite_type_option_id , units @@ -170,7 +174,8 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca join payment using (payment_id) join currency using (currency_code) where payment_id = $1 -`, paymentID) + order by campsite_type_option_id +`, draft.PaymentID) if err != nil { return nil, err } @@ -188,20 +193,62 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca if option == nil { 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 { return nil, rows.Err() } - maybeAddLine(numAdults, touristTax, locale.PgettextNoop("Tourist tax", "cart")) + return draft, nil +} - if total != "0.0" { - cart.Total = total +func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) { + 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 - if downPayment != total { - cart.DownPayment = downPayment + if draft.DownPayment != draft.Total { + cart.DownPayment = draft.DownPayment } } diff --git a/pkg/booking/public.go b/pkg/booking/public.go index 3f21c98..6ccd017 100644 --- a/pkg/booking/public.go +++ b/pkg/booking/public.go @@ -130,11 +130,12 @@ type bookingOptionFields struct { } type campsiteTypeOption struct { - ID int - Label string - Min int - Max int - Input *form.Input + ID int + Label string + Min int + Max int + Input *form.Input + Subtotal string } type bookingCustomerFields struct { diff --git a/pkg/campsite/admin.go b/pkg/campsite/admin.go index 214cd68..0003132 100644 --- a/pkg/campsite/admin.go +++ b/pkg/campsite/admin.go @@ -13,12 +13,12 @@ import ( "github.com/jackc/pgx/v4" "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/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/season" "dev.tandem.ws/tandem/camper/pkg/template" ) @@ -95,106 +95,21 @@ func serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User, panic(err) } 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 { panic(err) } - page.Months = collectMonths(page.From.Date(), page.To.Date()) + page.Months = booking.CollectMonths(from, to) 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 { From *form.Month To *form.Month - Campsites []*campsiteEntry - Months []*Month + Campsites []*booking.CampsiteEntry + Months []*booking.Month } func newCampsiteIndex() *campsiteIndex { @@ -224,62 +139,8 @@ func (page *campsiteIndex) Parse(r *http.Request) error { 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) { - 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) { diff --git a/pkg/template/render.go b/pkg/template/render.go index 79e791b..f68138e 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -56,6 +56,10 @@ func MustRenderAdminNoLayout(w io.Writer, r *http.Request, user *auth.User, comp 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{}) { layout := "layout.gohtml" mustRenderLayout(w, user, company, publicTemplateFile, data, layout, filename) diff --git a/web/static/camper.css b/web/static/camper.css index a081770..8cb28c7 100644 --- a/web/static/camper.css +++ b/web/static/camper.css @@ -772,7 +772,7 @@ label[x-show] > span, label[x-show] > br { /**/ #campsites-booking { - height: 90vh; + max-height: 90vh; overflow: scroll; } @@ -784,6 +784,10 @@ label[x-show] > span, label[x-show] > br { background-color: var(--camper--color--rosy); } +#campsites-booking .today { + border-left: 2px solid red; +} + #campsites-booking colgroup { border-right: 2px solid; } @@ -792,6 +796,7 @@ label[x-show] > span, label[x-show] > br { border: 1px solid; min-width: 2.25ch; max-width: 2.25ch; + width: 2.25ch; } #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 { padding: 0 .5ch; + max-width: calc(2.25ch * var(--days)); + overflow: hidden; + white-space: nowrap; } #campsites-booking thead tr:last-child th { @@ -861,3 +869,52 @@ label[x-show] > span, label[x-show] > br { } /**/ +/**/ + +#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; +} + +/**/ diff --git a/web/templates/admin/booking/fields.gohtml b/web/templates/admin/booking/fields.gohtml new file mode 100644 index 0000000..e6f4d2c --- /dev/null +++ b/web/templates/admin/booking/fields.gohtml @@ -0,0 +1,283 @@ + +{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.adminBookingForm*/ -}} +{{ CSRFInput }} +
+ + {{ with .CampsiteType -}} + + {{- end }} + {{ with .Dates -}} +
+ {{ with .ArrivalDate -}} + + {{- end }} + {{ with .DepartureDate -}} + + {{- end }} +
+ {{- end }} + {{ with .Options -}} + {{ with .ZonePreferences -}} + + {{- end }} + {{- end }} + {{ with $guests := .Guests -}} + {{ $draft := $.Cart.Draft }} +
+ + + + + + + + + + + + + + + {{ with .NumberAdults -}} + + + + + + {{- end }} + {{ with .NumberTeenagers -}} + + + + + + {{- end }} + {{ with .NumberChildren -}} + + + + + + {{- end }} + {{ with .NumberDogs -}} + + + + + + {{- end }} + {{ with $.Options -}} + {{ range .Options -}} + + + + + + {{- end }} + {{- end }} + + + + + + + + + + + + +
{{( pgettext "Units" "header" )}}{{( pgettext "Decription" "header" )}}{{( pgettext "Total" "header" )}}
{{ $draft.NumNights }}{{( pgettext "Night" "cart" )}}{{ formatPrice $draft.Nights }}
+ +
+ {{ template "error-message" . }} +
{{ formatPrice $draft.Adults }}
+ + +
+ {{ template "error-message" . }} +
{{ formatPrice $draft.Teenagers }}
+ + +
+ {{ template "error-message" . }} +
{{ formatPrice $draft.Children }}
+ + +
+ {{ template "error-message" . }} +
{{ formatPrice $draft.Dogs }}
+ + + +
+ {{ template "error-message" .Input }} +
{{ formatPrice .Subtotal }}
{{ $draft.NumAdults }}{{( pgettext "Tourist tax" "cart" )}}{{ formatPrice $draft.TouristTax }}
{{( pgettext "Total" "header" )}}{{ formatPrice $draft.Total }}
+ {{ if not .NumberDogs -}} + {{( gettext "Note: This accommodation does not allow dogs.") | raw }} + {{- end }} + {{ if .Error -}} +

{{ .Error }}

+ {{- end }} +
+ {{- end }} + {{ with .Customer -}} +
+ {{( pgettext "Customer Details" "title" )}} + {{ with .FullName -}} + + {{- end }} + {{ with .Country -}} + + {{- end }} + {{ with .Address -}} + + {{- end }} + {{ with .PostalCode -}} + + {{- end }} + {{ with .City -}} + + {{- end }} + {{ with .Email -}} + + {{- end }} + {{ with .Phone -}} + + {{- end }} + {{ with $.Guests.ACSICard -}} + + {{- end }} +
+ {{- end }} + {{ if .Campsites -}} +

{{( pgettext "Campsites" "title" )}}

+ {{ template "grid.gohtml" . }} + {{- end }} +
+ + +{{ define "campsite-heading" -}} + +{{- end }} diff --git a/web/templates/admin/booking/form.gohtml b/web/templates/admin/booking/form.gohtml new file mode 100644 index 0000000..d07a8d8 --- /dev/null +++ b/web/templates/admin/booking/form.gohtml @@ -0,0 +1,33 @@ + +{{ 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" -}} +
  • {{( pgettext "Bookings" "title" )}}
  • +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.adminBookingForm*/ -}} +

    {{ template "title" .}}

    +
    + {{ template "fields.gohtml" . }} +
    +{{- end }} diff --git a/web/templates/admin/booking/grid.gohtml b/web/templates/admin/booking/grid.gohtml new file mode 100644 index 0000000..e349233 --- /dev/null +++ b/web/templates/admin/booking/grid.gohtml @@ -0,0 +1,50 @@ +
    + + + {{ range .Months }} + + {{ range .Spans }} + + {{- end }} + + {{- end }} + + + + {{ range .Months }} + + {{- end }} + + + {{ range .Months }} + {{ range .Days }} + + {{- end }} + {{- end }} + + + + {{ range $campsite := .Campsites -}} + + + {{ range $.Months }} + {{ range $day := .Days }} + {{ with index $campsite.Bookings $day -}} + + {{- else -}} + + {{- end }} + {{- end }} + {{- end }} + + {{- end }} + +
    {{( pgettext "Label" "header" )}}{{ pgettext .Name "month" }} {{ .Year }}
    {{ .Day }}
    + {{ template "campsite-heading" . }} + +
    {{ .Holder }}
    +
    +
    diff --git a/web/templates/admin/campsite/index.gohtml b/web/templates/admin/campsite/index.gohtml index 1b49baf..ba19775 100644 --- a/web/templates/admin/campsite/index.gohtml +++ b/web/templates/admin/campsite/index.gohtml @@ -17,60 +17,7 @@

    {{( pgettext "Campsites" "title" )}}

    {{ if .Campsites -}} -
    - - - {{ range .Months }} - - {{ range .Spans }} - - {{- end }} - - {{- end }} - - - - {{ range .Months }} - - {{- end }} - - - {{ range .Months }} - {{ range .Days }} - - {{- end }} - {{- end }} - - - - {{ range $campsite := .Campsites -}} - - - {{ range $.Months }} - {{ range $day := .Days }} - {{ with index $campsite.Bookings $day -}} - - {{- else -}} - - {{- end }} - {{- end }} - {{- end }} - - {{- end }} - -
    {{( pgettext "Label" "header" )}}{{ pgettext .Name "month" }} {{ .Year }}
    {{ .Day }}
    - {{- if isAdmin -}} - {{ .Label }} - {{- else -}} - {{ .Label }} - {{- end -}} - -
    {{ .Holder }}
    -
    -
    + {{ template "grid.gohtml" . }}
    {{ with .From -}} @@ -96,3 +43,11 @@

    {{( gettext "No campsites added yet." )}}

    {{- end }} {{- end }} + +{{ define "campsite-heading" -}} + {{- if isAdmin -}} + {{ .Label }} + {{- else -}} + {{ .Label }} + {{- end -}} +{{- end }}