Add filtering and pagination to customer section

This commit is contained in:
jordi fita mas 2024-05-03 18:10:16 +02:00
parent 50548c29ab
commit 3a7d454826
4 changed files with 188 additions and 30 deletions

View File

@ -2,6 +2,7 @@ package customer
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"dev.tandem.ws/tandem/camper/pkg/auth" "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) { 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 { if err != nil {
panic(err) panic(err)
} }
page := &customerIndex{ page := &customerIndex{
Customers: customers, Customers: filters.buildCursor(customers),
Filters: filters,
} }
page.MustRender(w, r, user, company) page.MustRender(w, r, user, company)
} }
func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *auth.Company) ([]*customerEntry, error) { func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *auth.Company, filters *filterForm) ([]*customerEntry, error) {
rows, err := conn.Query(ctx, ` where, args := filters.BuildQuery(nil)
select '/admin/customers/' || slug rows, err := conn.Query(ctx, fmt.Sprintf(`
select contact_id
, '/admin/customers/' || slug
, name , name
, coalesce(email::text, '') , coalesce(email::text, '')
, coalesce(phone::text, '') , coalesce(phone::text, '')
from contact from contact
left join contact_email using (contact_id) left join contact_email using (contact_id)
left join contact_phone using (contact_id) left join contact_phone using (contact_id)
where company_id = $1 where (%s)
order by name order by name, contact_id
`, company.ID) LIMIT %d
`, where, filters.perPage+1), args...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -108,7 +118,7 @@ func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *a
var customers []*customerEntry var customers []*customerEntry
for rows.Next() { for rows.Next() {
customer := &customerEntry{} 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 return nil, err
} }
customers = append(customers, customer) customers = append(customers, customer)
@ -118,6 +128,7 @@ func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *a
} }
type customerEntry struct { type customerEntry struct {
ID int
URL string URL string
Name string Name string
Email string Email string
@ -126,10 +137,15 @@ type customerEntry struct {
type customerIndex struct { type customerIndex struct {
Customers []*customerEntry Customers []*customerEntry
Filters *filterForm
} }
func (page *customerIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { 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) { func addCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {

98
pkg/customer/filter.go Normal file
View File

@ -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 != ""
}

View File

@ -12,27 +12,52 @@
{{ define "content" -}} {{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/customer.customerIndex*/ -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/customer.customerIndex*/ -}}
<a href="/admin/customers/new">{{( pgettext "Add Customer" "action" )}}</a> <a href="/admin/customers/new">{{( pgettext "Add Customer" "action" )}}</a>
{{ template "filters-toggle" }}
<form class="filters" method="GET" action="/admin/customers"
data-hx-target="main" data-hx-boost="true" data-hx-trigger="change,search,submit"
aria-labelledby="filters-toggle"
>
{{ with .Filters }}
<fieldset>
{{ with .Name -}}
<label>
{{( pgettext "Name" "input" )}}<br>
<input type="text"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
{{ template "error-message" . }}
</label>
{{- end }}
{{ with .Email -}}
<label>
{{( pgettext "Email" "input" )}}<br>
<input type="text"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
{{ template "error-message" . }}
</label>
{{- end }}
</fieldset>
{{ end }}
{{ if .Filters.HasValue }}
<a href="/admin/customers" class="button">{{( pgettext "Reset" "action" )}}</a>
{{ end }}
</form>
<h2>{{ template "title" . }}</h2> <h2>{{ template "title" . }}</h2>
<table> {{ if .Customers -}}
<thead> <table>
<tr> <thead>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Email" "header" )}}</th>
<th scope="col">{{( pgettext "Phone" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ range .Customers -}}
<tr> <tr>
<td><a href="{{ .URL }}">{{ .Name }}</a></td> <th scope="col">{{( pgettext "Name" "header" )}}</th>
<td>{{ .Email }}</td> <th scope="col">{{( pgettext "Email" "header" )}}</th>
<td>{{ .Phone }}</td> <th scope="col">{{( pgettext "Phone" "header" )}}</th>
</tr> </tr>
{{- else -}} </thead>
<tr> <tbody>
<td colspan="3">{{( gettext "No customer found." )}}</td> {{ template "results.gohtml" . }}
</tr> </tbody>
{{- end }} </table>
</tbody> {{- else }}
</table> <p>{{( gettext "No customer found." )}}</p>
{{- end }}
{{- end }} {{- end }}

View File

@ -0,0 +1,19 @@
{{ range .Customers -}}
<tr>
<td><a href="{{ .URL }}">{{ .Name }}</a></td>
<td>{{ .Email }}</td>
<td>{{ .Phone }}</td>
</tr>
{{- end }}
{{ if .Filters.Cursor.Val }}
<tr>
<td colspan="3">
{{ with .Filters -}}
<form method="get" data-hx-push-url="false" data-hx-boost="true" data-hx-target="closest tr" data-hx-swap="outerHTML">
{{ with .Cursor -}}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{- end }}
<button type="submit">{{( pgettext "Load more" "action" )}}</button>
</form>
{{- end }}
<td>
</tr>
{{- end }}