Compare commits
2 Commits
c2f6d299b4
...
7d895fe5f9
Author | SHA1 | Date |
---|---|---|
jordi fita mas | 7d895fe5f9 | |
jordi fita mas | 2ced61d304 |
|
@ -27,6 +27,7 @@ type InputField struct {
|
|||
Label string
|
||||
Type string
|
||||
Val string
|
||||
Is string
|
||||
Required bool
|
||||
Attributes []template.HTMLAttr
|
||||
Errors []error
|
||||
|
|
|
@ -6,10 +6,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
HxLocation = "HX-Location"
|
||||
HxRefresh = "HX-Refresh"
|
||||
HxRequest = "HX-Request"
|
||||
HxTrigger = "HX-Trigger"
|
||||
HxLocation = "HX-Location"
|
||||
HxRefresh = "HX-Refresh"
|
||||
HxRequest = "HX-Request"
|
||||
HxTrigger = "HX-Trigger"
|
||||
HxTriggerAfterSettle = "HX-Trigger-After-Settle"
|
||||
)
|
||||
|
||||
type HTMxLocation struct {
|
||||
|
|
106
pkg/invoices.go
106
pkg/invoices.go
|
@ -222,7 +222,8 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
|
|||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
slug := params[0].Value
|
||||
if slug == "new" {
|
||||
switch slug {
|
||||
case "new":
|
||||
locale := getLocale(r)
|
||||
form := newInvoiceForm(r.Context(), conn, locale, company)
|
||||
if invoiceToDuplicate := r.URL.Query().Get("duplicate"); invoiceToDuplicate != "" {
|
||||
|
@ -233,24 +234,42 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
|
|||
form.Date.Val = time.Now().Format("2006-01-02")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
mustRenderNewInvoiceForm(w, r, form)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
case "product-form":
|
||||
company := mustGetCompany(r)
|
||||
query := r.URL.Query()
|
||||
index, _ := strconv.Atoi(query.Get("index"))
|
||||
conn := getConn(r)
|
||||
form := newInvoiceProductForm(index, company, getLocale(r), mustGetTaxOptions(r.Context(), conn, company))
|
||||
slug := query.Get("slug")
|
||||
if len(slug) > 0 {
|
||||
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
quantity, _ := strconv.Atoi(query.Get("product.quantity." + strconv.Itoa(index)))
|
||||
if quantity > 0 {
|
||||
form.Quantity.Val = strconv.Itoa(quantity)
|
||||
}
|
||||
w.Header().Set(HxTriggerAfterSettle, "recompute")
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -643,7 +662,8 @@ func (form *invoiceForm) Update() {
|
|||
for n, product := range products {
|
||||
if product.Quantity.Val != "0" {
|
||||
if n != len(form.Products) {
|
||||
product.Reindex(len(form.Products))
|
||||
product.Index = len(form.Products)
|
||||
product.Rename()
|
||||
}
|
||||
form.Products = append(form.Products, product)
|
||||
}
|
||||
|
@ -706,6 +726,7 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel
|
|||
type invoiceProductForm struct {
|
||||
locale *Locale
|
||||
company *Company
|
||||
Index int
|
||||
InvoiceProductId *InputField
|
||||
ProductId *InputField
|
||||
Name *InputField
|
||||
|
@ -720,6 +741,7 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
|
|||
form := &invoiceProductForm{
|
||||
locale: locale,
|
||||
company: company,
|
||||
Index: index,
|
||||
InvoiceProductId: &InputField{
|
||||
Label: pgettext("input", "Id", locale),
|
||||
Type: "hidden",
|
||||
|
@ -734,6 +756,15 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
|
|||
Label: pgettext("input", "Name", locale),
|
||||
Type: "text",
|
||||
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{
|
||||
Label: pgettext("input", "Description", locale),
|
||||
|
@ -771,12 +802,12 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
|
|||
Options: taxOptions,
|
||||
},
|
||||
}
|
||||
form.Reindex(index)
|
||||
form.Rename()
|
||||
return form
|
||||
}
|
||||
|
||||
func (form *invoiceProductForm) Reindex(index int) {
|
||||
suffix := "." + strconv.Itoa(index)
|
||||
func (form *invoiceProductForm) Rename() {
|
||||
suffix := "." + strconv.Itoa(form.Index)
|
||||
form.InvoiceProductId.Name = "product.invoice_product_id" + suffix
|
||||
form.ProductId.Name = "product.id" + suffix
|
||||
form.Name.Name = "product.name" + suffix
|
||||
|
@ -820,6 +851,35 @@ func (form *invoiceProductForm) Validate() bool {
|
|||
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) {
|
||||
locale := getLocale(r)
|
||||
conn := getConn(r)
|
||||
|
|
102
pkg/products.go
102
pkg/products.go
|
@ -8,6 +8,7 @@ import (
|
|||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ProductEntry struct {
|
||||
|
@ -19,14 +20,21 @@ type ProductEntry struct {
|
|||
|
||||
type productsIndexPage struct {
|
||||
Products []*ProductEntry
|
||||
Filters *productFilterForm
|
||||
}
|
||||
|
||||
func IndexProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
tag := r.URL.Query().Get("tag")
|
||||
locale := getLocale(r)
|
||||
filters := newProductFilterForm(locale)
|
||||
if err := filters.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
page := &productsIndexPage{
|
||||
Products: mustCollectProductEntries(r.Context(), conn, company, tag),
|
||||
Products: mustCollectProductEntries(r.Context(), conn, company, filters),
|
||||
Filters: filters,
|
||||
}
|
||||
mustRenderMainTemplate(w, r, "products/index.gohtml", page)
|
||||
}
|
||||
|
@ -152,8 +160,72 @@ func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprout
|
|||
}
|
||||
}
|
||||
|
||||
func mustCollectProductEntries(ctx context.Context, conn *Conn, company *Company, tag string) []*ProductEntry {
|
||||
rows, err := conn.Query(ctx, `
|
||||
type productFilterForm struct {
|
||||
Name *InputField
|
||||
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
|
||||
, product.name
|
||||
, to_price(price, decimal_digits)
|
||||
|
@ -161,19 +233,15 @@ func mustCollectProductEntries(ctx context.Context, conn *Conn, company *Company
|
|||
from product
|
||||
join company using (company_id)
|
||||
join currency using (currency_code)
|
||||
where product.company_id = $1 and (($2 = '') or (tags @> array[$2]::tag_name[]))
|
||||
where (%s)
|
||||
order by name
|
||||
`, company.Id, tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
`, strings.Join(where, ") AND (")), args...)
|
||||
defer rows.Close()
|
||||
|
||||
var entries []*ProductEntry
|
||||
for rows.Next() {
|
||||
entry := &ProductEntry{}
|
||||
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Price, &entry.Tags)
|
||||
if err != nil {
|
||||
if err := rows.Scan(&entry.Slug, &entry.Name, &entry.Price, &entry.Tags); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
|
@ -281,3 +349,15 @@ func (form *productForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
|
|||
form.Tax,
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ func NewRouter(db *Db) http.Handler {
|
|||
companyRouter.POST("/invoices/:slug/edit", HandleEditInvoiceAction)
|
||||
companyRouter.PUT("/invoices/:slug/tags", HandleUpdateInvoiceTags)
|
||||
companyRouter.GET("/invoices/:slug/tags/edit", ServeEditInvoiceTags)
|
||||
companyRouter.GET("/search/products", HandleProductSearch)
|
||||
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
mustRenderMainTemplate(w, r, "dashboard.gohtml", nil)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<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>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -614,7 +614,8 @@ main > nav {
|
|||
max-width: 35rem;
|
||||
}
|
||||
|
||||
[is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags, [is="numerus-multiselect"] .options {
|
||||
[is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags,
|
||||
[is="numerus-multiselect"] .options, [is="numerus-product-search"] .options {
|
||||
font-size: 1em;
|
||||
list-style: none;
|
||||
color: var(--numerus--text-color);
|
||||
|
@ -682,6 +683,7 @@ main > nav {
|
|||
min-width: initial;
|
||||
}
|
||||
|
||||
[is="numerus-product-search"] .options,
|
||||
[is="numerus-multiselect"] .options {
|
||||
padding: 0;
|
||||
border-top: 0;
|
||||
|
@ -691,6 +693,7 @@ main > nav {
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
[is="numerus-product-search"] .options li,
|
||||
[is="numerus-multiselect"] .options li {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
@ -698,7 +701,8 @@ main > nav {
|
|||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
[is="numerus-multiselect"] .options li:hover, [is="numerus-multiselect"] .options .highlight {
|
||||
[is="numerus-product-search"] .options li:hover,[is="numerus-product-search"] .options .highlight,
|
||||
[is="numerus-multiselect"] .options li:hover,[is="numerus-multiselect"] .options .highlight {
|
||||
background-color: var(--numerus--color--light-gray);
|
||||
}
|
||||
|
||||
|
|
|
@ -506,8 +506,52 @@ 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-tags', Tags, {extends: 'div'});
|
||||
customElements.define('numerus-product-search', ProductSearch, {extends: 'div'});
|
||||
|
||||
let savedTitle = '';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
{{ define "input-field" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}}
|
||||
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
||||
<div class="input{{ if .Errors }} has-errors{{ end }}"{{ if .Is }} is="{{ .Is }}"{{ end }}>
|
||||
{{ if eq .Type "textarea" }}
|
||||
<textarea name="{{ .Name }}" id="{{ .Name }}-field"
|
||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
||||
|
@ -112,11 +112,18 @@
|
|||
|
||||
{{ define "invoice-product-form" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ 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 class="new-invoice-product"
|
||||
data-hx-select="unset"
|
||||
data-hx-vals='{"index": {{ .Index }}}'
|
||||
data-hx-include="[name='product.quantity.{{ .Index }}']"
|
||||
>
|
||||
{{ template "hidden-field" .InvoiceProductId }}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ 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 }}
|
||||
|
|
|
@ -30,14 +30,11 @@
|
|||
{{ template "input-field" .Notes }}
|
||||
|
||||
{{- range $product := .Products }}
|
||||
<fieldset class="new-invoice-product">
|
||||
{{ template "hidden-field" .InvoiceProductId }}
|
||||
{{ template "invoice-product-form" . }}
|
||||
</fieldset>
|
||||
{{ template "invoice-product-form" . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<table>
|
||||
<table id="invoice-summary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{(pgettext "Subtotal" "title")}}</th>
|
||||
|
@ -59,8 +56,13 @@
|
|||
<fieldset>
|
||||
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
|
||||
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>
|
||||
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
|
||||
id="recompute-button"
|
||||
name="action" value="update"
|
||||
type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||
<button class="primary" name="_method" value="PUT"
|
||||
|
@ -69,4 +71,10 @@
|
|||
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.body.addEventListener('recompute', function () {
|
||||
document.getElementById('recompute-button').click();
|
||||
});
|
||||
</script>
|
||||
{{- end }}
|
||||
|
|
|
@ -30,13 +30,11 @@
|
|||
{{ template "input-field" .Notes }}
|
||||
|
||||
{{- range $product := .Products }}
|
||||
<fieldset class="new-invoice-product">
|
||||
{{ template "invoice-product-form" . }}
|
||||
</fieldset>
|
||||
{{ template "invoice-product-form" . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<table>
|
||||
<table id="invoice-summary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{(pgettext "Subtotal" "title")}}</th>
|
||||
|
@ -58,14 +56,24 @@
|
|||
<fieldset>
|
||||
<button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
|
||||
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>
|
||||
<button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
|
||||
id="recompute-button"
|
||||
name="action" value="update"
|
||||
type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||
<button class="primary" name="action" value="add"
|
||||
type="submit">{{( pgettext "New invoice" "action" )}}</button>
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.body.addEventListener('recompute', function () {
|
||||
document.getElementById('recompute-button').click();
|
||||
});
|
||||
</script>
|
||||
{{- end }}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
|
||||
{{ template "invoice-product-form" . }}
|
||||
{{- end }}
|
|
@ -19,6 +19,20 @@
|
|||
|
||||
{{ define "content" }}
|
||||
{{- /*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>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -37,7 +51,7 @@
|
|||
<td>
|
||||
{{- range $index, $tag := .Tags }}
|
||||
{{- if gt $index 0 }}, {{ end -}}
|
||||
<a href="?tag={{ . }}" data-hx-target="main" data-hx-boost="true">{{ . }}</a>
|
||||
<a href="?tags={{ . }}" data-hx-target="main" data-hx-boost="true">{{ . }}</a>
|
||||
{{- end }}
|
||||
</td>
|
||||
<td class="numeric">{{ .Price | formatPrice }}</td>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{{ 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 }}
|
Loading…
Reference in New Issue