Compare commits

..

No commits in common. "e9cc331ee06cb80ac1610cdb95ede11e445ab058" and "55980367bc0abad4fd0430fe8de865a5f871dd8d" have entirely different histories.

20 changed files with 204 additions and 887 deletions

View File

@ -1,42 +0,0 @@
-- Deploy numerus:product to pg
-- requires: schema_numerus
-- requires: company
begin;
set search_path to numerus, public;
create table product (
product_id serial primary key,
company_id integer not null references company,
slug uuid not null default gen_random_uuid(),
name text not null,
description text not null,
price integer not null,
tax_id integer not null references tax,
created_at timestamptz not null default current_timestamp
);
comment on column product.price is
'Price is stored in cents.';
grant select, insert, update, delete on table product to invoicer;
grant select, insert, update, delete on table product to admin;
grant usage on sequence product_product_id_seq to invoicer;
grant usage on sequence product_product_id_seq to admin;
alter table product enable row level security;
create policy company_policy
on product
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = product.company_id
)
);
commit;

View File

@ -144,7 +144,7 @@ func HandleCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httpr
}
company := mustGetCompany(r)
conn.MustExec(r.Context(), "update company set business_name = $1, vatin = ($11 || $2)::vatin, trade_name = $3, phone = parse_packed_phone_number($4, $11), email = $5, web = $6, address = $7, city = $8, province = $9, postal_code = $10, country_code = $11, currency_code = $12 where company_id = $13", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency, company.Id)
http.Redirect(w, r, companyURI(company, "/tax-details"), http.StatusSeeOther)
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
return
}
@ -260,8 +260,9 @@ func HandleDeleteCompanyTax(w http.ResponseWriter, r *http.Request, params httpr
return
}
conn := getConn(r)
company := mustGetCompany(r)
conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId)
http.Redirect(w, r, companyURI(mustGetCompany(r), "/tax-details"), http.StatusSeeOther)
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
}
func HandleAddCompanyTax(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@ -283,5 +284,5 @@ func HandleAddCompanyTax(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
conn := getConn(r)
company := mustGetCompany(r)
conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, form.Name, form.Rate.Integer())
http.Redirect(w, r, companyURI(company, "/tax-details"), http.StatusSeeOther)
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
}

View File

