Compare commits

..

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

19 changed files with 3071 additions and 10321 deletions

1
debian/control vendored
View File

@ -8,7 +8,6 @@ Build-Depends:
gettext, gettext,
golang-any, golang-any,
golang-github-jackc-pgx-v4-dev, golang-github-jackc-pgx-v4-dev,
golang-github-julienschmidt-httprouter-dev,
golang-github-leonelquinteros-gotext-dev, golang-github-leonelquinteros-gotext-dev,
golang-golang-x-text-dev, golang-golang-x-text-dev,
postgresql-all (>= 217~), postgresql-all (>= 217~),

View File

@ -20,13 +20,4 @@ values (1, 'Retenció 15 %', -0.15)
, (1, 'IVA 21 %', 0.21) , (1, 'IVA 21 %', 0.21)
; ;
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
values (1, 1, 'Melcior', 'IR1', 'Rei Blanc', parse_packed_phone_number('0732621', 'IR'), 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR')
, (2, 1, 'Gaspar', 'IN2', 'Rei Ros', parse_packed_phone_number('111', 'IN'), 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN')
, (3, 1, 'Baltasar', 'YE3', 'Rei Negre', parse_packed_phone_number('1-111-111', 'YE'), 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE')
, (4, 1, 'Caganera', 'ES41414141L', '', parse_packed_phone_number('222 222 222', 'ES'), 'caganera@pesebre.cat', '', 'C/ De lHort, 4', 'Olot', 'Girona', '17800', 'ES')
, (5, 1, 'Bou', 'ES41414142C', '', parse_packed_phone_number('333 333 333', 'ES'), 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES')
, (6, 1, 'Rabadà', 'ES41414143K', '', parse_packed_phone_number('444 444 444', 'ES'), 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES')
;
commit; commit;

1
go.mod
View File

@ -4,7 +4,6 @@ go 1.18
require ( require (
github.com/jackc/pgx/v4 v4.17.2 github.com/jackc/pgx/v4 v4.17.2
github.com/julienschmidt/httprouter v1.3.0
github.com/leonelquinteros/gotext v1.5.1 github.com/leonelquinteros/gotext v1.5.1
golang.org/x/text v0.3.8 golang.org/x/text v0.3.8
) )

2
go.sum
View File

@ -62,8 +62,6 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=

View File

@ -3,11 +3,11 @@ package pkg
import ( import (
"context" "context"
"errors" "errors"
"github.com/julienschmidt/httprouter"
"html/template" "html/template"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
) )
const ( const (
@ -19,26 +19,44 @@ type Company struct {
Slug string Slug string
} }
func CompanyHandler(next http.Handler) httprouter.Handle { func CompanyHandler(next http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
company := &Company{ slug := r.URL.Path
Slug: params[0].Value, if idx := strings.IndexByte(slug, '/'); idx >= 0 {
slug = slug[:idx]
} }
conn := getConn(r) conn := getConn(r)
err := conn.QueryRow(r.Context(), "select company_id from company where slug = $1", company.Slug).Scan(&company.Id) company := &Company{
Slug: slug,
}
err := conn.QueryRow(r.Context(), "select company_id from company where slug = $1", slug).Scan(&company.Id)
if err != nil { if err != nil {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
ctx := context.WithValue(r.Context(), ContextCompanyKey, company) ctx := context.WithValue(r.Context(), ContextCompanyKey, company)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r2 := new(http.Request)
*r2 = *r // Same as StripPrefix
r2.URL = new(url.URL) p := strings.TrimPrefix(r.URL.Path, slug)
*r2.URL = *r.URL rp := strings.TrimPrefix(r.URL.RawPath, slug)
r2.URL.Path = params[1].Value if len(p) < len(r.URL.Path) && (r.URL.RawPath == "" || len(rp) < len(r.URL.RawPath)) {
next.ServeHTTP(w, r2) r2 := new(http.Request)
} *r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
if p == "" {
r2.URL.Path = "/"
} else {
r2.URL.Path = p
}
r2.URL.RawPath = rp
next.ServeHTTP(w, r2)
} else {
http.NotFound(w, r)
}
})
} }
func getCompany(r *http.Request) *Company { func getCompany(r *http.Request) *Company {
@ -106,70 +124,41 @@ func (form *taxDetailsForm) mustFillFromDatabase(ctx context.Context, conn *Conn
type TaxDetailsPage struct { type TaxDetailsPage struct {
DetailsForm *taxDetailsForm DetailsForm *taxDetailsForm
NewTaxForm *taxForm NewTaxForm *newTaxForm
Taxes []*Tax Taxes []*Tax
} }
func GetCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func CompanyTaxDetailsHandler() http.Handler {
mustRenderTaxDetailsForm(w, r, newTaxDetailsFormFromDatabase(r)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
} locale := getLocale(r)
conn := getConn(r)
func newTaxDetailsFormFromDatabase(r *http.Request) *taxDetailsForm { page := &TaxDetailsPage{
locale := getLocale(r) DetailsForm: newTaxDetailsForm(r.Context(), conn, locale),
conn := getConn(r) NewTaxForm: newNewTaxForm(locale),
form := newTaxDetailsForm(r.Context(), conn, locale) }
company := mustGetCompany(r)
company := mustGetCompany(r) if r.Method == "POST" {
form.mustFillFromDatabase(r.Context(), conn, company) if err := page.DetailsForm.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return form return
} }
if err := verifyCsrfTokenValid(r); err != nil {
func HandleCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { http.Error(w, err.Error(), http.StatusForbidden)
locale := getLocale(r) return
conn := getConn(r) }
form := newTaxDetailsForm(r.Context(), conn, locale) if ok := page.DetailsForm.Validate(r.Context(), conn); ok {
if err := form.Parse(r); err != nil { form := page.DetailsForm
http.Error(w, err.Error(), http.StatusBadRequest) 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)
return http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
} return
if err := verifyCsrfTokenValid(r); err != nil { }
http.Error(w, err.Error(), http.StatusForbidden) w.WriteHeader(http.StatusUnprocessableEntity)
return } else {
} page.DetailsForm.mustFillFromDatabase(r.Context(), conn, company)
if ok := form.Validate(r.Context(), conn); !ok { }
w.WriteHeader(http.StatusUnprocessableEntity) page.Taxes = mustGetTaxes(r.Context(), conn, company)
mustRenderTaxDetailsForm(w, r, form) mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
return })
}
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, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
return
}
func mustRenderTaxDetailsForm(w http.ResponseWriter, r *http.Request, form *taxDetailsForm) {
locale := getLocale(r)
page := &TaxDetailsPage{
DetailsForm: form,
NewTaxForm: newTaxForm(locale),
}
mustRenderTexDetailsPage(w, r, page)
}
func mustRenderTaxForm(w http.ResponseWriter, r *http.Request, form *taxForm) {
page := &TaxDetailsPage{
DetailsForm: newTaxDetailsFormFromDatabase(r),
NewTaxForm: form,
}
mustRenderTexDetailsPage(w, r, page)
}
func mustRenderTexDetailsPage(w http.ResponseWriter, r *http.Request, page *TaxDetailsPage) {
conn := getConn(r)
company := mustGetCompany(r)
page.Taxes = mustGetTaxes(r.Context(), conn, company)
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
} }
func mustGetCompany(r *http.Request) *Company { func mustGetCompany(r *http.Request) *Company {
@ -203,14 +192,14 @@ func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax {
return taxes return taxes
} }
type taxForm struct { type newTaxForm struct {
locale *Locale locale *Locale
Name *InputField Name *InputField
Rate *InputField Rate *InputField
} }
func newTaxForm(locale *Locale) *taxForm { func newNewTaxForm(locale *Locale) *newTaxForm {
return &taxForm{ return &newTaxForm{
locale: locale, locale: locale,
Name: &InputField{ Name: &InputField{
Name: "tax_name", Name: "tax_name",
@ -231,7 +220,7 @@ func newTaxForm(locale *Locale) *taxForm {
} }
} }
func (form *taxForm) Parse(r *http.Request) error { func (form *newTaxForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return err return err
} }
@ -240,7 +229,7 @@ func (form *taxForm) Parse(r *http.Request) error {
return nil return nil
} }
func (form *taxForm) Validate() bool { func (form *newTaxForm) Validate() bool {
validator := newFormValidator() validator := newFormValidator()
validator.CheckRequiredInput(form.Name, gettext("Tax name can not be empty.", form.locale)) validator.CheckRequiredInput(form.Name, gettext("Tax name can not be empty.", form.locale))
if validator.CheckRequiredInput(form.Rate, gettext("Tax rate can not be empty.", form.locale)) { if validator.CheckRequiredInput(form.Rate, gettext("Tax rate can not be empty.", form.locale)) {
@ -249,40 +238,44 @@ func (form *taxForm) Validate() bool {
return validator.AllOK() return validator.AllOK()
} }
func HandleDeleteCompanyTax(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func CompanyTaxHandler() http.Handler {
taxId, err := strconv.Atoi(params[0].Value) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err != nil { param := r.URL.Path
http.NotFound(w, r) if idx := strings.LastIndexByte(param, '/'); idx >= 0 {
return param = param[idx+1:]
} }
if err := verifyCsrfTokenValid(r); err != nil { conn := getConn(r)
http.Error(w, err.Error(), http.StatusForbidden) company := mustGetCompany(r)
return if taxId, err := strconv.Atoi(param); err == nil {
} if err := verifyCsrfTokenValid(r); err != nil {
conn := getConn(r) http.Error(w, err.Error(), http.StatusForbidden)
company := mustGetCompany(r) return
conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId) }
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther) conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId)
} http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
} else {
func HandleAddCompanyTax(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { locale := getLocale(r)
locale := getLocale(r) form := newNewTaxForm(locale)
form := newTaxForm(locale) if err := form.Parse(r); err != nil {
if err := form.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusBadRequest) return
return }
} if err := verifyCsrfTokenValid(r); err != nil {
if err := verifyCsrfTokenValid(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden)
http.Error(w, err.Error(), http.StatusForbidden) return
return }
} if form.Validate() {
if !form.Validate() { conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, form.Name, form.Rate.Integer())
w.WriteHeader(http.StatusUnprocessableEntity) http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
mustRenderTaxForm(w, r, form) } else {
return w.WriteHeader(http.StatusUnprocessableEntity)
} }
conn := getConn(r) page := &TaxDetailsPage{
company := mustGetCompany(r) DetailsForm: newTaxDetailsForm(r.Context(), conn, locale).mustFillFromDatabase(r.Context(), conn, company),
conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, form.Name, form.Rate.Integer()) NewTaxForm: form,
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther) Taxes: mustGetTaxes(r.Context(), conn, company),
}
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
}
})
} }

