Add filtering and pagination to customer section
This commit is contained in:
parent
50548c29ab
commit
3a7d454826
|
@ -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) {
|
||||||
|
|
|
@ -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 != ""
|
||||||
|
}
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 }}
|
Loading…
Reference in New Issue