@ -21,11 +21,11 @@ type ContactsIndexPage struct {
func IndexContacts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
company := mustGetCompany(r)
company := getCompany(r)
page := &ContactsIndexPage{
Contacts: mustGetContactEntries(r.Context(), conn, company),
}
mustRenderAppTemplate(w, r, "contacts/index.gohtml", page)
mustRenderAppTemplate(w, r, "contacts-index.gohtml", page)
}
func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
@ -52,11 +52,11 @@ func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
}
func mustRenderNewContactForm(w http.ResponseWriter, r *http.Request, form *contactForm) {
mustRenderAppTemplate(w, r, "contacts/new.gohtml", form)
mustRenderAppTemplate(w, r, "contacts-new.gohtml", form)
}
func mustRenderEditContactForm(w http.ResponseWriter, r *http.Request, form *contactForm) {
mustRenderAppTemplate(w, r, "contacts/edit.gohtml", form)
mustRenderAppTemplate(w, r, "contacts-edit.gohtml", form)
}
func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@ -75,9 +75,9 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param
mustRenderNewContactForm(w, r, form)
return
}
company := mustGetCompany(r)
company := getCompany(r)
conn.MustExec(r.Context(), "insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) values ($1, $2, ($12 || $3)::vatin, $4, parse_packed_phone_number($5, $12), $6, $7, $8, $9, $10, $11, $12)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country)
http.Redirect(w, r, companyURI(company, "/contacts"), http.StatusSeeOther)
http.Redirect(w, r, "/company/"+company.Slug+"/contacts", http.StatusSeeOther)
}
func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
@ -100,7 +100,8 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout
if slug == "" {
http.NotFound(w, r)
}
http.Redirect(w, r, companyURI(mustGetCompany(r), "/contacts/"+slug), http.StatusSeeOther)
company := getCompany(r)
http.Redirect(w, r, "/company/"+company.Slug+"/contacts/"+slug, http.StatusSeeOther)
}
func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []*ContactEntry {
@ -222,7 +223,7 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor
},
Country: &SelectField{
Name: "country",
Label: pgettext("input", "Tax", locale),
Label: pgettext("input", "Country", locale),
Options: mustGetCountryOptions(ctx, conn, locale),
Selected: "ES",
Attributes: []template.HTMLAttr{

View File

@ -1,189 +0,0 @@
package pkg
import (
"context"
"github.com/jackc/pgx/v4"
"github.com/julienschmidt/httprouter"
"html/template"
"math"
"net/http"
)
type ProductEntry struct {
Slug string
Name string
Price int
}
type productsIndexPage struct {
Products []*ProductEntry
}
func IndexProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
company := mustGetCompany(r)
page := &productsIndexPage{
Products: mustGetProductEntries(r.Context(), conn, company),
}
mustRenderAppTemplate(w, r, "products/index.gohtml", page)
}
func GetProductForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newProductForm(r.Context(), conn, locale, company)
slug := params[0].Value
if slug == "new" {
w.WriteHeader(http.StatusOK)
mustRenderNewProductForm(w, r, form)
return
}
err := conn.QueryRow(r.Context(), "select name, description, price, tax_id from product where slug = $1", slug).Scan(form.Name, form.Description, form.Price, form.Tax)
if err != nil {
if err == pgx.ErrNoRows {
http.NotFound(w, r)
return
} else {
panic(err)
}
}
w.WriteHeader(http.StatusOK)
mustRenderEditProductForm(w, r, form)
}
func mustRenderNewProductForm(w http.ResponseWriter, r *http.Request, form *productForm) {
mustRenderAppTemplate(w, r, "products/new.gohtml", form)
}
func mustRenderEditProductForm(w http.ResponseWriter, r *http.Request, form *productForm) {
mustRenderAppTemplate(w, r, "products/edit.gohtml", form)
}
func HandleAddProduct(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
company := mustGetCompany(r)
form := newProductForm(r.Context(), conn, locale, company)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !form.Validate() {
mustRenderNewProductForm(w, r, form)
return
}
conn.MustExec(r.Context(), "insert into product (company_id, name, description, price, tax_id) values ($1, $2, $3, $4, $5)", company.Id, form.Name, form.Description, form.Price, form.Tax)
http.Redirect(w, r, companyURI(company, "/products"), http.StatusSeeOther)
}
func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
company := mustGetCompany(r)
form := newProductForm(r.Context(), conn, locale, company)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !form.Validate() {
mustRenderEditProductForm(w, r, form)
return
}
slug := conn.MustGetText(r.Context(), "", "update product set name = $1, description = $2, price = $3, tax_id = $4 where slug = $5 returning slug", form.Name, form.Description, form.Price, form.Tax, params[0].Value)
if slug == "" {
http.NotFound(w, r)
}
http.Redirect(w, r, companyURI(company, "/products/"+slug), http.StatusSeeOther)
}
func mustGetProductEntries(ctx context.Context, conn *Conn, company *Company) []*ProductEntry {
rows, err := conn.Query(ctx, "select slug, name, price from product where company_id = $1 order by name", company.Id)
if err != nil {
panic(err)
}
defer rows.Close()
var entries []*ProductEntry
for rows.Next() {
entry := &ProductEntry{}
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Price)
if err != nil {
panic(err)
}
entries = append(entries, entry)
}
if rows.Err() != nil {
panic(rows.Err())
}
return entries
}
type productForm struct {
locale *Locale
Name *InputField
Description *InputField
Price *InputField
Tax *SelectField
}
func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *productForm {
return &productForm{
locale: locale,
Name: &InputField{
Name: "name",
Label: pgettext("input", "Name", locale),
Type: "text",
Required: true,
},
Description: &InputField{
Name: "description",
Label: pgettext("input", "Description", locale),
Type: "text",
},
Price: &InputField{
Name: "price",
Label: pgettext("input", "Price", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
`min="0"`,
},
},
Tax: &SelectField{
Name: "tax",
Label: pgettext("input", "Tax", locale),
Options: MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id),
},
}
}
func (form *productForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Name.FillValue(r)
form.Description.FillValue(r)
form.Price.FillValue(r)
form.Tax.FillValue(r)
return nil
}
func (form *productForm) Validate() bool {
validator := newFormValidator()
validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale))
if validator.CheckRequiredInput(form.Price, gettext("Price can not be empty.", form.locale)) {
validator.CheckValidInteger(form.Price, 0, math.MaxInt, gettext("Price must be a number greater than zero.", form.locale))
}
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
return validator.AllOK()
}

View File

@ -130,8 +130,8 @@ func HandleProfileForm(w http.ResponseWriter, r *http.Request, _ httprouter.Para
if form.Password.Val != "" {
conn.MustExec(r.Context(), "select change_password($1)", form.Password)
}
company := mustGetCompany(r)
http.Redirect(w, r, companyURI(company, "/profile"), http.StatusSeeOther)
company := getCompany(r)
http.Redirect(w, r, "/company/"+company.Slug+"/profile", http.StatusSeeOther)
}
func mustRenderProfileForm(w http.ResponseWriter, r *http.Request, form *profileForm) {

View File

@ -18,10 +18,6 @@ func NewRouter(db *Db) http.Handler {
companyRouter.POST("/contacts", HandleAddContact)
companyRouter.GET("/contacts/:slug", GetContactForm)
companyRouter.PUT("/contacts/:slug", HandleUpdateContact)
companyRouter.GET("/products", IndexProducts)
companyRouter.POST("/products", HandleAddProduct)
companyRouter.GET("/products/:slug", GetProductForm)
companyRouter.PUT("/products/:slug", HandleUpdateProduct)
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
})
@ -42,10 +38,8 @@ func NewRouter(db *Db) http.Handler {
user := getUser(r)
if user.LoggedIn {
conn := getConn(r)
company := &Company{
Slug: conn.MustGetText(r.Context(), "", "select slug::text from company order by company_id limit 1"),
}
http.Redirect(w, r, companyURI(company, "/"), http.StatusFound)
slug := conn.MustGetText(r.Context(), "", "select slug::text from company order by company_id limit 1")
http.Redirect(w, r, "/company/"+slug+"/", http.StatusFound)
} else {
http.Redirect(w, r, "/login", http.StatusSeeOther)
}

View File

@ -25,7 +25,10 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
return locale.Language.String()
},
"companyURI": func(uri string) string {
return companyURI(company, uri)
if company == nil {
return uri
}
return "/company/" + company.Slug + uri
},
"csrfToken": func() template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, csrfTokenField, user.CsrfToken))
@ -53,13 +56,6 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
}
}
func companyURI(company *Company, uri string) string {
if company == nil {
return uri
}
return "/company/" + company.Slug + uri
}
func overrideMethodField(method string) template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, overrideMethodName, method))
}