View File

@ -2,14 +2,11 @@ package pkg
import ( import (
"context" "context"
"github.com/jackc/pgx/v4"
"github.com/julienschmidt/httprouter"
"html/template" "html/template"
"net/http" "net/http"
) )
type ContactEntry struct { type ContactEntry struct {
Slug string
Name string Name string
Email string Email string
Phone string Phone string
@ -19,93 +16,38 @@ type ContactsIndexPage struct {
Contacts []*ContactEntry Contacts []*ContactEntry
} }
func IndexContacts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func ContactsHandler() http.Handler {
conn := getConn(r) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
company := getCompany(r) conn := getConn(r)
page := &ContactsIndexPage{ company := getCompany(r)
Contacts: mustGetContactEntries(r.Context(), conn, company), if r.Method == "POST" {
} locale := getLocale(r)
mustRenderAppTemplate(w, r, "contacts-index.gohtml", page) form := newContactForm(r.Context(), conn, locale)
} if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) { return
locale := getLocale(r) }
conn := getConn(r) if err := verifyCsrfTokenValid(r); err != nil {
form := newContactForm(r.Context(), conn, locale) http.Error(w, err.Error(), http.StatusForbidden)
slug := params[0].Value return
if slug == "new" { }
w.WriteHeader(http.StatusOK) if form.Validate(r.Context(), conn) {
mustRenderNewContactForm(w, r, form) conn.MustExec(r.Context(), "insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, province, city, 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)
return http.Redirect(w, r, "/company/"+company.Slug+"/contacts", http.StatusSeeOther)
} } else {
err := conn.QueryRow(r.Context(), "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code from contact where slug = $1", slug).Scan(form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country) mustRenderAppTemplate(w, r, "contacts-new.gohtml", form)
if err != nil { }
if err == pgx.ErrNoRows {
http.NotFound(w, r)
return
} else { } else {
panic(err) page := &ContactsIndexPage{
Contacts: mustGetContactEntries(r.Context(), conn, company),
}
mustRenderAppTemplate(w, r, "contacts-index.gohtml", page)
} }
} })
w.WriteHeader(http.StatusOK)
mustRenderEditContactForm(w, r, form)
}
func mustRenderNewContactForm(w http.ResponseWriter, r *http.Request, form *contactForm) {
mustRenderAppTemplate(w, r, "contacts-new.gohtml", form)
}
func mustRenderEditContactForm(w http.ResponseWriter, r *http.Request, form *contactForm) {
mustRenderAppTemplate(w, r, "contacts-edit.gohtml", form)
}
func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
form := newContactForm(r.Context(), conn, locale)
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(r.Context(), conn) {
mustRenderNewContactForm(w, r, form)
return
}
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, "/company/"+company.Slug+"/contacts", http.StatusSeeOther)
}
func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
form := newContactForm(r.Context(), conn, locale)
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(r.Context(), conn) {
mustRenderEditContactForm(w, r, form)
return
}
slug := conn.MustGetText(r.Context(), "", "update contact 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 where slug = $12 returning slug", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, params[0].Value)
if slug == "" {
http.NotFound(w, r)
}
company := getCompany(r)
http.Redirect(w, r, "/company/"+company.Slug+"/contacts/"+slug, http.StatusSeeOther)
} }
func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []*ContactEntry { func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []*ContactEntry {
rows, err := conn.Query(ctx, "select slug, business_name, email, phone from contact where company_id = $1 order by business_name", company.Id) rows, err := conn.Query(ctx, "select business_name, email, phone from contact where company_id = $1 order by business_name", company.Id)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -114,7 +56,7 @@ func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []
var entries []*ContactEntry var entries []*ContactEntry
for rows.Next() { for rows.Next() {
entry := &ContactEntry{} entry := &ContactEntry{}
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Email, &entry.Phone) err = rows.Scan(&entry.Name, &entry.Email, &entry.Phone)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -275,3 +217,12 @@ func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool {
validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale))
return validator.AllOK() return validator.AllOK()
} }
func NewContactHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
locale := getLocale(r)
conn := getConn(r)
form := newContactForm(r.Context(), conn, locale)
mustRenderAppTemplate(w, r, "contacts-new.gohtml", form)
})
}

