From 3a7d4548262bf67caaab01277062efe0a5a2b435 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 3 May 2024 18:10:16 +0200 Subject: [PATCH] Add filtering and pagination to customer section --- pkg/customer/admin.go | 36 +++++--- pkg/customer/filter.go | 98 +++++++++++++++++++++ web/templates/admin/customer/index.gohtml | 65 +++++++++----- web/templates/admin/customer/results.gohtml | 19 ++++ 4 files changed, 188 insertions(+), 30 deletions(-) create mode 100644 pkg/customer/filter.go create mode 100644 web/templates/admin/customer/results.gohtml diff --git a/pkg/customer/admin.go b/pkg/customer/admin.go index 9dfc218..fb19109 100644 --- a/pkg/customer/admin.go +++ b/pkg/customer/admin.go @@ -2,6 +2,7 @@ package customer import ( "context" + "fmt" "net/http" "dev.tandem.ws/tandem/camper/pkg/auth" @@ -78,28 +79,37 @@ func (h *AdminHandler) customerHandler(user *auth.User, company *auth.Company, c } func serveCustomerIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - customers, err := collectCustomerEntries(r.Context(), conn, company) + filters := newFilterForm(company) + if err := filters.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + customers, err := collectCustomerEntries(r.Context(), conn, company, filters) if err != nil { panic(err) } page := &customerIndex{ - Customers: customers, + Customers: filters.buildCursor(customers), + Filters: filters, } page.MustRender(w, r, user, company) } -func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *auth.Company) ([]*customerEntry, error) { - rows, err := conn.Query(ctx, ` - select '/admin/customers/' || slug +func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *auth.Company, filters *filterForm) ([]*customerEntry, error) { + where, args := filters.BuildQuery(nil) + rows, err := conn.Query(ctx, fmt.Sprintf(` + select contact_id + , '/admin/customers/' || slug , name , coalesce(email::text, '') , coalesce(phone::text, '') from contact left join contact_email using (contact_id) left join contact_phone using (contact_id) - where company_id = $1 - order by name - `, company.ID) + where (%s) + order by name, contact_id + LIMIT %d + `, where, filters.perPage+1), args...) if err != nil { return nil, err } @@ -108,7 +118,7 @@ func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *a var customers []*customerEntry for rows.Next() { customer := &customerEntry{} - if err = rows.Scan(&customer.URL, &customer.Name, &customer.Email, &customer.Phone); err != nil { + if err = rows.Scan(&customer.ID, &customer.URL, &customer.Name, &customer.Email, &customer.Phone); err != nil { return nil, err } customers = append(customers, customer) @@ -118,6 +128,7 @@ func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *a } type customerEntry struct { + ID int URL string Name string Email string @@ -126,10 +137,15 @@ type customerEntry struct { type customerIndex struct { Customers []*customerEntry + Filters *filterForm } func (page *customerIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { - template.MustRenderAdmin(w, r, user, company, "customer/index.gohtml", page) + if httplib.IsHTMxRequest(r) && page.Filters.pagination { + template.MustRenderAdminNoLayout(w, r, user, company, "customer/results.gohtml", page) + } else { + template.MustRenderAdminFiles(w, r, user, company, page, "customer/index.gohtml", "customer/results.gohtml") + } } func addCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { diff --git a/pkg/customer/filter.go b/pkg/customer/filter.go new file mode 100644 index 0000000..9b00082 --- /dev/null +++ b/pkg/customer/filter.go @@ -0,0 +1,98 @@ +package customer + +import ( + "fmt" + "net/http" + "strings" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/form" +) + +type filterForm struct { + company *auth.Company + perPage int + pagination bool + Name *form.Input + Email *form.Input + Cursor *form.Input +} + +func newFilterForm(company *auth.Company) *filterForm { + return &filterForm{ + company: company, + perPage: 25, + Name: &form.Input{ + Name: "name", + }, + Email: &form.Input{ + Name: "email", + }, + Cursor: &form.Input{ + Name: "cursor", + }, + } +} + +func (f *filterForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Name.FillValue(r) + f.Email.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("company_id = $%d", f.company.ID) + maybeAppendWhere("name ILIKE $%d", f.Name.Val, func(v string) interface{} { + return "%" + v + "%" + }) + maybeAppendWhere("email ILIKE $%d", f.Email.Val, func(v string) interface{} { + return "%" + v + "%" + }) + + if f.Cursor.Val != "" { + params := strings.Split(f.Cursor.Val, ";") + if len(params) == 2 { + where = append(where, fmt.Sprintf("(name, contact_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(customers []*customerEntry) []*customerEntry { + if len(customers) <= f.perPage { + f.Cursor.Val = "" + return customers + } + customers = customers[:f.perPage] + last := customers[f.perPage-1] + f.Cursor.Val = fmt.Sprintf("%s;%d", last.Name, last.ID) + return customers +} + +func (f *filterForm) HasValue() bool { + return f.Name.Val != "" || + f.Email.Val != "" +} diff --git a/web/templates/admin/customer/index.gohtml b/web/templates/admin/customer/index.gohtml index 5704538..f55a428 100644 --- a/web/templates/admin/customer/index.gohtml +++ b/web/templates/admin/customer/index.gohtml @@ -12,27 +12,52 @@ {{ define "content" -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/customer.customerIndex*/ -}} {{( pgettext "Add Customer" "action" )}} + {{ template "filters-toggle" }} +
+ {{ with .Filters }} +
+ {{ with .Name -}} + + {{- end }} + {{ with .Email -}} + + {{- end }} +
+ {{ end }} + {{ if .Filters.HasValue }} + {{( pgettext "Reset" "action" )}} + {{ end }} +

{{ template "title" . }}

- - - - - - - - - - {{ range .Customers -}} + {{ if .Customers -}} +
{{( pgettext "Name" "header" )}}{{( pgettext "Email" "header" )}}{{( pgettext "Phone" "header" )}}
+ - - - + + + - {{- else -}} - - - - {{- end }} - -
{{ .Name }}{{ .Email }}{{ .Phone }}{{( pgettext "Name" "header" )}}{{( pgettext "Email" "header" )}}{{( pgettext "Phone" "header" )}}
{{( gettext "No customer found." )}}
+ + + {{ template "results.gohtml" . }} + + + {{- else }} +

{{( gettext "No customer found." )}}

+ {{- end }} {{- end }} diff --git a/web/templates/admin/customer/results.gohtml b/web/templates/admin/customer/results.gohtml new file mode 100644 index 0000000..26af933 --- /dev/null +++ b/web/templates/admin/customer/results.gohtml @@ -0,0 +1,19 @@ +{{ range .Customers -}} + + {{ .Name }} + {{ .Email }} + {{ .Phone }} + +{{- end }} +{{ if .Filters.Cursor.Val }} + + + {{ with .Filters -}} +
+ {{ with .Cursor -}}{{- end }} + +
+ {{- end }} + + +{{- end }}