From 5d4fe15e88cb502ff40a55cfa75390d6bc0a4672 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 3 May 2024 00:28:48 +0200 Subject: [PATCH] Add filtering and pagination for bookings --- pkg/booking/admin.go | 157 +++++++++++++++++++-- web/templates/admin/booking/index.gohtml | 64 +++++++-- web/templates/admin/booking/results.gohtml | 28 ++++ 3 files changed, 228 insertions(+), 21 deletions(-) create mode 100644 web/templates/admin/booking/results.gohtml diff --git a/pkg/booking/admin.go b/pkg/booking/admin.go index 59acc3f..4ace3aa 100644 --- a/pkg/booking/admin.go +++ b/pkg/booking/admin.go @@ -9,6 +9,7 @@ import ( "context" "dev.tandem.ws/tandem/camper/pkg/ods" "errors" + "fmt" "net/http" "strconv" "strings" @@ -137,17 +138,27 @@ func (h *AdminHandler) bookingHandler(user *auth.User, company *auth.Company, co } func serveBookingIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language) + filters := newFilterForm(r.Context(), conn, company, user.Locale) + if err := filters.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language, filters) if err != nil { panic(err) } - page := bookingIndex(bookings) + page := &bookingIndex{ + Bookings: filters.buildCursor(bookings), + Filters: filters, + } page.MustRender(w, r, user, company) } -func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag) ([]*bookingEntry, error) { - rows, err := conn.Query(ctx, ` - select left(slug::text, 10) +func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag, filters *filterForm) ([]*bookingEntry, error) { + where, args := filters.BuildQuery([]interface{}{lang.String()}) + rows, err := conn.Query(ctx, fmt.Sprintf(` + select booking_id + , left(slug::text, 10) , '/admin/bookings/' || slug , lower(stay) , upper(stay) @@ -157,8 +168,11 @@ func collectBookingEntries(ctx context.Context, conn *database.Conn, lang langua from booking join booking_status as status using (booking_status) left join booking_status_i18n as i18n on status.booking_status = i18n.booking_status and i18n.lang_tag = $1 - order by stay desc - `, lang) + where (%s) + order by lower(stay) desc + , booking_id desc + LIMIT %d + `, where, filters.perPage+1), args...) if err != nil { return nil, err } @@ -167,7 +181,7 @@ func collectBookingEntries(ctx context.Context, conn *database.Conn, lang langua var entries []*bookingEntry for rows.Next() { entry := &bookingEntry{} - if err = rows.Scan(&entry.Reference, &entry.URL, &entry.ArrivalDate, &entry.DepartureDate, &entry.HolderName, &entry.Status, &entry.StatusLabel); err != nil { + if err = rows.Scan(&entry.ID, &entry.Reference, &entry.URL, &entry.ArrivalDate, &entry.DepartureDate, &entry.HolderName, &entry.Status, &entry.StatusLabel); err != nil { return nil, err } entries = append(entries, entry) @@ -177,6 +191,7 @@ func collectBookingEntries(ctx context.Context, conn *database.Conn, lang langua } type bookingEntry struct { + ID int Reference string URL string ArrivalDate time.Time @@ -186,7 +201,10 @@ type bookingEntry struct { StatusLabel string } -type bookingIndex []*bookingEntry +type bookingIndex struct { + Bookings []*bookingEntry + Filters *filterForm +} func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { switch r.URL.Query().Get("format") { @@ -198,7 +216,7 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user "Holder Name", "Status", } - table, err := ods.WriteTable(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error { + table, err := ods.WriteTable(page.Bookings, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error { if err := ods.WriteCellString(sb, entry.Reference); err != nil { return err } @@ -217,10 +235,127 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user } ods.MustWriteResponse(w, table, user.Locale.Pgettext("bookings.ods", "filename")) default: - template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page) + if httplib.IsHTMxRequest(r) && page.Filters.pagination { + template.MustRenderAdminNoLayout(w, r, user, company, "booking/results.gohtml", page) + } else { + template.MustRenderAdminFiles(w, r, user, company, page, "booking/index.gohtml", "booking/results.gohtml") + } } } +type filterForm struct { + locale *locale.Locale + company *auth.Company + perPage int + pagination bool + HolderName *form.Input + BookingStatus *form.Select + FromDate *form.Input + ToDate *form.Input + Cursor *form.Input +} + +func newFilterForm(ctx context.Context, conn *database.Conn, company *auth.Company, locale *locale.Locale) *filterForm { + return &filterForm{ + locale: locale, + company: company, + perPage: 25, + HolderName: &form.Input{ + Name: "holder_name", + }, + BookingStatus: &form.Select{ + Name: "booking_status", + Options: mustGetBookingStatusOptions(ctx, conn, locale), + }, + FromDate: &form.Input{ + Name: "from_date", + }, + ToDate: &form.Input{ + Name: "to_date", + }, + Cursor: &form.Input{ + Name: "cursor", + }, + } +} + +func mustGetBookingStatusOptions(ctx context.Context, conn *database.Conn, locale *locale.Locale) []*form.Option { + return form.MustGetOptions(ctx, conn, ` + select booking_status.booking_status + , isi18n.name + from booking_status + join booking_status_i18n isi18n using(booking_status) + where isi18n.lang_tag = $1 + order by booking_status`, locale.Language) +} + +func (f *filterForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.HolderName.FillValue(r) + f.BookingStatus.FillValue(r) + f.FromDate.FillValue(r) + f.ToDate.FillValue(r) + f.Cursor.FillValue(r) + f.pagination = f.Cursor.Val != "" + return nil +} + +func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) { + var where []string + appendWhere := func(expression string, value interface{}) { + args = append(args, value) + where = append(where, fmt.Sprintf(expression, len(args))) + } + maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) { + if value != "" { + if conv == nil { + appendWhere(expression, value) + } else { + appendWhere(expression, conv(value)) + } + } + } + + appendWhere("booking.company_id = $%d", f.company.ID) + maybeAppendWhere("booking.holder_name ILIKE $%d", f.HolderName.Val, func(v string) interface{} { + return "%" + v + "%" + }) + maybeAppendWhere("booking.booking_status = $%d", f.BookingStatus.String(), nil) + maybeAppendWhere("lower(stay) >= $%d", f.FromDate.Val, nil) + maybeAppendWhere("lower(stay) <= $%d", f.ToDate.Val, nil) + + if f.Cursor.Val != "" { + params := strings.Split(f.Cursor.Val, ";") + if len(params) == 2 { + where = append(where, fmt.Sprintf("(lower(stay), booking_id) < ($%d, $%d)", len(args)+1, len(args)+2)) + args = append(args, params[0]) + args = append(args, params[1]) + } + } + + return strings.Join(where, ") AND ("), args +} + +func (f *filterForm) buildCursor(bookings []*bookingEntry) []*bookingEntry { + if len(bookings) <= f.perPage { + f.Cursor.Val = "" + return bookings + } + bookings = bookings[:f.perPage] + last := bookings[f.perPage-1] + f.Cursor.Val = fmt.Sprintf("%s;%d", last.ArrivalDate.Format(database.ISODateFormat), last.ID) + return bookings +} + +func (f *filterForm) HasValue() bool { + return f.HolderName.Val != "" || + (len(f.BookingStatus.Selected) > 0 && f.BookingStatus.Selected[0] != "") || + f.FromDate.Val != "" || + f.ToDate.Val != "" +} + type adminBookingForm struct { *bookingForm ID int diff --git a/web/templates/admin/booking/index.gohtml b/web/templates/admin/booking/index.gohtml index 60d89cc..b9a0f1e 100644 --- a/web/templates/admin/booking/index.gohtml +++ b/web/templates/admin/booking/index.gohtml @@ -13,8 +13,60 @@ {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.bookingIndex*/ -}} {{( pgettext "Add Booking" "action" )}} {{( pgettext "Export Bookings" "action" )}} + {{ template "filters-toggle" }} +
+ {{ with .Filters }} +
+ {{ with .HolderName -}} + + {{- end }} + {{ with .BookingStatus -}} + + {{- end }} + {{ with .FromDate -}} + + {{- end }} + {{ with .ToDate -}} + + {{- end }} +
+ {{ end }} + {{ if .Filters.HasValue }} + {{( pgettext "Reset" "action" )}} + {{ end }} +

{{( pgettext "Bookings" "title" )}}

- {{ if . -}} + {{ if .Bookings -}} @@ -26,15 +78,7 @@ - {{ range . -}} - - - - - - - - {{- end }} + {{ template "results.gohtml" . }}
{{ .Reference }}{{ .ArrivalDate | formatDate }}{{ .DepartureDate | formatDate }}{{ .HolderName }}{{ .StatusLabel }}
{{ else -}} diff --git a/web/templates/admin/booking/results.gohtml b/web/templates/admin/booking/results.gohtml new file mode 100644 index 0000000..3d3c102 --- /dev/null +++ b/web/templates/admin/booking/results.gohtml @@ -0,0 +1,28 @@ +{{ range .Bookings -}} + + {{ .Reference }} + {{ .ArrivalDate | formatDate }} + {{ .DepartureDate | formatDate }} + {{ .HolderName }} + {{ .StatusLabel }} + +{{- end }} +{{ if .Filters.Cursor.Val }} + + + {{ with .Filters -}} +
+ {{ with .HolderName -}}{{- end }} + {{ with .BookingStatus -}}{{- end }} + {{ with .FromDate -}}{{- end }} + {{ with .ToDate -}}{{- end }} + {{ with .Cursor -}}{{- end }} + +
+ {{- end }} + + +{{- end }}