View File

@ -22,7 +22,7 @@ func NewLocale(lang language.Tag) *Locale {
} }
} }
func LocaleSetter(db *Db, next http.Handler) http.Handler { func SetLocale(db *Db, next http.Handler) http.Handler {
availableLanguages := mustGetAvailableLanguages(db) availableLanguages := mustGetAvailableLanguages(db)
var matcher = language.NewMatcher(availableLanguages) var matcher = language.NewMatcher(availableLanguages)

View File

@ -3,7 +3,6 @@ package pkg
import ( import (
"context" "context"
"errors" "errors"
"github.com/julienschmidt/httprouter"
"html/template" "html/template"
"net" "net"
"net/http" "net/http"
@ -73,59 +72,49 @@ func (form *loginForm) Validate() bool {
return validator.AllOK() return validator.AllOK()
} }
func GetLoginForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func LoginHandler() http.Handler {
user := getUser(r) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if user.LoggedIn { user := getUser(r)
http.Redirect(w, r, "/", http.StatusSeeOther) if user.LoggedIn {
return
}
locale := getLocale(r)
form := newLoginForm(locale)
w.WriteHeader(http.StatusOK)
mustRenderLoginForm(w, r, form)
}
func HandleLoginForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
user := getUser(r)
if user.LoggedIn {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
locale := getLocale(r)
form := newLoginForm(locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if form.Validate() {
conn := getConn(r)
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", form.Email, form.Password, remoteAddr(r))
if cookie != "" {
setSessionCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
form.Errors = append(form.Errors, errors.New(gettext("Invalid user or password.", locale))) locale := getLocale(r)
w.WriteHeader(http.StatusUnauthorized) form := newLoginForm(locale)
} else { if r.Method == "POST" {
w.WriteHeader(http.StatusUnprocessableEntity) if err := form.Parse(r); err != nil {
} http.Error(w, err.Error(), http.StatusBadRequest)
mustRenderLoginForm(w, r, form) return
}
if form.Validate() {
conn := getConn(r)
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", form.Email, form.Password, remoteAddr(r))
if cookie != "" {
setSessionCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
form.Errors = append(form.Errors, errors.New(gettext("Invalid user or password.", locale)))
w.WriteHeader(http.StatusUnauthorized)
} else {
w.WriteHeader(http.StatusUnprocessableEntity)
}
}
mustRenderWebTemplate(w, r, "login.gohtml", form)
})
} }
func mustRenderLoginForm(w http.ResponseWriter, r *http.Request, form *loginForm) { func LogoutHandler() http.Handler {
mustRenderWebTemplate(w, r, "login.gohtml", form) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
} if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
func HandleLogout(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { return
if err := verifyCsrfTokenValid(r); err != nil { }
http.Error(w, err.Error(), http.StatusForbidden) conn := getConn(r)
return conn.MustExec(r.Context(), "select logout()")
} http.SetCookie(w, createSessionCookie("", -24*time.Hour))
conn := getConn(r) http.Redirect(w, r, "/login", http.StatusSeeOther)
conn.MustExec(r.Context(), "select logout()") })
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
http.Redirect(w, r, "/login", http.StatusSeeOther)
} }
func remoteAddr(r *http.Request) string { func remoteAddr(r *http.Request) string {
@ -159,7 +148,7 @@ type AppUser struct {
CsrfToken string CsrfToken string
} }
func LoginChecker(db *Db, next http.Handler) http.Handler { func CheckLogin(db *Db, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx = r.Context() var ctx = r.Context()
if cookie, err := r.Cookie(sessionCookie); err == nil { if cookie, err := r.Cookie(sessionCookie); err == nil {
@ -206,13 +195,13 @@ func getConn(r *http.Request) *Conn {
return r.Context().Value(ContextConnKey).(*Conn) return r.Context().Value(ContextConnKey).(*Conn)
} }
func Authenticated(next httprouter.Handle) httprouter.Handle { func Authenticated(next http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := getUser(r) user := getUser(r)
if user.LoggedIn { if user.LoggedIn {
next(w, r, params) next.ServeHTTP(w, r)
} else { } else {
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)
} }
} })
} }

View File

@ -2,7 +2,6 @@ package pkg
import ( import (
"context" "context"
"github.com/julienschmidt/httprouter"
"html/template" "html/template"
"net/http" "net/http"
) )
@ -94,46 +93,38 @@ func (form *profileForm) Validate() bool {
return validator.AllOK() return validator.AllOK()
} }
func GetProfileForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func ProfileHandler() http.Handler {
user := getUser(r) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn := getConn(r) user := getUser(r)
locale := getLocale(r) conn := getConn(r)
form := newProfileForm(r.Context(), conn, locale) locale := getLocale(r)
form.Name.Val = conn.MustGetText(r.Context(), "", "select name from user_profile") form := newProfileForm(r.Context(), conn, locale)
form.Email.Val = user.Email if r.Method == "POST" {
form.Language.Selected = user.Language.String() if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
w.WriteHeader(http.StatusOK) return
mustRenderProfileForm(w, r, form) }
} if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
func HandleProfileForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { return
conn := getConn(r) }
locale := getLocale(r) if ok := form.Validate(); ok {
form := newProfileForm(r.Context(), conn, locale) //goland:noinspection SqlWithoutWhere
if err := form.Parse(r); err != nil { cookie := conn.MustGetText(r.Context(), "", "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", form.Name, form.Email, form.Language)
http.Error(w, err.Error(), http.StatusBadRequest) setSessionCookie(w, cookie)
return if form.Password.Val != "" {
} conn.MustExec(r.Context(), "select change_password($1)", form.Password)
if err := verifyCsrfTokenValid(r); err != nil { }
http.Error(w, err.Error(), http.StatusForbidden) company := getCompany(r)
return http.Redirect(w, r, "/company/"+company.Slug+"/profile", http.StatusSeeOther)
} return
if ok := form.Validate(); !ok { }
w.WriteHeader(http.StatusUnprocessableEntity) w.WriteHeader(http.StatusUnprocessableEntity)
mustRenderProfileForm(w, r, form) } else {
return form.Name.Val = conn.MustGetText(r.Context(), "", "select name from user_profile")
} form.Email.Val = user.Email
//goland:noinspection SqlWithoutWhere form.Language.Selected = user.Language.String()
cookie := conn.MustGetText(r.Context(), "", "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", form.Name, form.Email, form.Language) }
setSessionCookie(w, cookie) mustRenderAppTemplate(w, r, "profile.gohtml", form)
if form.Password.Val != "" { })
conn.MustExec(r.Context(), "select change_password($1)", form.Password)
}
company := getCompany(r)
http.Redirect(w, r, "/company/"+company.Slug+"/profile", http.StatusSeeOther)
}
func mustRenderProfileForm(w http.ResponseWriter, r *http.Request, form *profileForm) {
mustRenderAppTemplate(w, r, "profile.gohtml", form)
} }