265
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-02-04 11:24+0100\n"
"POT-Creation-Date: 2023-02-03 13:57+0100\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -44,73 +44,9 @@ msgstr "Tauler"
#: web/template/app.gohtml:44
msgctxt "nav"
msgid "Products"
msgstr "Productes"
#: web/template/app.gohtml:45
msgctxt "nav"
msgid "Contacts"
msgstr "Contactes"
#: web/template/contacts/new.gohtml:2 web/template/contacts/new.gohtml:11
#: web/template/contacts/new.gohtml:15
msgctxt "title"
msgid "New Contact"
msgstr "Nou contacte"
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8
#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9
#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9
msgctxt "title"
msgid "Home"
msgstr "Inici"
#: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
msgctxt "title"
msgid "Contacts"
msgstr "Contactes"
#: web/template/contacts/new.gohtml:31 web/template/contacts/index.gohtml:13
msgctxt "action"
msgid "New contact"
msgstr "Nou contacte"
#: web/template/contacts/index.gohtml:21
msgctxt "contact"
msgid "All"
msgstr "Tots"
#: web/template/contacts/index.gohtml:22
msgctxt "title"
msgid "Customer"
msgstr "Client"
#: web/template/contacts/index.gohtml:23
msgctxt "title"
msgid "Email"
msgstr "Correu-e"
#: web/template/contacts/index.gohtml:24
msgctxt "title"
msgid "Phone"
msgstr "Telèfon"
#: web/template/contacts/index.gohtml:39
msgid "No contacts added yet."
msgstr "No hi ha cap contacte."
#: web/template/contacts/edit.gohtml:2 web/template/contacts/edit.gohtml:15
msgctxt "title"
msgid "Edit Contact “%s”"
msgstr "Edició del contacte «%s»"
#: web/template/contacts/edit.gohtml:33
msgctxt "action"
msgid "Update contact"
msgstr "Actualitza contacte"
#: web/template/login.gohtml:2 web/template/login.gohtml:15
msgctxt "title"
msgid "Login"
@ -127,6 +63,13 @@ msgctxt "title"
msgid "User Settings"
msgstr "Configuració usuari"
#: web/template/profile.gohtml:9 web/template/contacts-edit.gohtml:9
#: web/template/contacts-index.gohtml:8 web/template/tax-details.gohtml:8
#: web/template/contacts-new.gohtml:9
msgctxt "title"
msgid "Home"
msgstr "Inici"
#: web/template/profile.gohtml:18
msgctxt "title"
msgid "User Access Data"
@ -147,6 +90,51 @@ msgctxt "action"
msgid "Save changes"
msgstr "Desa canvis"
#: web/template/contacts-edit.gohtml:2 web/template/contacts-edit.gohtml:15
msgctxt "title"
msgid "Edit Contact “%s”"
msgstr "Edició del contacte «%s»"
#: web/template/contacts-edit.gohtml:10 web/template/contacts-index.gohtml:2
#: web/template/contacts-index.gohtml:9 web/template/contacts-new.gohtml:10
msgctxt "title"
msgid "Contacts"
msgstr "Contactes"
#: web/template/contacts-edit.gohtml:33
msgctxt "action"
msgid "Update contact"
msgstr "Actualitza contacte"
#: web/template/contacts-index.gohtml:13 web/template/contacts-new.gohtml:31
msgctxt "action"
msgid "New contact"
msgstr "Nou contacte"
#: web/template/contacts-index.gohtml:20
msgctxt "contact"
msgid "All"
msgstr "Tots"
#: web/template/contacts-index.gohtml:21
msgctxt "title"
msgid "Customer"
msgstr "Client"
#: web/template/contacts-index.gohtml:22
msgctxt "title"
msgid "Email"
msgstr "Correu-e"
#: web/template/contacts-index.gohtml:23
msgctxt "title"
msgid "Phone"
msgstr "Telèfon"
#: web/template/contacts-index.gohtml:38
msgid "No contacts added yet."
msgstr "No hi ha cap contacte."
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:9
#: web/template/tax-details.gohtml:13
msgctxt "title"
@ -161,7 +149,7 @@ msgstr "Moneda"
#: web/template/tax-details.gohtml:47
msgctxt "title"
msgid "Tax Name"
msgstr "Nom impost"
msgstr "Nom import"
#: web/template/tax-details.gohtml:48
msgctxt "title"
@ -182,53 +170,13 @@ msgctxt "action"
msgid "Add new tax"
msgstr "Afegeix nou impost"
#: web/template/products/new.gohtml:2 web/template/products/new.gohtml:11
#: web/template/products/new.gohtml:15
#: web/template/contacts-new.gohtml:2 web/template/contacts-new.gohtml:11
#: web/template/contacts-new.gohtml:15
msgctxt "title"
msgid "New Product"
msgstr "Nou producte"
msgid "New Contact"
msgstr "Nou contacte"
#: web/template/products/new.gohtml:10 web/template/products/index.gohtml:2
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
msgctxt "title"
msgid "Products"
msgstr "Productes"
#: web/template/products/new.gohtml:25 web/template/products/index.gohtml:13
msgctxt "action"
msgid "New product"
msgstr "Nou producte"
#: web/template/products/index.gohtml:21
msgctxt "product"
msgid "All"
msgstr "Tots"
#: web/template/products/index.gohtml:22
msgctxt "title"
msgid "Name"
msgstr "Nom"
#: web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Preu"
#: web/template/products/index.gohtml:37
msgid "No products added yet."
msgstr "No hi ha cap producte."
#: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15
msgctxt "title"
msgid "Edit Product “%s”"
msgstr "Edició del producte «%s»"
#: web/template/products/edit.gohtml:26
msgctxt "action"
msgid "Update product"
msgstr "Actualitza producte"
#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:178
#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:179
msgctxt "input"
msgid "Email"
msgstr "Correu-e"
@ -238,11 +186,11 @@ msgctxt "input"
msgid "Password"
msgstr "Contrasenya"
#: pkg/login.go:69 pkg/profile.go:88 pkg/contacts.go:262
#: pkg/login.go:69 pkg/profile.go:88 pkg/contacts.go:263
msgid "Email can not be empty."
msgstr "No podeu deixar el correu-e en blanc."
#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:263
#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:264
msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat."
@ -254,42 +202,6 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:144
msgctxt "input"
msgid "Name"
msgstr "Nom"
#: pkg/products.go:150
msgctxt "input"
msgid "Description"
msgstr "Descripció"
#: pkg/products.go:155
msgctxt "input"
msgid "Price"
msgstr "Preu"
#: pkg/products.go:164 pkg/contacts.go:225
msgctxt "input"
msgid "Tax"
msgstr "Impost"
#: pkg/products.go:183 pkg/profile.go:91
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/products.go:184
msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc."
#: pkg/products.go:185
msgid "Price must be a number greater than zero."
msgstr "El preu ha de ser un número major a zero."
#: pkg/products.go:187
msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid."
#: pkg/company.go:78
msgctxt "input"
msgid "Currency"
@ -341,6 +253,10 @@ msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:91
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/profile.go:92
msgid "Confirmation does not match password."
msgstr "La confirmació no és igual a la contrasenya."
@ -349,103 +265,104 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/contacts.go:149
#: pkg/contacts.go:150
msgctxt "input"
msgid "Business name"
msgstr "Nom i cognoms"
#: pkg/contacts.go:158
#: pkg/contacts.go:159
msgctxt "input"
msgid "VAT number"
msgstr "DNI / NIF"
#: pkg/contacts.go:164
#: pkg/contacts.go:165
msgctxt "input"
msgid "Trade name"
msgstr "Nom comercial"
#: pkg/contacts.go:169
#: pkg/contacts.go:170
msgctxt "input"
msgid "Phone"
msgstr "Telèfon"
#: pkg/contacts.go:187
#: pkg/contacts.go:188
msgctxt "input"
msgid "Web"
msgstr "Web"
#: pkg/contacts.go:195
#: pkg/contacts.go:196
msgctxt "input"
msgid "Address"
msgstr "Adreça"
#: pkg/contacts.go:204
#: pkg/contacts.go:205
msgctxt "input"
msgid "City"
msgstr "Població"
#: pkg/contacts.go:210
#: pkg/contacts.go:211
msgctxt "input"
msgid "Province"
msgstr "Província"
#: pkg/contacts.go:216
#: pkg/contacts.go:217
msgctxt "input"
msgid "Postal code"
msgstr "Codi postal"
#: pkg/contacts.go:255
#: pkg/contacts.go:226
msgctxt "input"
msgid "Country"
msgstr "País"
#: pkg/contacts.go:256
msgid "Business name can not be empty."
msgstr "No podeu deixar el nom i els cognoms en blanc."
#: pkg/contacts.go:256
#: pkg/contacts.go:257
msgid "VAT number can not be empty."
msgstr "No podeu deixar el DNI o NIF en blanc."
#: pkg/contacts.go:257
#: pkg/contacts.go:258
msgid "This value is not a valid VAT number."
msgstr "Aquest valor no és un DNI o NIF vàlid."
#: pkg/contacts.go:259
#: pkg/contacts.go:260
msgid "Phone can not be empty."
msgstr "No podeu deixar el telèfon en blanc."
#: pkg/contacts.go:260
#: pkg/contacts.go:261
msgid "This value is not a valid phone number."
msgstr "Aquest valor no és un telèfon vàlid."
#: pkg/contacts.go:266
#: pkg/contacts.go:267
msgid "This value is not a valid web address. It should be like https://domain.com/."
msgstr "Aquest valor no és una adreça web vàlida. Hauria de ser similar a https://domini.cat/."
#: pkg/contacts.go:268
#: pkg/contacts.go:269
msgid "Address can not be empty."
msgstr "No podeu deixar ladreça en blanc."
#: pkg/contacts.go:269
#: pkg/contacts.go:270
msgid "City can not be empty."
msgstr "No podeu deixar la població en blanc."
#: pkg/contacts.go:270
#: pkg/contacts.go:271
msgid "Province can not be empty."
msgstr "No podeu deixar la província en blanc."
#: pkg/contacts.go:271
#: pkg/contacts.go:272
msgid "Postal code can not be empty."
msgstr "No podeu deixar el codi postal en blanc."
#: pkg/contacts.go:272
#: pkg/contacts.go:273
msgid "This value is not a valid postal code."
msgstr "Aquest valor no és un codi postal vàlid."
#: pkg/contacts.go:274
#: pkg/contacts.go:275
msgid "Selected country is not valid."
msgstr "Heu seleccionat un país que no és vàlid."
#~ msgctxt "input"
#~ msgid "Country"
#~ msgstr "País"
#~ msgctxt "nav"
#~ msgid "Customers"
#~ msgstr "Clients"

