Compare commits

..

No commits in common. "7d895fe5f90d13caa970cdc8bed916fec3d24d0e" and "c2f6d299b47a0bcefb9af5bdcb293ba420a80986" have entirely different histories.

14 changed files with 59 additions and 354 deletions

View File

@ -27,7 +27,6 @@ type InputField struct {
Label string Label string
Type string Type string
Val string Val string
Is string
Required bool Required bool
Attributes []template.HTMLAttr Attributes []template.HTMLAttr
Errors []error Errors []error

View File

@ -6,11 +6,10 @@ import (
) )
const ( const (
HxLocation = "HX-Location" HxLocation = "HX-Location"
HxRefresh = "HX-Refresh" HxRefresh = "HX-Refresh"
HxRequest = "HX-Request" HxRequest = "HX-Request"
HxTrigger = "HX-Trigger" HxTrigger = "HX-Trigger"
HxTriggerAfterSettle = "HX-Trigger-After-Settle"
) )
type HTMxLocation struct { type HTMxLocation struct {

View File

@ -222,8 +222,7 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r) company := mustGetCompany(r)
slug := params[0].Value slug := params[0].Value
switch slug { if slug == "new" {
case "new":
locale := getLocale(r) locale := getLocale(r)
form := newInvoiceForm(r.Context(), conn, locale, company) form := newInvoiceForm(r.Context(), conn, locale, company)
if invoiceToDuplicate := r.URL.Query().Get("duplicate"); invoiceToDuplicate != "" { if invoiceToDuplicate := r.URL.Query().Get("duplicate"); invoiceToDuplicate != "" {
@ -234,42 +233,24 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
form.Date.Val = time.Now().Format("2006-01-02") form.Date.Val = time.Now().Format("2006-01-02")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
mustRenderNewInvoiceForm(w, r, form) mustRenderNewInvoiceForm(w, r, form)
case "product-form": return
company := mustGetCompany(r) }
query := r.URL.Query()
index, _ := strconv.Atoi(query.Get("index")) pdf := false
conn := getConn(r) if strings.HasSuffix(slug, ".pdf") {
form := newInvoiceProductForm(index, company, getLocale(r), mustGetTaxOptions(r.Context(), conn, company)) pdf = true
slug := query.Get("slug") slug = slug[:len(slug)-len(".pdf")]
if len(slug) > 0 { }
if !form.MustFillFromDatabase(r.Context(), conn, slug) { inv := mustGetInvoice(r.Context(), conn, company, slug)
http.NotFound(w, r) if inv == nil {
return http.NotFound(w, r)
} return
quantity, _ := strconv.Atoi(query.Get("product.quantity." + strconv.Itoa(index))) }
if quantity > 0 { if pdf {
form.Quantity.Val = strconv.Itoa(quantity) w.Header().Set("Content-Type", "application/pdf")
} mustWriteInvoicePdf(w, r, inv)
w.Header().Set(HxTriggerAfterSettle, "recompute") } else {
} mustRenderMainTemplate(w, r, "invoices/view.gohtml", inv)
mustRenderStandaloneTemplate(w, r, "invoices/product-form.gohtml", form)
default:
pdf := false
if strings.HasSuffix(slug, ".pdf") {
pdf = true
slug = slug[:len(slug)-len(".pdf")]
}
inv := mustGetInvoice(r.Context(), conn, company, slug)
if inv == nil {
http.NotFound(w, r)
return
}
if pdf {
w.Header().Set("Content-Type", "application/pdf")
mustWriteInvoicePdf(w, r, inv)
} else {
mustRenderMainTemplate(w, r, "invoices/view.gohtml", inv)
}
} }
} }
@ -662,8 +643,7 @@ func (form *invoiceForm) Update() {
for n, product := range products { for n, product := range products {
if product.Quantity.Val != "0" { if product.Quantity.Val != "0" {
if n != len(form.Products) { if n != len(form.Products) {
product.Index = len(form.Products) product.Reindex(len(form.Products))
product.Rename()
} }
form.Products = append(form.Products, product) form.Products = append(form.Products, product)
} }
@ -726,7 +706,6 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel
type invoiceProductForm struct { type invoiceProductForm struct {
locale *Locale locale *Locale
company *Company company *Company
Index int
InvoiceProductId *InputField InvoiceProductId *InputField
ProductId *InputField ProductId *InputField
Name *InputField Name *InputField
@ -741,7 +720,6 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
form := &invoiceProductForm{ form := &invoiceProductForm{
locale: locale, locale: locale,
company: company, company: company,
Index: index,
InvoiceProductId: &InputField{ InvoiceProductId: &InputField{
Label: pgettext("input", "Id", locale), Label: pgettext("input", "Id", locale),
Type: "hidden", Type: "hidden",
@ -756,15 +734,6 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
Label: pgettext("input", "Name", locale), Label: pgettext("input", "Name", locale),
Type: "text", Type: "text",
Required: true, Required: true,
Is: "numerus-product-search",
Attributes: []template.HTMLAttr{
`autocomplete="off"`,
`data-hx-trigger="keyup changed delay:200"`,
`data-hx-target="next .options"`,
`data-hx-indicator="closest div"`,
`data-hx-swap="innerHTML"`,
template.HTMLAttr(fmt.Sprintf(`data-hx-get="%v"`, companyURI(company, "/search/products"))),
},
}, },
Description: &InputField{ Description: &InputField{
Label: pgettext("input", "Description", locale), Label: pgettext("input", "Description", locale),
@ -802,12 +771,12 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
Options: taxOptions, Options: taxOptions,
}, },
} }
form.Rename() form.Reindex(index)
return form return form
} }
func (form *invoiceProductForm) Rename() { func (form *invoiceProductForm) Reindex(index int) {
suffix := "." + strconv.Itoa(form.Index) suffix := "." + strconv.Itoa(index)
form.InvoiceProductId.Name = "product.invoice_product_id" + suffix form.InvoiceProductId.Name = "product.invoice_product_id" + suffix
form.ProductId.Name = "product.id" + suffix form.ProductId.Name = "product.id" + suffix
form.Name.Name = "product.name" + suffix form.Name.Name = "product.name" + suffix
@ -851,35 +820,6 @@ func (form *invoiceProductForm) Validate() bool {
return validator.AllOK() return validator.AllOK()
} }
func (form *invoiceProductForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
select product_id
, name
, description
, to_price(price, decimal_digits)
, 1 as quantity
, 0 as discount
, array_remove(array_agg(tax_id), null)
from product
join company using (company_id)
join currency using (currency_code)
left join product_tax using (product_id)
where product.slug = $1
group by product_id
, name
, description
, price
, decimal_digits
`, slug).Scan(
form.ProductId,
form.Name,
form.Description,
form.Price,
form.Quantity,
form.Discount,
form.Tax))
}
func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) locale := getLocale(r)
conn := getConn(r) conn := getConn(r)

View File

@ -8,7 +8,6 @@ import (
"math" "math"
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
type ProductEntry struct { type ProductEntry struct {
@ -20,21 +19,14 @@ type ProductEntry struct {
type productsIndexPage struct { type productsIndexPage struct {
Products []*ProductEntry Products []*ProductEntry
Filters *productFilterForm
} }
func IndexProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func IndexProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r) company := mustGetCompany(r)
locale := getLocale(r) tag := r.URL.Query().Get("tag")
filters := newProductFilterForm(locale)
if err := filters.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
page := &productsIndexPage{ page := &productsIndexPage{
Products: mustCollectProductEntries(r.Context(), conn, company, filters), Products: mustCollectProductEntries(r.Context(), conn, company, tag),
Filters: filters,
} }
mustRenderMainTemplate(w, r, "products/index.gohtml", page) mustRenderMainTemplate(w, r, "products/index.gohtml", page)
} }
@ -160,72 +152,8 @@ func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprout
} }
} }
type productFilterForm struct { func mustCollectProductEntries(ctx context.Context, conn *Conn, company *Company, tag string) []*ProductEntry {
Name *InputField rows, err := conn.Query(ctx, `
Tags *TagsField
TagsCondition *ToggleField
}
func newProductFilterForm(locale *Locale) *productFilterForm {
return &productFilterForm{
Name: &InputField{
Name: "number",
Label: pgettext("input", "Invoice Number", locale),
Type: "search",
},
Tags: &TagsField{
Name: "tags",
Label: pgettext("input", "Tags", locale),
},
TagsCondition: &ToggleField{
Name: "tags_condition",
Label: pgettext("input", "Tags Condition", locale),
Selected: "and",
FirstOption: &ToggleOption{
Value: "and",
Label: pgettext("tag condition", "All", locale),
Description: gettext("Invoices must have all the specified labels.", locale),
},
SecondOption: &ToggleOption{
Value: "or",
Label: pgettext("tag condition", "Any", locale),
Description: gettext("Invoices must have at least one of the specified labels.", locale),
},
},
}
}
func (form *productFilterForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Name.FillValue(r)
form.Tags.FillValue(r)
form.TagsCondition.FillValue(r)
return nil
}
func mustCollectProductEntries(ctx context.Context, conn *Conn, company *Company, filters *productFilterForm) []*ProductEntry {
args := []interface{}{company.Id}
where := []string{"product.company_id = $1"}
appendWhere := func(expression string, value interface{}) {
args = append(args, value)
where = append(where, fmt.Sprintf(expression, len(args)))
}
if filters != nil {
name := strings.TrimSpace(filters.Name.String())
if name != "" {
appendWhere("product.name ilike $%d", "%"+name+"%")
}
if len(filters.Tags.Tags) > 0 {
if filters.TagsCondition.Selected == "and" {
appendWhere("product.tags @> $%d", filters.Tags)
} else {
appendWhere("product.tags && $%d", filters.Tags)
}
}
}
rows := conn.MustQuery(ctx, fmt.Sprintf(`
select product.slug select product.slug
, product.name , product.name
, to_price(price, decimal_digits) , to_price(price, decimal_digits)
@ -233,15 +161,19 @@ func mustCollectProductEntries(ctx context.Context, conn *Conn, company *Company
from product from product
join company using (company_id) join company using (company_id)
join currency using (currency_code) join currency using (currency_code)
where (%s) where product.company_id = $1 and (($2 = '') or (tags @> array[$2]::tag_name[]))
order by name order by name
`, strings.Join(where, ") AND (")), args...) `, company.Id, tag)
if err != nil {
panic(err)
}
defer rows.Close() defer rows.Close()
var entries []*ProductEntry var entries []*ProductEntry
for rows.Next() { for rows.Next() {
entry := &ProductEntry{} entry := &ProductEntry{}
if err := rows.Scan(&entry.Slug, &entry.Name, &entry.Price, &entry.Tags); err != nil { err = rows.Scan(&entry.Slug, &entry.Name, &entry.Price, &entry.Tags)
if err != nil {
panic(err) panic(err)
} }
entries = append(entries, entry) entries = append(entries, entry)
@ -349,15 +281,3 @@ func (form *productForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
form.Tax, form.Tax,
form.Tags)) form.Tags))
} }
func HandleProductSearch(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
filters := newProductFilterForm(getLocale(r))
query := r.URL.Query()
index := query.Get("index")
filters.Name.Val = strings.TrimSpace(query.Get("product.name." + index))
var products []*ProductEntry
if len(filters.Name.Val) > 0 {
products = mustCollectProductEntries(r.Context(), getConn(r), mustGetCompany(r), filters)
}
mustRenderStandaloneTemplate(w, r, "products/search.gohtml", products)
}

View File

@ -33,7 +33,6 @@ func NewRouter(db *Db) http.Handler {
companyRouter.POST("/invoices/:slug/edit", HandleEditInvoiceAction) companyRouter.POST("/invoices/:slug/edit", HandleEditInvoiceAction)
companyRouter.PUT("/invoices/:slug/tags", HandleUpdateInvoiceTags) companyRouter.PUT("/invoices/:slug/tags", HandleUpdateInvoiceTags)
companyRouter.GET("/invoices/:slug/tags/edit", ServeEditInvoiceTags) companyRouter.GET("/invoices/:slug/tags/edit", ServeEditInvoiceTags)
companyRouter.GET("/search/products", HandleProductSearch)
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
mustRenderMainTemplate(w, r, "dashboard.gohtml", nil) mustRenderMainTemplate(w, r, "dashboard.gohtml", nil)
}) })

View File

@ -1,52 +0,0 @@
<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#494949">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite"/>
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite"/>
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite"/>
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite"/>
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite"/>
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite"/>
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite"/>
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite"/>
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite"/>
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite"/>
</rect>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -614,8 +614,7 @@ main > nav {
max-width: 35rem; max-width: 35rem;
} }
[is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags, [is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags, [is="numerus-multiselect"] .options {
[is="numerus-multiselect"] .options, [is="numerus-product-search"] .options {
font-size: 1em; font-size: 1em;
list-style: none; list-style: none;
color: var(--numerus--text-color); color: var(--numerus--text-color);
@ -683,7 +682,6 @@ main > nav {
min-width: initial; min-width: initial;
} }
[is="numerus-product-search"] .options,
[is="numerus-multiselect"] .options { [is="numerus-multiselect"] .options {
padding: 0; padding: 0;
border-top: 0; border-top: 0;
@ -693,7 +691,6 @@ main > nav {
z-index: 10; z-index: 10;
} }
[is="numerus-product-search"] .options li,
[is="numerus-multiselect"] .options li { [is="numerus-multiselect"] .options li {
display: block; display: block;
width: 100%; width: 100%;
@ -701,8 +698,7 @@ main > nav {
padding: 1rem 2rem; padding: 1rem 2rem;
} }
[is="numerus-product-search"] .options li:hover,[is="numerus-product-search"] .options .highlight, [is="numerus-multiselect"] .options li:hover, [is="numerus-multiselect"] .options .highlight {
[is="numerus-multiselect"] .options li:hover,[is="numerus-multiselect"] .options .highlight {
background-color: var(--numerus--color--light-gray); background-color: var(--numerus--color--light-gray);
} }

View File

@ -506,52 +506,8 @@ class Tags extends HTMLDivElement {
} }
} }
class ProductSearch extends HTMLDivElement {
constructor() {
super();
this.initialized = false;
this.tags = [];
}
connectedCallback() {
if (!this.isConnected) {
return;
}
if (!this.initialized) {
for (const child of this.children) {
switch (child.nodeName) {
case 'INPUT':
this.input = child;
break;
case 'LABEL':
this.label = child;
break;
}
}
if (this.label && this.input) {
this.init();
}
}
}
init() {
this.initialized = true;
const list = document.createElement('ul');
list.classList.add('options')
list.setAttribute('role', 'listbox')
this.append(list);
const indicator = document.createElement('img');
indicator.classList.add('htmx-indicator');
indicator.src = '/static/bars.svg';
this.append(indicator);
}
}
customElements.define('numerus-multiselect', Multiselect, {extends: 'div'}); customElements.define('numerus-multiselect', Multiselect, {extends: 'div'});
customElements.define('numerus-tags', Tags, {extends: 'div'}); customElements.define('numerus-tags', Tags, {extends: 'div'});
customElements.define('numerus-product-search', ProductSearch, {extends: 'div'});
let savedTitle = ''; let savedTitle = '';

View File

@ -7,7 +7,7 @@
{{ define "input-field" -}} {{ define "input-field" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}}
<div class="input{{ if .Errors }} has-errors{{ end }}"{{ if .Is }} is="{{ .Is }}"{{ end }}> <div class="input {{ if .Errors }}has-errors{{ end }}">
{{ if eq .Type "textarea" }} {{ if eq .Type "textarea" }}
<textarea name="{{ .Name }}" id="{{ .Name }}-field" <textarea name="{{ .Name }}" id="{{ .Name }}-field"
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }} {{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
@ -112,18 +112,11 @@
{{ define "invoice-product-form" -}} {{ define "invoice-product-form" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
<fieldset class="new-invoice-product" {{ template "hidden-field" .ProductId }}
data-hx-select="unset" {{ template "input-field" .Name }}
data-hx-vals='{"index": {{ .Index }}}' {{ template "input-field" .Price }}
data-hx-include="[name='product.quantity.{{ .Index }}']" {{ template "input-field" .Quantity }}
> {{ template "input-field" .Discount }}
{{ template "hidden-field" .InvoiceProductId }} {{ template "input-field" .Description }}
{{ template "hidden-field" .ProductId }} {{ template "select-field" .Tax }}
{{ template "input-field" .Name }}
{{ template "input-field" .Price }}
{{ template "input-field" .Quantity }}
{{ template "input-field" .Discount }}
{{ template "input-field" .Description }}
{{ template "select-field" .Tax }}
</fieldset>
{{- end }} {{- end }}

View File

@ -30,11 +30,14 @@
{{ template "input-field" .Notes }} {{ template "input-field" .Notes }}
{{- range $product := .Products }} {{- range $product := .Products }}
{{ template "invoice-product-form" . }} <fieldset class="new-invoice-product">
{{ template "hidden-field" .InvoiceProductId }}
{{ template "invoice-product-form" . }}
</fieldset>
{{- end }} {{- end }}
{{- end }} {{- end }}
<table id="invoice-summary"> <table>
<tbody> <tbody>
<tr> <tr>
<th scope="row">{{(pgettext "Subtotal" "title")}}</th> <th scope="row">{{(pgettext "Subtotal" "title")}}</th>
@ -56,13 +59,8 @@
<fieldset> <fieldset>
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit" <button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
name="action" value="select-products" name="action" value="select-products"
data-hx-get="{{ companyURI "/invoices/product-form" }}"
data-hx-target="#invoice-summary" data-hx-swap="beforebegin"
data-hx-select="unset"
data-hx-vals="js:{index: document.querySelectorAll('.new-invoice-product').length}"
type="submit">{{( pgettext "Add products" "action" )}}</button> type="submit">{{( pgettext "Add products" "action" )}}</button>
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit" <button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
id="recompute-button"
name="action" value="update" name="action" value="update"
type="submit">{{( pgettext "Update" "action" )}}</button> type="submit">{{( pgettext "Update" "action" )}}</button>
<button class="primary" name="_method" value="PUT" <button class="primary" name="_method" value="PUT"
@ -71,10 +69,4 @@
</form> </form>
</section> </section>
<script>
document.body.addEventListener('recompute', function () {
document.getElementById('recompute-button').click();
});
</script>
{{- end }} {{- end }}

View File

@ -30,11 +30,13 @@
{{ template "input-field" .Notes }} {{ template "input-field" .Notes }}
{{- range $product := .Products }} {{- range $product := .Products }}
{{ template "invoice-product-form" . }} <fieldset class="new-invoice-product">
{{ template "invoice-product-form" . }}
</fieldset>
{{- end }} {{- end }}
{{- end }} {{- end }}
<table id="invoice-summary"> <table>
<tbody> <tbody>
<tr> <tr>
<th scope="row">{{(pgettext "Subtotal" "title")}}</th> <th scope="row">{{(pgettext "Subtotal" "title")}}</th>
@ -56,24 +58,14 @@
<fieldset> <fieldset>
<button formnovalidate formaction="{{ companyURI "/invoices/new" }}" <button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
name="action" value="select-products" name="action" value="select-products"
data-hx-get="{{ companyURI "/invoices/product-form" }}"
data-hx-target="#invoice-summary" data-hx-swap="beforebegin"
data-hx-select="unset"
data-hx-vals="js:{index: document.querySelectorAll('.new-invoice-product').length}"
type="submit">{{( pgettext "Add products" "action" )}}</button> type="submit">{{( pgettext "Add products" "action" )}}</button>
<button formnovalidate formaction="{{ companyURI "/invoices/new" }}" <button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
id="recompute-button"
name="action" value="update" name="action" value="update"
type="submit">{{( pgettext "Update" "action" )}}</button> type="submit">{{( pgettext "Update" "action" )}}</button>
<button class="primary" name="action" value="add" <button class="primary" name="action" value="add"
type="submit">{{( pgettext "New invoice" "action" )}}</button> type="submit">{{( pgettext "New invoice" "action" )}}</button>
</fieldset> </fieldset>
</form> </form>
</section> </section>
<script>
document.body.addEventListener('recompute', function () {
document.getElementById('recompute-button').click();
});
</script>
{{- end }} {{- end }}

View File

@ -1,4 +0,0 @@
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
{{ template "invoice-product-form" . }}
{{- end }}

View File

@ -19,20 +19,6 @@
{{ define "content" }} {{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productsIndexPage*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productsIndexPage*/ -}}
<div aria-label="{{( pgettext "Filters" "title" )}}">
<form method="GET" action="{{ companyURI "/products"}}"
data-hx-target="main"
data-hx-boost="true"
data-hx-trigger="change,search,submit"
>
{{ with .Filters }}
{{ template "input-field" .Name }}
{{ template "tags-field" .Tags | addTagsAttr (print `data-conditions="` .TagsCondition.Name `-field"`) }}
{{ template "toggle-field" .TagsCondition }}
{{ end }}
<button type="submit">{{( pgettext "Filter" "action" )}}</button>
</form>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -51,7 +37,7 @@
<td> <td>
{{- range $index, $tag := .Tags }} {{- range $index, $tag := .Tags }}
{{- if gt $index 0 }}, {{ end -}} {{- if gt $index 0 }}, {{ end -}}
<a href="?tags={{ . }}" data-hx-target="main" data-hx-boost="true">{{ . }}</a> <a href="?tag={{ . }}" data-hx-target="main" data-hx-boost="true">{{ . }}</a>
{{- end }} {{- end }}
</td> </td>
<td class="numeric">{{ .Price | formatPrice }}</td> <td class="numeric">{{ .Price | formatPrice }}</td>

View File

@ -1,11 +0,0 @@
{{ define "content" }}
{{- /*gotype: []dev.tandem.ws/tandem/numerus/pkg.ProductEntry*/ -}}
{{- range $product := . }}
<li
data-hx-get="{{ companyURI "/invoices/product-form" }}?slug={{ .Slug }}"
data-hx-target="closest fieldset"
data-hx-swap="outerHTML"
data-hx-trigger="click"
>{{ .Name }}</li>
{{- end }}
{{- end }}