View File

@ -2,73 +2,43 @@ package pkg
import ( import (
"net/http" "net/http"
"github.com/julienschmidt/httprouter"
) )
func NewRouter(db *Db) http.Handler { func NewRouter(db *Db) http.Handler {
companyRouter := httprouter.New() companyRouter := http.NewServeMux()
companyRouter.GET("/profile", GetProfileForm) companyRouter.Handle("/tax-details", CompanyTaxDetailsHandler())
companyRouter.POST("/profile", HandleProfileForm) companyRouter.Handle("/tax/", CompanyTaxHandler())
companyRouter.GET("/tax-details", GetCompanyTaxDetailsForm) companyRouter.Handle("/tax", CompanyTaxHandler())
companyRouter.POST("/tax-details", HandleCompanyTaxDetailsForm) companyRouter.Handle("/profile", ProfileHandler())
companyRouter.POST("/tax", HandleAddCompanyTax) companyRouter.Handle("/contacts/new", NewContactHandler())
companyRouter.DELETE("/tax/:taxId", HandleDeleteCompanyTax) companyRouter.Handle("/contacts", ContactsHandler())
companyRouter.GET("/contacts", IndexContacts) companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
companyRouter.POST("/contacts", HandleAddContact)
companyRouter.GET("/contacts/:slug", GetContactForm)
companyRouter.PUT("/contacts/:slug", HandleUpdateContact)
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil) mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
}) })
router := httprouter.New() router := http.NewServeMux()
router.ServeFiles("/static/*filepath", http.Dir("web/static")) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
router.GET("/login", GetLoginForm) router.Handle("/login", LoginHandler())
router.POST("/login", HandleLoginForm) router.Handle("/logout", Authenticated(LogoutHandler()))
router.POST("/logout", Authenticated(HandleLogout)) router.Handle("/company/", Authenticated(http.StripPrefix("/company/", CompanyHandler(companyRouter))))
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
companyHandler := Authenticated(CompanyHandler(companyRouter))
router.GET("/company/:slug/*rest", companyHandler)
router.POST("/company/:slug/*rest", companyHandler)
router.PUT("/company/:slug/*rest", companyHandler)
router.DELETE("/company/:slug/*rest", companyHandler)
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
user := getUser(r) user := getUser(r)
if user.LoggedIn { if user.LoggedIn {
conn := getConn(r) conn := getConn(r)
slug := conn.MustGetText(r.Context(), "", "select slug::text from company order by company_id limit 1") var slug string
http.Redirect(w, r, "/company/"+slug+"/", http.StatusFound) err := conn.QueryRow(r.Context(), "select slug::text from company order by company_id limit 1").Scan(&slug)
if err != nil {
panic(err)
}
http.Redirect(w, r, "/company/"+slug, http.StatusFound)
} else { } else {
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)
} }
}) })
var handler http.Handler = router var handler http.Handler = router
handler = MethodOverrider(handler) handler = SetLocale(db, handler)
handler = LocaleSetter(db, handler) handler = CheckLogin(db, handler)
handler = LoginChecker(db, handler)
handler = Recoverer(handler) handler = Recoverer(handler)
handler = Logger(handler) handler = Logger(handler)
return handler return handler
} }
func MethodOverrider(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
override := r.FormValue(overrideMethodName)
if override == http.MethodDelete || override == http.MethodPut {
r2 := new(http.Request)
*r2 = *r
r2.Method = override
r = r2
}
}
next.ServeHTTP(w, r)
})
}

View File

@ -7,8 +7,6 @@ import (
"net/http" "net/http"
) )
const overrideMethodName = "_method"
func templateFile(name string) string { func templateFile(name string) string {
return "web/template/" + name return "web/template/" + name
} }
@ -41,12 +39,6 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
field.Attributes = append(field.Attributes, template.HTMLAttr(attr)) field.Attributes = append(field.Attributes, template.HTMLAttr(attr))
return field return field
}, },
"deleteMethod": func() template.HTML {
return overrideMethodField(http.MethodDelete)
},
"putMethod": func() template.HTML {
return overrideMethodField(http.MethodPut)
},
}) })
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout), templateFile("form.gohtml")); err != nil { if _, err := t.ParseFiles(templateFile(filename), templateFile(layout), templateFile("form.gohtml")); err != nil {
panic(err) panic(err)
@ -56,9 +48,6 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
} }
} }
func overrideMethodField(method string) template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, overrideMethodName, method))
}
func mustRenderAppTemplate(w io.Writer, r *http.Request, filename string, data interface{}) { func mustRenderAppTemplate(w io.Writer, r *http.Request, filename string, data interface{}) {
mustRenderTemplate(w, r, "app.gohtml", filename, data) mustRenderTemplate(w, r, "app.gohtml", filename, data)
} }