263
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-02-04 11:24+0100\n"
"POT-Creation-Date: 2023-02-03 13:57+0100\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -44,73 +44,9 @@ msgstr "Panel"
#: web/template/app.gohtml:44
msgctxt "nav"
msgid "Products"
msgstr "Productos"
#: web/template/app.gohtml:45
msgctxt "nav"
msgid "Contacts"
msgstr "Contactos"
#: web/template/contacts/new.gohtml:2 web/template/contacts/new.gohtml:11
#: web/template/contacts/new.gohtml:15
msgctxt "title"
msgid "New Contact"
msgstr "Nuevo contacto"
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8
#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9
#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9
msgctxt "title"
msgid "Home"
msgstr "Inicio"
#: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
msgctxt "title"
msgid "Contacts"
msgstr "Contactos"
#: web/template/contacts/new.gohtml:31 web/template/contacts/index.gohtml:13
msgctxt "action"
msgid "New contact"
msgstr "Nuevo contacto"
#: web/template/contacts/index.gohtml:21
msgctxt "contact"
msgid "All"
msgstr "Todos"
#: web/template/contacts/index.gohtml:22
msgctxt "title"
msgid "Customer"
msgstr "Cliente"
#: web/template/contacts/index.gohtml:23
msgctxt "title"
msgid "Email"
msgstr "Correo-e"
#: web/template/contacts/index.gohtml:24
msgctxt "title"
msgid "Phone"
msgstr "Teléfono"
#: web/template/contacts/index.gohtml:39
msgid "No contacts added yet."
msgstr "No hay contactos."
#: web/template/contacts/edit.gohtml:2 web/template/contacts/edit.gohtml:15
msgctxt "title"
msgid "Edit Contact “%s”"
msgstr "Edición del contacto «%s»"
#: web/template/contacts/edit.gohtml:33
msgctxt "action"
msgid "Update contact"
msgstr "Actualizar contacto"
#: web/template/login.gohtml:2 web/template/login.gohtml:15
msgctxt "title"
msgid "Login"
@ -127,6 +63,13 @@ msgctxt "title"
msgid "User Settings"
msgstr "Configuración usuario"
#: web/template/profile.gohtml:9 web/template/contacts-edit.gohtml:9
#: web/template/contacts-index.gohtml:8 web/template/tax-details.gohtml:8
#: web/template/contacts-new.gohtml:9
msgctxt "title"
msgid "Home"
msgstr "Inicio"
#: web/template/profile.gohtml:18
msgctxt "title"
msgid "User Access Data"
@ -147,6 +90,51 @@ msgctxt "action"
msgid "Save changes"
msgstr "Guardar cambios"
#: web/template/contacts-edit.gohtml:2 web/template/contacts-edit.gohtml:15
msgctxt "title"
msgid "Edit Contact “%s”"
msgstr "Edición del contacto «%s»"
#: web/template/contacts-edit.gohtml:10 web/template/contacts-index.gohtml:2
#: web/template/contacts-index.gohtml:9 web/template/contacts-new.gohtml:10
msgctxt "title"
msgid "Contacts"
msgstr "Contactos"
#: web/template/contacts-edit.gohtml:33
msgctxt "action"
msgid "Update contact"
msgstr "Actualizar contacto"
#: web/template/contacts-index.gohtml:13 web/template/contacts-new.gohtml:31
msgctxt "action"
msgid "New contact"
msgstr "Nuevo contacto"
#: web/template/contacts-index.gohtml:20
msgctxt "contact"
msgid "All"
msgstr "Todos"
#: web/template/contacts-index.gohtml:21
msgctxt "title"
msgid "Customer"
msgstr "Cliente"
#: web/template/contacts-index.gohtml:22
msgctxt "title"
msgid "Email"
msgstr "Correo-e"
#: web/template/contacts-index.gohtml:23
msgctxt "title"
msgid "Phone"
msgstr "Teléfono"
#: web/template/contacts-index.gohtml:38
msgid "No contacts added yet."
msgstr "No hay contactos."
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:9
#: web/template/tax-details.gohtml:13
msgctxt "title"
@ -182,53 +170,13 @@ msgctxt "action"
msgid "Add new tax"
msgstr "Añadir nuevo impuesto"
#: web/template/products/new.gohtml:2 web/template/products/new.gohtml:11
#: web/template/products/new.gohtml:15
#: web/template/contacts-new.gohtml:2 web/template/contacts-new.gohtml:11
#: web/template/contacts-new.gohtml:15
msgctxt "title"
msgid "New Product"
msgstr "Nuevo producto"
msgid "New Contact"
msgstr "Nuevo contacto"
#: web/template/products/new.gohtml:10 web/template/products/index.gohtml:2
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
msgctxt "title"
msgid "Products"
msgstr "Productos"
#: web/template/products/new.gohtml:25 web/template/products/index.gohtml:13
msgctxt "action"
msgid "New product"
msgstr "Nuevo producto"
#: web/template/products/index.gohtml:21
msgctxt "product"
msgid "All"
msgstr "Todos"
#: web/template/products/index.gohtml:22
msgctxt "title"
msgid "Name"
msgstr "Nombre"
#: web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Precio"
#: web/template/products/index.gohtml:37
msgid "No products added yet."
msgstr "No hay productos."
#: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15
msgctxt "title"
msgid "Edit Product “%s”"
msgstr "Edición del producto «%s»"
#: web/template/products/edit.gohtml:26
msgctxt "action"
msgid "Update product"
msgstr "Actualizar producto"
#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:178
#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:179
msgctxt "input"
msgid "Email"
msgstr "Correo-e"
@ -238,11 +186,11 @@ msgctxt "input"
msgid "Password"
msgstr "Contraseña"
#: pkg/login.go:69 pkg/profile.go:88 pkg/contacts.go:262
#: pkg/login.go:69 pkg/profile.go:88 pkg/contacts.go:263
msgid "Email can not be empty."
msgstr "No podéis dejar el correo-e en blanco."
#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:263
#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:264
msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es."
@ -254,42 +202,6 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:144
msgctxt "input"
msgid "Name"
msgstr "Nombre"
#: pkg/products.go:150
msgctxt "input"
msgid "Description"
msgstr "Descripción"
#: pkg/products.go:155
msgctxt "input"
msgid "Price"
msgstr "Precio"
#: pkg/products.go:164 pkg/contacts.go:225
msgctxt "input"
msgid "Tax"
msgstr "Impuesto"
#: pkg/products.go:183 pkg/profile.go:91
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/products.go:184
msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco."
#: pkg/products.go:185
msgid "Price must be a number greater than zero."
msgstr "El precio tiene que ser un número mayor a cero."
#: pkg/products.go:187
msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido."
#: pkg/company.go:78
msgctxt "input"
msgid "Currency"
@ -341,6 +253,10 @@ msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:91
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/profile.go:92
msgid "Confirmation does not match password."
msgstr "La confirmación no corresponde con la contraseña."
@ -349,103 +265,104 @@ msgstr "La confirmación no corresponde con la contraseña."
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."
#: pkg/contacts.go:149
#: pkg/contacts.go:150
msgctxt "input"
msgid "Business name"
msgstr "Nombre y apellidos"
#: pkg/contacts.go:158
#: pkg/contacts.go:159
msgctxt "input"
msgid "VAT number"
msgstr "DNI / NIF"
#: pkg/contacts.go:164
#: pkg/contacts.go:165
msgctxt "input"
msgid "Trade name"
msgstr "Nombre comercial"
#: pkg/contacts.go:169
#: pkg/contacts.go:170
msgctxt "input"
msgid "Phone"
msgstr "Teléfono"
#: pkg/contacts.go:187
#: pkg/contacts.go:188
msgctxt "input"
msgid "Web"
msgstr "Web"
#: pkg/contacts.go:195
#: pkg/contacts.go:196
msgctxt "input"
msgid "Address"
msgstr "Dirección"
#: pkg/contacts.go:204
#: pkg/contacts.go:205
msgctxt "input"
msgid "City"
msgstr "Población"
#: pkg/contacts.go:210
#: pkg/contacts.go:211
msgctxt "input"
msgid "Province"
msgstr "Provincia"
#: pkg/contacts.go:216
#: pkg/contacts.go:217
msgctxt "input"
msgid "Postal code"
msgstr "Código postal"
#: pkg/contacts.go:255
#: pkg/contacts.go:226
msgctxt "input"
msgid "Country"
msgstr "País"
#: pkg/contacts.go:256
msgid "Business name can not be empty."
msgstr "No podéis dejar el nombre y los apellidos en blanco."
#: pkg/contacts.go:256
#: pkg/contacts.go:257
msgid "VAT number can not be empty."
msgstr "No podéis dejar el DNI o NIF en blanco."
#: pkg/contacts.go:257
#: pkg/contacts.go:258
msgid "This value is not a valid VAT number."
msgstr "Este valor no es un DNI o NIF válido."
#: pkg/contacts.go:259
#: pkg/contacts.go:260
msgid "Phone can not be empty."
msgstr "No podéis dejar el teléfono en blanco."
#: pkg/contacts.go:260
#: pkg/contacts.go:261
msgid "This value is not a valid phone number."
msgstr "Este valor no es un teléfono válido."
#: pkg/contacts.go:266
#: pkg/contacts.go:267
msgid "This value is not a valid web address. It should be like https://domain.com/."
msgstr "Este valor no es una dirección web válida. Tiene que ser parecida a https://dominio.es/."
#: pkg/contacts.go:268
#: pkg/contacts.go:269
msgid "Address can not be empty."
msgstr "No podéis dejar la dirección en blanco."
#: pkg/contacts.go:269
#: pkg/contacts.go:270
msgid "City can not be empty."
msgstr "No podéis dejar la población en blanco."
#: pkg/contacts.go:270
#: pkg/contacts.go:271
msgid "Province can not be empty."
msgstr "No podéis dejar la provincia en blanco."
#: pkg/contacts.go:271
#: pkg/contacts.go:272
msgid "Postal code can not be empty."
msgstr "No podéis dejar el código postal en blanco."
#: pkg/contacts.go:272
#: pkg/contacts.go:273
msgid "This value is not a valid postal code."
msgstr "Este valor no es un código postal válido válido."
#: pkg/contacts.go:274
#: pkg/contacts.go:275
msgid "Selected country is not valid."
msgstr "Habéis escogido un país que no es válido."
#~ msgctxt "input"
#~ msgid "Country"
#~ msgstr "País"
#~ msgctxt "nav"
#~ msgid "Customers"
#~ msgstr "Clientes"

