Compare commits
No commits in common. "7d895fe5f90d13caa970cdc8bed916fec3d24d0e" and "c2f6d299b47a0bcefb9af5bdcb293ba420a80986" have entirely different histories.
7d895fe5f9
...
c2f6d299b4
|
@ -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
|
||||||
|
|
|
@ -10,7 +10,6 @@ const (
|
||||||
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 {
|
||||||
|
|
|
@ -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,26 +233,9 @@ 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":
|
|
||||||
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
|
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
|
pdf := false
|
||||||
if strings.HasSuffix(slug, ".pdf") {
|
if strings.HasSuffix(slug, ".pdf") {
|
||||||
pdf = true
|
pdf = true
|
||||||
|
@ -270,7 +252,6 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
|
||||||
} else {
|
} else {
|
||||||
mustRenderMainTemplate(w, r, "invoices/view.gohtml", inv)
|
mustRenderMainTemplate(w, r, "invoices/view.gohtml", inv)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustWriteInvoicePdf(w io.Writer, r *http.Request, inv *invoice) {
|
func mustWriteInvoicePdf(w io.Writer, r *http.Request, inv *invoice) {
|
||||||
|
@ -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)
|
||||||
|
|
102
pkg/products.go
102
pkg/products.go
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 |
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 = '';
|
||||||
|
|
||||||
|
|
|
@ -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,12 +112,6 @@
|
||||||
|
|
||||||
{{ 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"
|
|
||||||
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 "hidden-field" .ProductId }}
|
||||||
{{ template "input-field" .Name }}
|
{{ template "input-field" .Name }}
|
||||||
{{ template "input-field" .Price }}
|
{{ template "input-field" .Price }}
|
||||||
|
@ -125,5 +119,4 @@
|
||||||
{{ template "input-field" .Discount }}
|
{{ template "input-field" .Discount }}
|
||||||
{{ template "input-field" .Description }}
|
{{ template "input-field" .Description }}
|
||||||
{{ template "select-field" .Tax }}
|
{{ template "select-field" .Tax }}
|
||||||
</fieldset>
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -30,11 +30,14 @@
|
||||||
{{ template "input-field" .Notes }}
|
{{ template "input-field" .Notes }}
|
||||||
|
|
||||||
{{- range $product := .Products }}
|
{{- range $product := .Products }}
|
||||||
|
<fieldset class="new-invoice-product">
|
||||||
|
{{ template "hidden-field" .InvoiceProductId }}
|
||||||
{{ template "invoice-product-form" . }}
|
{{ 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 }}
|
||||||
|
|
|
@ -30,11 +30,13 @@
|
||||||
{{ template "input-field" .Notes }}
|
{{ template "input-field" .Notes }}
|
||||||
|
|
||||||
{{- range $product := .Products }}
|
{{- range $product := .Products }}
|
||||||
|
<fieldset class="new-invoice-product">
|
||||||
{{ template "invoice-product-form" . }}
|
{{ 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 }}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{{ define "content" }}
|
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
|
|
||||||
{{ template "invoice-product-form" . }}
|
|
||||||
{{- end }}
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }}
|
|
Loading…
Reference in New Issue