327
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-02-03 13:57+0100\n" "POT-Creation-Date: 2023-02-01 11:26+0100\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -32,17 +32,17 @@ msgctxt "menu"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuració fiscal" msgstr "Configuració fiscal"
#: web/template/app.gohtml:34 #: web/template/app.gohtml:33
msgctxt "action" msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Surt" msgstr "Surt"
#: web/template/app.gohtml:43 #: web/template/app.gohtml:42
msgctxt "nav" msgctxt "nav"
msgid "Dashboard" msgid "Dashboard"
msgstr "Tauler" msgstr "Tauler"
#: web/template/app.gohtml:44 #: web/template/app.gohtml:43
msgctxt "nav" msgctxt "nav"
msgid "Contacts" msgid "Contacts"
msgstr "Contactes" msgstr "Contactes"
@ -57,311 +57,222 @@ msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entra" msgstr "Entra"
#: web/template/profile.gohtml:2 web/template/profile.gohtml:10 #: web/template/profile.gohtml:2 web/template/profile.gohtml:7
#: web/template/profile.gohtml:14
msgctxt "title" msgctxt "title"
msgid "User Settings" msgid "User Settings"
msgstr "Configuració usuari" msgstr "Configuració usuari"
#: web/template/profile.gohtml:9 web/template/contacts-edit.gohtml:9 #: web/template/profile.gohtml:10
#: 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" msgctxt "title"
msgid "User Access Data" msgid "User Access Data"
msgstr "Dades accés usuari" msgstr "Dades accés usuari"
#: web/template/profile.gohtml:24 #: web/template/profile.gohtml:16
msgctxt "title" msgctxt "title"
msgid "Password Change" msgid "Password Change"
msgstr "Canvi contrasenya" msgstr "Canvi contrasenya"
#: web/template/profile.gohtml:31 #: web/template/profile.gohtml:23
msgctxt "title" msgctxt "title"
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:96 #: web/template/profile.gohtml:27 web/template/tax-details.gohtml:133
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Desa canvis" msgstr "Desa canvis"
#: web/template/contacts-edit.gohtml:2 web/template/contacts-edit.gohtml:15 #: web/template/contacts-index.gohtml:2
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" msgctxt "title"
msgid "Contacts" msgid "Contacts"
msgstr "Contactes" msgstr "Contactes"
#: web/template/contacts-edit.gohtml:33 #: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
msgctxt "action"
msgid "Update contact"
msgstr "Actualitza contacte"
#: web/template/contacts-index.gohtml:13 web/template/contacts-new.gohtml:31
msgctxt "action" msgctxt "action"
msgid "New contact" msgid "New contact"
msgstr "Nou contacte" msgstr "Nou contacte"
#: web/template/contacts-index.gohtml:20 #: web/template/contacts-index.gohtml:11
msgctxt "contact" msgctxt "contact"
msgid "All" msgid "All"
msgstr "Tots" msgstr "Tots"
#: web/template/contacts-index.gohtml:21 #: web/template/contacts-index.gohtml:12
msgctxt "title" msgctxt "title"
msgid "Customer" msgid "Customer"
msgstr "Client" msgstr "Client"
#: web/template/contacts-index.gohtml:22 #: web/template/contacts-index.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Email" msgid "Email"
msgstr "Correu-e" msgstr "Correu-e"
#: web/template/contacts-index.gohtml:23 #: web/template/contacts-index.gohtml:14
msgctxt "title" msgctxt "title"
msgid "Phone" msgid "Phone"
msgstr "Telèfon" msgstr "Telèfon"
#: web/template/contacts-index.gohtml:38 #: web/template/contacts-index.gohtml:29
msgid "No contacts added yet." msgid "No contacts added yet."
msgstr "No hi ha cap contacte." msgstr "No hi ha cap contacte."
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:9 #: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
#: web/template/tax-details.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuració fiscal" msgstr "Configuració fiscal"
#: web/template/tax-details.gohtml:31 #: web/template/tax-details.gohtml:11 web/template/contacts-new.gohtml:11
msgctxt "title"
msgid "Currency"
msgstr "Moneda"
#: web/template/tax-details.gohtml:47
msgctxt "title"
msgid "Tax Name"
msgstr "Nom import"
#: web/template/tax-details.gohtml:48
msgctxt "title"
msgid "Rate (%)"
msgstr "Percentatge"
#: web/template/tax-details.gohtml:71
msgid "No taxes added yet."
msgstr "No hi ha cap impost."
#: web/template/tax-details.gohtml:77
msgctxt "title"
msgid "New Line"
msgstr "Nova línia"
#: web/template/tax-details.gohtml:88
msgctxt "action"
msgid "Add new tax"
msgstr "Afegeix nou impost"
#: 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"
#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:179
msgctxt "input"
msgid "Email"
msgstr "Correu-e"
#: pkg/login.go:47 pkg/profile.go:49
msgctxt "input"
msgid "Password"
msgstr "Contrasenya"
#: 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: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."
#: pkg/login.go:72
msgid "Password can not be empty."
msgstr "No podeu deixar la contrasenya en blanc."
#: pkg/login.go:108
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/company.go:78
msgctxt "input"
msgid "Currency"
msgstr "Moneda"
#: pkg/company.go:95
msgid "Selected currency is not valid."
msgstr "Heu seleccionat una moneda que no és vàlida."
#: pkg/company.go:217
msgctxt "input"
msgid "Tax name"
msgstr "Nom impost"
#: pkg/company.go:223
msgctxt "input"
msgid "Rate (%)"
msgstr "Percentatge"
#: pkg/company.go:245
msgid "Tax name can not be empty."
msgstr "No podeu deixar el nom de limpost en blanc."
#: pkg/company.go:246
msgid "Tax rate can not be empty."
msgstr "No podeu deixar percentatge en blanc."
#: pkg/company.go:247
msgid "Tax rate must be an integer between -99 and 99."
msgstr "El percentatge ha de ser entre -99 i 99."
#: pkg/profile.go:25
msgctxt "language option"
msgid "Automatic"
msgstr "Automàtic"
#: pkg/profile.go:31
msgctxt "input"
msgid "User name"
msgstr "Nom dusuari"
#: pkg/profile.go:57
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmació contrasenya"
#: pkg/profile.go:65
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."
#: pkg/profile.go:93
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/contacts.go:150
msgctxt "input" msgctxt "input"
msgid "Business name" msgid "Business name"
msgstr "Nom i cognoms" msgstr "Nom i cognom"
#: pkg/contacts.go:159 #: web/template/tax-details.gohtml:15 web/template/contacts-new.gohtml:15
msgctxt "input" msgctxt "input"
msgid "VAT number" msgid "VAT number"
msgstr "DNI / NIF" msgstr "DNI / NIF"
#: pkg/contacts.go:165 #: web/template/tax-details.gohtml:19 web/template/contacts-new.gohtml:19
msgctxt "input" msgctxt "input"
msgid "Trade name" msgid "Trade name"
msgstr "Nom comercial" msgstr "Nom comercial"
#: pkg/contacts.go:170 #: web/template/tax-details.gohtml:23 web/template/contacts-new.gohtml:23
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Telèfon" msgstr "Telèfon"
#: pkg/contacts.go:188 #: web/template/tax-details.gohtml:27 web/template/contacts-new.gohtml:27
#: pkg/login.go:33 pkg/profile.go:35
msgctxt "input"
msgid "Email"
msgstr "Correu-e"
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
msgctxt "input" msgctxt "input"
msgid "Web" msgid "Web"
msgstr "Web" msgstr "Web"
#: pkg/contacts.go:196 #: web/template/tax-details.gohtml:35 web/template/contacts-new.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Adreça" msgstr "Adreça"
#: pkg/contacts.go:205 #: web/template/tax-details.gohtml:39 web/template/contacts-new.gohtml:39
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Població" msgstr "Població"
#: pkg/contacts.go:211 #: web/template/tax-details.gohtml:43 web/template/contacts-new.gohtml:43
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Província" msgstr "Província"
#: pkg/contacts.go:217 #: web/template/tax-details.gohtml:47 web/template/contacts-new.gohtml:47
msgctxt "input" msgctxt "input"
msgid "Postal code" msgid "Postal code"
msgstr "Codi postal" msgstr "Codi postal"
#: pkg/contacts.go:226 #: web/template/tax-details.gohtml:56 web/template/contacts-new.gohtml:56
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: pkg/contacts.go:256 #: web/template/tax-details.gohtml:60
msgid "Business name can not be empty." msgctxt "input"
msgstr "No podeu deixar el nom i els cognoms en blanc." msgid "Currency"
msgstr "Moneda"
#: pkg/contacts.go:257 #: web/template/tax-details.gohtml:78
msgid "VAT number can not be empty." msgctxt "title"
msgstr "No podeu deixar el DNI o NIF en blanc." msgid "Tax Name"
msgstr "Nom import"
#: pkg/contacts.go:258 #: web/template/tax-details.gohtml:79
msgid "This value is not a valid VAT number." msgctxt "title"
msgstr "Aquest valor no és un DNI o NIF vàlid." msgid "Rate (%)"
msgstr "Percentatge"
#: pkg/contacts.go:260 #: web/template/tax-details.gohtml:100
msgid "Phone can not be empty." msgid "No taxes added yet."
msgstr "No podeu deixar el telèfon en blanc." msgstr "No hi ha cap impost."
#: pkg/contacts.go:261 #: web/template/tax-details.gohtml:106
msgid "This value is not a valid phone number." msgctxt "title"
msgstr "Aquest valor no és un telèfon vàlid." msgid "New Line"
msgstr "Nova línia"
#: pkg/contacts.go:267 #: web/template/tax-details.gohtml:111
msgid "This value is not a valid web address. It should be like https://domain.com/." msgctxt "input"
msgstr "Aquest valor no és una adreça web vàlida. Hauria de ser similar a https://domini.cat/." msgid "Tax name"
msgstr "Nom impost"
#: pkg/contacts.go:269 #: web/template/tax-details.gohtml:118
msgid "Address can not be empty." msgctxt "input"
msgstr "No podeu deixar ladreça en blanc." msgid "Rate (%)"
msgstr "Percentatge"
#: pkg/contacts.go:270 #: web/template/tax-details.gohtml:125
msgid "City can not be empty." msgctxt "action"
msgstr "No podeu deixar la població en blanc." msgid "Add new tax"
msgstr "Afegeix nou impost"
#: pkg/contacts.go:271 #: web/template/contacts-new.gohtml:2 web/template/contacts-new.gohtml:7
msgid "Province can not be empty." msgctxt "title"
msgstr "No podeu deixar la província en blanc." msgid "New Contact"
msgstr "Nou contacte"
#: pkg/contacts.go:272 #: pkg/login.go:44 pkg/profile.go:41
msgid "Postal code can not be empty." msgctxt "input"
msgstr "No podeu deixar el codi postal en blanc." msgid "Password"
msgstr "Contrasenya"
#: pkg/contacts.go:273 #: pkg/login.go:66 pkg/profile.go:71
msgid "This value is not a valid postal code." msgid "Email can not be empty."
msgstr "Aquest valor no és un codi postal vàlid." msgstr "No podeu deixar el correu-e en blanc."
#: pkg/contacts.go:275 #: pkg/login.go:67 pkg/profile.go:72
msgid "Selected country is not valid." msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Heu seleccionat un país que no és vàlid." msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat."
#: pkg/login.go:69
msgid "Password can not be empty."
msgstr "No podeu deixar la contrasenya en blanc."
#: pkg/login.go:95
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/profile.go:23
msgctxt "language option"
msgid "Automatic"
msgstr "Automàtic"
#: pkg/profile.go:29
msgctxt "input"
msgid "User name"
msgstr "Nom dusuari"
#: pkg/profile.go:46
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmació contrasenya"
#: pkg/profile.go:51
msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:74
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/profile.go:75
msgid "Confirmation does not match password."
msgstr "La confirmació no és igual a la contrasenya."
#: pkg/profile.go:76
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."
#~ msgctxt "nav" #~ msgctxt "nav"
#~ msgid "Customers" #~ msgid "Customers"