View File

@ -1,7 +0,0 @@
-- Revert numerus:product from pg
begin;
drop table if exists numerus.product;
commit;

View File

@ -39,4 +39,3 @@ company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <
tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates
tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
product [schema_numerus company] 2023-02-04T09:17:24Z jordi fita mas <jordi@tandem.blog> # Add relation for products

View File

@ -1,147 +0,0 @@
-- Test product
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(55);
set search_path to numerus, auth, public;
select has_table('product');
select has_pk('product' );
select table_privs_are('product', 'guest', array []::text[]);
select table_privs_are('product', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('product', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('product', 'authenticator', array []::text[]);
select has_sequence('product_product_id_seq');
select sequence_privs_are('product_product_id_seq', 'guest', array[]::text[]);
select sequence_privs_are('product_product_id_seq', 'invoicer', array['USAGE']);
select sequence_privs_are('product_product_id_seq', 'admin', array['USAGE']);
select sequence_privs_are('product_product_id_seq', 'authenticator', array[]::text[]);
select has_column('product', 'product_id');
select col_is_pk('product', 'product_id');
select col_type_is('product', 'product_id', 'integer');
select col_not_null('product', 'product_id');
select col_has_default('product', 'product_id');
select col_default_is('product', 'product_id', 'nextval(''product_product_id_seq''::regclass)');
select has_column('product', 'company_id');
select col_is_fk('product', 'company_id');
select fk_ok('product', 'company_id', 'company', 'company_id');
select col_type_is('product', 'company_id', 'integer');
select col_not_null('product', 'company_id');
select col_hasnt_default('product', 'company_id');
select has_column('product', 'slug');
select col_type_is('product', 'slug', 'uuid');
select col_not_null('product', 'slug');
select col_has_default('product', 'slug');
select col_default_is('product', 'slug', 'gen_random_uuid()');
select has_column('product', 'name');
select col_type_is('product', 'name', 'text');
select col_not_null('product', 'name');
select col_hasnt_default('product', 'name');
select has_column('product', 'description');
select col_type_is('product', 'description', 'text');
select col_not_null('product', 'description');
select col_hasnt_default('product', 'description');
select has_column('product', 'price');
select col_type_is('product', 'price', 'integer');
select col_not_null('product', 'price');
select col_hasnt_default('product', 'price');
select has_column('product', 'tax_id');
select col_is_fk('product', 'tax_id');
select fk_ok('product', 'tax_id', 'tax', 'tax_id');
select col_type_is('product', 'tax_id', 'integer');
select col_not_null('product', 'tax_id');
select col_hasnt_default('product', 'tax_id');
select has_column('product', 'created_at');
select col_type_is('product', 'created_at', 'timestamp with time zone');
select col_not_null('product', 'created_at');
select col_has_default('product', 'created_at');
select col_default_is('product', 'created_at', current_timestamp);
set client_min_messages to warning;
truncate product cascade;
truncate tax cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD')
;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into tax (tax_id, company_id, name, rate)
values (3, 2, 'IVA 21 %', 0.21)
, (6, 4, 'IVA 10 %', 0.10)
;
insert into product (company_id, name, description, price, tax_id)
values (2, 'Product 1', 'Description 1', 1200, 3)
, (4, 'Product 2', 'Description 2', 2400, 6)
;
prepare product_data as
select company_id, name
from product
order by company_id, name;
set role invoicer;
select is_empty('product_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'product_data',
$$ values (2, 'Product 1')
$$,
'Should only list products of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'product_data',
$$ values (4, 'Product 2')
$$,
'Should only list products of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'product_data',
'42501', 'permission denied for table product',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

View File

@ -1,19 +0,0 @@
-- Verify numerus:product on pg
begin;
select product_id
, company_id
, slug
, name
, description
, price
, tax_id
, created_at
from numerus.product
where false;
select 1 / count(*) from pg_class where oid = 'numerus.product'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.product'::regclass;
rollback;

View File

@ -41,7 +41,6 @@
<nav aria-label="{{( pgettext "Main" "title" )}}">
<ul>
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
<li><a href="{{ companyURI "/products" }}">{{( pgettext "Products" "nav" )}}</a></li>
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
</ul>
</nav>

View File

@ -17,7 +17,7 @@
{{ csrfToken }}
{{ putMethod }}
{{ template "input-field" .BusinessName }}
{{ template "input-field" .BusinessName | addInputAttr "autofocus" }}
{{ template "input-field" .VATIN }}
{{ template "input-field" .TradeName }}
{{ template "input-field" .Phone }}

View File

@ -14,7 +14,6 @@
</p>
</nav>
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.ContactsIndexPage*/ -}}
<table>
<thead>
<tr>
@ -26,7 +25,7 @@
</thead>
<tbody>
{{ with .Contacts }}
{{- range $contact := . }}
{{- range $tax := . }}
<tr>
<td></td>
<td><a href="{{ companyURI "/contacts/"}}{{ .Slug }}">{{ .Name }}</a></td>

View File

@ -1,30 +0,0 @@
{{ define "title" -}}
{{printf (pgettext "Edit Product “%s”" "title") .Name.Val }}
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/products"}}">{{( pgettext "Products" "title" )}}</a> /
<a>{{ .Name.Val }}</a>
</p>
</nav>
<section class="dialog-content">
<h2>{{printf (pgettext "Edit Product “%s”" "title") .Name.Val }}</h2>
<form method="POST">
{{ csrfToken }}
{{ putMethod }}
{{ template "input-field" .Name | addInputAttr "autofocus" }}
{{ template "input-field" .Description }}
{{ template "input-field" .Price }}
{{ template "select-field" .Tax }}
<fieldset>
<button class="primary" type="submit">{{( pgettext "Update product" "action" )}}</button>
</fieldset>
</form>
</section>
{{- end }}

View File

@ -1,42 +0,0 @@
{{ define "title" -}}
{{( pgettext "Products" "title" )}}
{{- end }}
{{ define "content" }}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a>{{( pgettext "Products" "title" )}}</a>
</p>
<p>
<a class="primary button"
href="{{ companyURI "/products/new" }}">{{( pgettext "New product" "action" )}}</a>
</p>
</nav>
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productsIndexPage*/ -}}
<table>
<thead>
<tr>
<th>{{( pgettext "All" "product" )}}</th>
<th>{{( pgettext "Name" "title" )}}</th>
<th>{{( pgettext "Price" "title" )}}</th>
</tr>
</thead>
<tbody>
{{ with .Products }}
{{- range $product := . }}
<tr>
<td></td>
<td><a href="{{ companyURI "/products/"}}{{ .Slug }}">{{ .Name }}</a></td>
<td>{{ .Price }}</td>
</tr>
{{- end }}
{{ else }}
<tr>
<td colspan="4">{{( gettext "No products added yet." )}}</td>
</tr>
{{ end }}
</tbody>
</table>
{{- end }}

View File

@ -1,30 +0,0 @@
{{ define "title" -}}
{{( pgettext "New Product" "title" )}}
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/products"}}">{{( pgettext "Products" "title" )}}</a> /
<a>{{( pgettext "New Product" "title" )}}</a>
</p>
</nav>
<section class="dialog-content">
<h2>{{(pgettext "New Product" "title")}}</h2>
<form method="POST" action="{{ companyURI "/products" }}">
{{ csrfToken }}
{{ template "input-field" .Name | addInputAttr "autofocus" }}
{{ template "input-field" .Description }}
{{ template "input-field" .Price }}
{{ template "select-field" .Tax }}
<fieldset>
<button class="primary" type="submit">{{( pgettext "New product" "action" )}}</button>
</fieldset>
</form>
</section>
{{- end }}