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 }}