325
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-02-03 13:57+0100\n" "POT-Creation-Date: 2023-02-01 11:26+0100\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -32,17 +32,17 @@ msgctxt "menu"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuración fiscal" msgstr "Configuración fiscal"
#: web/template/app.gohtml:34 #: web/template/app.gohtml:33
msgctxt "action" msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Salir" msgstr "Salir"
#: web/template/app.gohtml:43 #: web/template/app.gohtml:42
msgctxt "nav" msgctxt "nav"
msgid "Dashboard" msgid "Dashboard"
msgstr "Panel" msgstr "Panel"
#: web/template/app.gohtml:44 #: web/template/app.gohtml:43
msgctxt "nav" msgctxt "nav"
msgid "Contacts" msgid "Contacts"
msgstr "Contactos" msgstr "Contactos"
@ -57,311 +57,222 @@ msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entrar" msgstr "Entrar"
#: web/template/profile.gohtml:2 web/template/profile.gohtml:10 #: web/template/profile.gohtml:2 web/template/profile.gohtml:7
#: web/template/profile.gohtml:14
msgctxt "title" msgctxt "title"
msgid "User Settings" msgid "User Settings"
msgstr "Configuración usuario" msgstr "Configuración usuario"
#: web/template/profile.gohtml:9 web/template/contacts-edit.gohtml:9 #: web/template/profile.gohtml:10
#: 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" msgctxt "title"
msgid "User Access Data" msgid "User Access Data"
msgstr "Datos acceso usuario" msgstr "Datos acceso usuario"
#: web/template/profile.gohtml:24 #: web/template/profile.gohtml:16
msgctxt "title" msgctxt "title"
msgid "Password Change" msgid "Password Change"
msgstr "Cambio de contraseña" msgstr "Cambio de contraseña"
#: web/template/profile.gohtml:31 #: web/template/profile.gohtml:23
msgctxt "title" msgctxt "title"
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:96 #: web/template/profile.gohtml:27 web/template/tax-details.gohtml:133
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Guardar cambios" msgstr "Guardar cambios"
#: web/template/contacts-edit.gohtml:2 web/template/contacts-edit.gohtml:15 #: web/template/contacts-index.gohtml:2
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" msgctxt "title"
msgid "Contacts" msgid "Contacts"
msgstr "Contactos" msgstr "Contactos"
#: web/template/contacts-edit.gohtml:33 #: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
msgctxt "action"
msgid "Update contact"
msgstr "Actualizar contacto"
#: web/template/contacts-index.gohtml:13 web/template/contacts-new.gohtml:31
msgctxt "action" msgctxt "action"
msgid "New contact" msgid "New contact"
msgstr "Nuevo contacto" msgstr "Nuevo contacto"
#: web/template/contacts-index.gohtml:20 #: web/template/contacts-index.gohtml:11
msgctxt "contact" msgctxt "contact"
msgid "All" msgid "All"
msgstr "Todos" msgstr "Todos"
#: web/template/contacts-index.gohtml:21 #: web/template/contacts-index.gohtml:12
msgctxt "title" msgctxt "title"
msgid "Customer" msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: web/template/contacts-index.gohtml:22 #: web/template/contacts-index.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Email" msgid "Email"
msgstr "Correo-e" msgstr "Correo-e"
#: web/template/contacts-index.gohtml:23 #: web/template/contacts-index.gohtml:14
msgctxt "title" msgctxt "title"
msgid "Phone" msgid "Phone"
msgstr "Teléfono" msgstr "Teléfono"
#: web/template/contacts-index.gohtml:38 #: web/template/contacts-index.gohtml:29
msgid "No contacts added yet." msgid "No contacts added yet."
msgstr "No hay contactos." msgstr "No hay contactos."
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:9 #: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
#: web/template/tax-details.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuración fiscal" msgstr "Configuración fiscal"
#: web/template/tax-details.gohtml:31 #: web/template/tax-details.gohtml:11 web/template/contacts-new.gohtml:11
msgctxt "title"
msgid "Currency"
msgstr "Moneda"
#: web/template/tax-details.gohtml:47
msgctxt "title"
msgid "Tax Name"
msgstr "Nombre impuesto"
#: web/template/tax-details.gohtml:48
msgctxt "title"
msgid "Rate (%)"
msgstr "Porcentaje"
#: web/template/tax-details.gohtml:71
msgid "No taxes added yet."
msgstr "No hay impuestos."
#: web/template/tax-details.gohtml:77
msgctxt "title"
msgid "New Line"
msgstr "Nueva línea"
#: web/template/tax-details.gohtml:88
msgctxt "action"
msgid "Add new tax"
msgstr "Añadir nuevo impuesto"
#: 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"
#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:179
msgctxt "input"
msgid "Email"
msgstr "Correo-e"
#: pkg/login.go:47 pkg/profile.go:49
msgctxt "input"
msgid "Password"
msgstr "Contraseña"
#: 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: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."
#: pkg/login.go:72
msgid "Password can not be empty."
msgstr "No podéis dejar la contraseña en blanco."
#: pkg/login.go:108
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/company.go:78
msgctxt "input"
msgid "Currency"
msgstr "Moneda"
#: pkg/company.go:95
msgid "Selected currency is not valid."
msgstr "Habéis escogido una moneda que no es válida."
#: pkg/company.go:217
msgctxt "input"
msgid "Tax name"
msgstr "Nombre impuesto"
#: pkg/company.go:223
msgctxt "input"
msgid "Rate (%)"
msgstr "Porcentaje"
#: pkg/company.go:245
msgid "Tax name can not be empty."
msgstr "No podéis dejar el nombre del impuesto en blanco."
#: pkg/company.go:246
msgid "Tax rate can not be empty."
msgstr "No podéis dejar el porcentaje en blanco."
#: pkg/company.go:247
msgid "Tax rate must be an integer between -99 and 99."
msgstr "El porcentaje tiene que estar entre -99 y 99."
#: pkg/profile.go:25
msgctxt "language option"
msgid "Automatic"
msgstr "Automático"
#: pkg/profile.go:31
msgctxt "input"
msgid "User name"
msgstr "Nombre de usuario"
#: pkg/profile.go:57
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmación contraseña"
#: pkg/profile.go:65
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."
#: pkg/profile.go:93
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."
#: pkg/contacts.go:150
msgctxt "input" msgctxt "input"
msgid "Business name" msgid "Business name"
msgstr "Nombre y apellidos" msgstr "Nombre y apellidos"
#: pkg/contacts.go:159 #: web/template/tax-details.gohtml:15 web/template/contacts-new.gohtml:15
msgctxt "input" msgctxt "input"
msgid "VAT number" msgid "VAT number"
msgstr "DNI / NIF" msgstr "DNI / NIF"
#: pkg/contacts.go:165 #: web/template/tax-details.gohtml:19 web/template/contacts-new.gohtml:19
msgctxt "input" msgctxt "input"
msgid "Trade name" msgid "Trade name"
msgstr "Nombre comercial" msgstr "Nombre comercial"
#: pkg/contacts.go:170 #: web/template/tax-details.gohtml:23 web/template/contacts-new.gohtml:23
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Teléfono" msgstr "Teléfono"
#: pkg/contacts.go:188 #: web/template/tax-details.gohtml:27 web/template/contacts-new.gohtml:27
#: pkg/login.go:33 pkg/profile.go:35
msgctxt "input"
msgid "Email"
msgstr "Correo-e"
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
msgctxt "input" msgctxt "input"
msgid "Web" msgid "Web"
msgstr "Web" msgstr "Web"
#: pkg/contacts.go:196 #: web/template/tax-details.gohtml:35 web/template/contacts-new.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Dirección" msgstr "Dirección"
#: pkg/contacts.go:205 #: web/template/tax-details.gohtml:39 web/template/contacts-new.gohtml:39
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Población" msgstr "Población"
#: pkg/contacts.go:211 #: web/template/tax-details.gohtml:43 web/template/contacts-new.gohtml:43
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Provincia" msgstr "Provincia"
#: pkg/contacts.go:217 #: web/template/tax-details.gohtml:47 web/template/contacts-new.gohtml:47
msgctxt "input" msgctxt "input"
msgid "Postal code" msgid "Postal code"
msgstr "Código postal" msgstr "Código postal"
#: pkg/contacts.go:226 #: web/template/tax-details.gohtml:56 web/template/contacts-new.gohtml:56
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: pkg/contacts.go:256 #: web/template/tax-details.gohtml:60
msgid "Business name can not be empty." msgctxt "input"
msgstr "No podéis dejar el nombre y los apellidos en blanco." msgid "Currency"
msgstr "Moneda"
#: pkg/contacts.go:257 #: web/template/tax-details.gohtml:78
msgid "VAT number can not be empty." msgctxt "title"
msgstr "No podéis dejar el DNI o NIF en blanco." msgid "Tax Name"
msgstr "Nombre impuesto"
#: pkg/contacts.go:258 #: web/template/tax-details.gohtml:79
msgid "This value is not a valid VAT number." msgctxt "title"
msgstr "Este valor no es un DNI o NIF válido." msgid "Rate (%)"
msgstr "Porcentage"
#: pkg/contacts.go:260 #: web/template/tax-details.gohtml:100
msgid "Phone can not be empty." msgid "No taxes added yet."
msgstr "No podéis dejar el teléfono en blanco." msgstr "No hay impuestos."
#: pkg/contacts.go:261 #: web/template/tax-details.gohtml:106
msgid "This value is not a valid phone number." msgctxt "title"
msgstr "Este valor no es un teléfono válido." msgid "New Line"
msgstr "Nueva línea"
#: pkg/contacts.go:267 #: web/template/tax-details.gohtml:111
msgid "This value is not a valid web address. It should be like https://domain.com/." msgctxt "input"
msgstr "Este valor no es una dirección web válida. Tiene que ser parecida a https://dominio.es/." msgid "Tax name"
msgstr "Nombre impuesto"
#: pkg/contacts.go:269 #: web/template/tax-details.gohtml:118
msgid "Address can not be empty." msgctxt "input"
msgstr "No podéis dejar la dirección en blanco." msgid "Rate (%)"
msgstr "Porcentage"
#: pkg/contacts.go:270 #: web/template/tax-details.gohtml:125
msgid "City can not be empty." msgctxt "action"
msgstr "No podéis dejar la población en blanco." msgid "Add new tax"
msgstr "Añadir nuevo impuesto"
#: pkg/contacts.go:271 #: web/template/contacts-new.gohtml:2 web/template/contacts-new.gohtml:7
msgid "Province can not be empty." msgctxt "title"
msgstr "No podéis dejar la provincia en blanco." msgid "New Contact"
msgstr "Nuevo contacto"
#: pkg/contacts.go:272 #: pkg/login.go:44 pkg/profile.go:41
msgid "Postal code can not be empty." msgctxt "input"
msgstr "No podéis dejar el código postal en blanco." msgid "Password"
msgstr "Contraseña"
#: pkg/contacts.go:273 #: pkg/login.go:66 pkg/profile.go:71
msgid "This value is not a valid postal code." msgid "Email can not be empty."
msgstr "Este valor no es un código postal válido válido." msgstr "No podéis dejar el correo-e en blanco."
#: pkg/contacts.go:275 #: pkg/login.go:67 pkg/profile.go:72
msgid "Selected country is not valid." msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Habéis escogido un país que no es válido." msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es."
#: pkg/login.go:69
msgid "Password can not be empty."
msgstr "No podéis dejar la contaseña en blanco."
#: pkg/login.go:95
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/profile.go:23
msgctxt "language option"
msgid "Automatic"
msgstr "Automático"
#: pkg/profile.go:29
msgctxt "input"
msgid "User name"
msgstr "Nombre de usuario"
#: pkg/profile.go:46
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmación contrasenya"
#: pkg/profile.go:51
msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:74
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/profile.go:75
msgid "Confirmation does not match password."
msgstr "La confirmación no corresponde con la contraseña."
#: pkg/profile.go:76
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."
#~ msgctxt "nav" #~ msgctxt "nav"
#~ msgid "Customers" #~ msgid "Customers"

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +0,0 @@
{{ define "title" -}}
{{printf (pgettext "Edit Contact “%s”" "title") .BusinessName.Val }}
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.contactForm*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/contacts"}}">{{( pgettext "Contacts" "title" )}}</a> /
<a>{{ .BusinessName.Val }}</a>
</p>
</nav>
<section class="dialog-content">
<h2>{{printf (pgettext "Edit Contact “%s”" "title") .BusinessName.Val }}</h2>
<form method="POST">
{{ csrfToken }}
{{ putMethod }}
{{ template "input-field" .BusinessName | addInputAttr "autofocus" }}
{{ template "input-field" .VATIN }}
{{ template "input-field" .TradeName }}
{{ template "input-field" .Phone }}
{{ template "input-field" .Email }}
{{ template "input-field" .Web }}
{{ template "input-field" .Address | addInputAttr `class="width-2x"` }}
{{ template "input-field" .City }}
{{ template "input-field" .Province }}
{{ template "input-field" .PostalCode }}
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
<fieldset>
<button class="primary" type="submit">{{( pgettext "Update contact" "action" )}}</button>
</fieldset>
</form>
</section>
{{- end }}

View File

@ -1,43 +1,34 @@
{{ define "title" -}} {{ define "title" -}}
{{( pgettext "Contacts" "title" )}} {{( pgettext "Contacts" "title" )}}
{{- end }} {{- end }}
{{ define "content" }} {{ define "content" }}
<nav> <a class="primary button" href="{{ companyURI "/contacts/new" }}">{{( pgettext "New contact" "action" )}}</a>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a>{{( pgettext "Contacts" "title" )}}</a>
</p>
<p>
<a class="primary button"
href="{{ companyURI "/contacts/new" }}">{{( pgettext "New contact" "action" )}}</a>
</p>
</nav>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{{( pgettext "All" "contact" )}}</th> <th>{{( pgettext "All" "contact" )}}</th>
<th>{{( pgettext "Customer" "title" )}}</th> <th>{{( pgettext "Customer" "title" )}}</th>
<th>{{( pgettext "Email" "title" )}}</th> <th>{{( pgettext "Email" "title" )}}</th>
<th>{{( pgettext "Phone" "title" )}}</th> <th>{{( pgettext "Phone" "title" )}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ with .Contacts }} {{ with .Contacts }}
{{- range $tax := . }} {{- range $tax := . }}
<tr> <tr>
<td></td> <td></td>
<td><a href="{{ companyURI "/contacts/"}}{{ .Slug }}">{{ .Name }}</a></td> <td>{{ .Name }}</td>
<td><a href="mailto:{{ .Email }}">{{ .Email }}</a></td> <td>{{ .Email }}</td>
<td><a href="tel:{{ .Phone }}">{{ .Phone }}</a></td> <td>{{ .Phone }}</td>
</tr> </tr>
{{- end }} {{- end }}
{{ else }} {{ else }}
<tr> <tr>
<td colspan="4">{{( gettext "No contacts added yet." )}}</td> <td colspan="4">{{( gettext "No contacts added yet." )}}</td>
</tr> </tr>
{{ end }} {{ end }}
</tbody> </tbody>
</table> </table>
{{- end }} {{- end }}

View File

@ -4,13 +4,6 @@
{{ define "content" }} {{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.contactForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.contactForm*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/contacts"}}">{{( pgettext "Contacts" "title" )}}</a> /
<a>{{( pgettext "New Contact" "title" )}}</a>
</p>
</nav>
<section class="dialog-content"> <section class="dialog-content">
<h2>{{(pgettext "New Contact" "title")}}</h2> <h2>{{(pgettext "New Contact" "title")}}</h2>
<form method="POST" action="{{ companyURI "/contacts" }}"> <form method="POST" action="{{ companyURI "/contacts" }}">
@ -28,7 +21,7 @@
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }} {{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
<fieldset> <fieldset>
<button class="primary" type="submit">{{( pgettext "New contact" "action" )}}</button> <button type="submit">{{( pgettext "New contact" "action" )}}</button>
</fieldset> </fieldset>
</form> </form>

View File

@ -4,12 +4,6 @@
{{ define "content" }} {{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.profileForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.profileForm*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a>{{( pgettext "User Settings" "title" )}}</a>
</p>
</nav>
<section class="dialog-content"> <section class="dialog-content">
<h2>{{(pgettext "User Settings" "title")}}</h2> <h2>{{(pgettext "User Settings" "title")}}</h2>
<form method="POST"> <form method="POST">

View File

@ -3,12 +3,6 @@
{{- end }} {{- end }}
{{ define "content" }} {{ define "content" }}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a>{{( pgettext "Tax Details" "title" )}}</a>
</p>
</nav>
<section class="dialog-content"> <section class="dialog-content">
<h2>{{(pgettext "Tax Details" "title")}}</h2> <h2>{{(pgettext "Tax Details" "title")}}</h2>
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.TaxDetailsPage*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.TaxDetailsPage*/ -}}
@ -59,7 +53,7 @@
<td> <td>
<form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}"> <form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}">
{{ csrfToken }} {{ csrfToken }}
{{ deleteMethod }} <input type="hidden" name="_method" value="DELETE"/>
<button class="icon" aria-label="{{( gettext "Delete tax" )}}" type="submit"><i <button class="icon" aria-label="{{( gettext "Delete tax" )}}" type="submit"><i
class="ri-delete-back-2-line"></i></button> class="ri-delete-back-2-line"></i></button>
</form> </form>