Compare commits
No commits in common. "7a439a40cc87cd68b593e14ec6a7c49fa989a9dd" and "b1c653e7de82477cbae233710a858392ed8d3e76" have entirely different histories.
7a439a40cc
...
b1c653e7de
|
@ -8,7 +8,7 @@ values (1, 'demo@numerus', 'Demo User', 'demo', 'invoicer')
|
||||||
;
|
;
|
||||||
|
|
||||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code)
|
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code)
|
||||||
values (1, 'Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de l’Hort', 'Castelló d’Empúries', 'Girona', '17486', 'ES', 'EUR');
|
values (1, 'Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de l’Hort', 'Castelló d’Empúries', 'Alt Empordà', '17486', 'ES', 'EUR');
|
||||||
|
|
||||||
insert into company_user (company_id, user_id)
|
insert into company_user (company_id, user_id)
|
||||||
values (1, 1)
|
values (1, 1)
|
||||||
|
|
216
pkg/company.go
216
pkg/company.go
|
@ -3,7 +3,6 @@ package pkg
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -83,75 +82,54 @@ type Tax struct {
|
||||||
Rate int
|
Rate int
|
||||||
}
|
}
|
||||||
|
|
||||||
type taxDetailsForm struct {
|
|
||||||
*contactForm
|
|
||||||
Currency *SelectField
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDetailsForm {
|
|
||||||
return &taxDetailsForm{
|
|
||||||
contactForm: newContactForm(ctx, conn, locale),
|
|
||||||
Currency: &SelectField{
|
|
||||||
Name: "currency",
|
|
||||||
Label: pgettext("input", "Currency", locale),
|
|
||||||
Options: MustGetOptions(ctx, conn, "select currency_code, currency_symbol from currency order by currency_code"),
|
|
||||||
Selected: "EUR",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *taxDetailsForm) Parse(r *http.Request) error {
|
|
||||||
if err := form.contactForm.Parse(r); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
form.Currency.FillValue(r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *taxDetailsForm) Validate(ctx context.Context, conn *Conn) bool {
|
|
||||||
validator := newFormValidator()
|
|
||||||
validator.CheckValidSelectOption(form.Currency, gettext("Selected currency is not valid.", form.locale))
|
|
||||||
return form.contactForm.Validate(ctx, conn) && validator.AllOK()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *taxDetailsForm) mustFillFromDatabase(ctx context.Context, conn *Conn, company *Company) *taxDetailsForm {
|
|
||||||
err := conn.QueryRow(ctx, "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code from company where company_id = $1", company.Id).Scan(form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return form
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaxDetailsPage struct {
|
type TaxDetailsPage struct {
|
||||||
DetailsForm *taxDetailsForm
|
BusinessName string
|
||||||
NewTaxForm *newTaxForm
|
VATIN string
|
||||||
Taxes []*Tax
|
TradeName string
|
||||||
|
Phone string
|
||||||
|
Email string
|
||||||
|
Web string
|
||||||
|
Address string
|
||||||
|
City string
|
||||||
|
Province string
|
||||||
|
PostalCode string
|
||||||
|
CountryCode string
|
||||||
|
Countries []CountryOption
|
||||||
|
CurrencyCode string
|
||||||
|
Currencies []CurrencyOption
|
||||||
|
Taxes []Tax
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompanyTaxDetailsHandler() http.Handler {
|
func CompanyTaxDetailsHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
conn := getConn(r)
|
page := &TaxDetailsPage{}
|
||||||
page := &TaxDetailsPage{
|
|
||||||
DetailsForm: newTaxDetailsForm(r.Context(), conn, locale),
|
|
||||||
NewTaxForm: newNewTaxForm(locale),
|
|
||||||
}
|
|
||||||
company := mustGetCompany(r)
|
company := mustGetCompany(r)
|
||||||
|
conn := getConn(r)
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
if err := page.DetailsForm.Parse(r); err != nil {
|
r.ParseForm()
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
page.BusinessName = r.FormValue("business_name")
|
||||||
return
|
page.CountryCode = r.FormValue("country")
|
||||||
}
|
page.VATIN = page.CountryCode + r.FormValue("vatin")
|
||||||
if ok := page.DetailsForm.Validate(r.Context(), conn); ok {
|
page.TradeName = r.FormValue("trade_name")
|
||||||
form := page.DetailsForm
|
page.Phone = r.FormValue("phone")
|
||||||
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)
|
page.Email = r.FormValue("email")
|
||||||
|
page.Web = r.FormValue("web")
|
||||||
|
page.Address = r.FormValue("address")
|
||||||
|
page.City = r.FormValue("city")
|
||||||
|
page.Province = r.FormValue("province")
|
||||||
|
page.PostalCode = r.FormValue("postal_code")
|
||||||
|
page.CurrencyCode = r.FormValue("currency")
|
||||||
|
conn.MustExec(r.Context(), "update company set business_name = $1, vatin = $2, 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", page.BusinessName, page.VATIN, page.TradeName, page.Phone, page.Email, page.Web, page.Address, page.City, page.Province, page.PostalCode, page.CountryCode, page.CurrencyCode, company.Id)
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
||||||
} else {
|
} else {
|
||||||
page.DetailsForm.mustFillFromDatabase(r.Context(), conn, company)
|
err := conn.QueryRow(r.Context(), "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code from company where company_id = $1", company.Id).Scan(&page.BusinessName, &page.VATIN, &page.TradeName, &page.Phone, &page.Email, &page.Web, &page.Address, &page.City, &page.Province, &page.PostalCode, &page.CountryCode, &page.CurrencyCode)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
page.Countries = mustGetCountryOptions(r.Context(), conn, locale)
|
||||||
|
page.Currencies = mustGetCurrencyOptions(r.Context(), conn)
|
||||||
page.Taxes = mustGetTaxes(r.Context(), conn, company)
|
page.Taxes = mustGetTaxes(r.Context(), conn, company)
|
||||||
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
|
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
|
||||||
})
|
})
|
||||||
|
@ -165,16 +143,62 @@ func mustGetCompany(r *http.Request) *Company {
|
||||||
return company
|
return company
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax {
|
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []CountryOption {
|
||||||
|
rows, err := conn.Query(ctx, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var countries []CountryOption
|
||||||
|
for rows.Next() {
|
||||||
|
var country CountryOption
|
||||||
|
err = rows.Scan(&country.Code, &country.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
countries = append(countries, country)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return countries
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetCurrencyOptions(ctx context.Context, conn *Conn) []CurrencyOption {
|
||||||
|
rows, err := conn.Query(ctx, "select currency_code, currency_symbol from currency order by currency_code")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var currencies []CurrencyOption
|
||||||
|
for rows.Next() {
|
||||||
|
var currency CurrencyOption
|
||||||
|
err = rows.Scan(¤cy.Code, ¤cy.Symbol)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
currencies = append(currencies, currency)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return currencies
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []Tax {
|
||||||
rows, err := conn.Query(ctx, "select tax_id, name, (rate * 100)::integer from tax where company_id = $1 order by rate, name", company.Id)
|
rows, err := conn.Query(ctx, "select tax_id, name, (rate * 100)::integer from tax where company_id = $1 order by rate, name", company.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var taxes []*Tax
|
var taxes []Tax
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
tax := &Tax{}
|
var tax Tax
|
||||||
err = rows.Scan(&tax.Id, &tax.Name, &tax.Rate)
|
err = rows.Scan(&tax.Id, &tax.Name, &tax.Rate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -188,52 +212,6 @@ func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax {
|
||||||
return taxes
|
return taxes
|
||||||
}
|
}
|
||||||
|
|
||||||
type newTaxForm struct {
|
|
||||||
locale *Locale
|
|
||||||
Name *InputField
|
|
||||||
Rate *InputField
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNewTaxForm(locale *Locale) *newTaxForm {
|
|
||||||
return &newTaxForm{
|
|
||||||
locale: locale,
|
|
||||||
Name: &InputField{
|
|
||||||
Name: "tax_name",
|
|
||||||
Label: pgettext("input", "Tax name", locale),
|
|
||||||
Type: "text",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
Rate: &InputField{
|
|
||||||
Name: "tax_rate",
|
|
||||||
Label: pgettext("input", "Rate (%)", locale),
|
|
||||||
Type: "number",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
"min=-99",
|
|
||||||
"max=99",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *newTaxForm) Parse(r *http.Request) error {
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
form.Name.FillValue(r)
|
|
||||||
form.Rate.FillValue(r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *newTaxForm) Validate() bool {
|
|
||||||
validator := newFormValidator()
|
|
||||||
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)) {
|
|
||||||
validator.CheckValidInteger(form.Rate, -99, 99, gettext("Tax rate must be an integer between -99 and 99.", form.locale))
|
|
||||||
}
|
|
||||||
return validator.AllOK()
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompanyTaxHandler() http.Handler {
|
func CompanyTaxHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
param := r.URL.Path
|
param := r.URL.Path
|
||||||
|
@ -244,26 +222,12 @@ func CompanyTaxHandler() http.Handler {
|
||||||
company := mustGetCompany(r)
|
company := mustGetCompany(r)
|
||||||
if taxId, err := strconv.Atoi(param); err == nil {
|
if taxId, err := strconv.Atoi(param); err == nil {
|
||||||
conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId)
|
conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId)
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
|
||||||
} else {
|
} else {
|
||||||
locale := getLocale(r)
|
r.ParseForm()
|
||||||
form := newNewTaxForm(locale)
|
name := r.FormValue("name")
|
||||||
if err := form.Parse(r); err != nil {
|
rate, _ := strconv.Atoi(r.FormValue("rate"))
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, name, rate)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
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())
|
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
||||||
}
|
|
||||||
page := &TaxDetailsPage{
|
|
||||||
DetailsForm: newTaxDetailsForm(r.Context(), conn, locale).mustFillFromDatabase(r.Context(), conn, company),
|
|
||||||
NewTaxForm: form,
|
|
||||||
Taxes: mustGetTaxes(r.Context(), conn, company),
|
|
||||||
}
|
|
||||||
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
192
pkg/contacts.go
192
pkg/contacts.go
|
@ -2,7 +2,6 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,18 +20,22 @@ func ContactsHandler() http.Handler {
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
company := getCompany(r)
|
company := getCompany(r)
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
locale := getLocale(r)
|
r.ParseForm()
|
||||||
form := newContactForm(r.Context(), conn, locale)
|
page := &NewContactPage{
|
||||||
if err := form.Parse(r); err != nil {
|
BusinessName: r.FormValue("business_name"),
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
VATIN: r.FormValue("vatin"),
|
||||||
return
|
TradeName: r.FormValue("trade_name"),
|
||||||
|
Phone: r.FormValue("phone"),
|
||||||
|
Email: r.FormValue("email"),
|
||||||
|
Web: r.FormValue("web"),
|
||||||
|
Address: r.FormValue("address"),
|
||||||
|
City: r.FormValue("city"),
|
||||||
|
Province: r.FormValue("province"),
|
||||||
|
PostalCode: r.FormValue("postal_code"),
|
||||||
|
CountryCode: r.FormValue("country"),
|
||||||
}
|
}
|
||||||
if form.Validate(r.Context(), conn) {
|
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, page.BusinessName, page.VATIN, page.TradeName, page.Phone, page.Email, page.Web, page.Address, page.City, page.Province, page.PostalCode, page.CountryCode)
|
||||||
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)
|
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/contacts", http.StatusSeeOther)
|
http.Redirect(w, r, "/company/"+company.Slug+"/contacts", http.StatusSeeOther)
|
||||||
} else {
|
|
||||||
mustRenderAppTemplate(w, r, "contacts-new.gohtml", form)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
page := &ContactsIndexPage{
|
page := &ContactsIndexPage{
|
||||||
Contacts: mustGetContactEntries(r.Context(), conn, company),
|
Contacts: mustGetContactEntries(r.Context(), conn, company),
|
||||||
|
@ -65,160 +68,29 @@ func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
type contactForm struct {
|
type NewContactPage struct {
|
||||||
locale *Locale
|
BusinessName string
|
||||||
BusinessName *InputField
|
VATIN string
|
||||||
VATIN *InputField
|
TradeName string
|
||||||
TradeName *InputField
|
Phone string
|
||||||
Phone *InputField
|
Email string
|
||||||
Email *InputField
|
Web string
|
||||||
Web *InputField
|
Address string
|
||||||
Address *InputField
|
City string
|
||||||
City *InputField
|
Province string
|
||||||
Province *InputField
|
PostalCode string
|
||||||
PostalCode *InputField
|
CountryCode string
|
||||||
Country *SelectField
|
Countries []CountryOption
|
||||||
}
|
|
||||||
|
|
||||||
func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactForm {
|
|
||||||
return &contactForm{
|
|
||||||
locale: locale,
|
|
||||||
BusinessName: &InputField{
|
|
||||||
Name: "business_name",
|
|
||||||
Label: pgettext("input", "Business name", locale),
|
|
||||||
Type: "text",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="organization"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
VATIN: &InputField{
|
|
||||||
Name: "vatin",
|
|
||||||
Label: pgettext("input", "VAT number", locale),
|
|
||||||
Type: "text",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
TradeName: &InputField{
|
|
||||||
Name: "trade_name",
|
|
||||||
Label: pgettext("input", "Trade name", locale),
|
|
||||||
Type: "text",
|
|
||||||
},
|
|
||||||
Phone: &InputField{
|
|
||||||
Name: "phone",
|
|
||||||
Label: pgettext("input", "Phone", locale),
|
|
||||||
Type: "tel",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="tel"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Email: &InputField{
|
|
||||||
Name: "email",
|
|
||||||
Label: pgettext("input", "Email", locale),
|
|
||||||
Type: "email",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="email"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Web: &InputField{
|
|
||||||
Name: "web",
|
|
||||||
Label: pgettext("input", "Web", locale),
|
|
||||||
Type: "url",
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="url"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Address: &InputField{
|
|
||||||
Name: "address",
|
|
||||||
Label: pgettext("input", "Address", locale),
|
|
||||||
Type: "text",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="address-line1"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
City: &InputField{
|
|
||||||
Name: "city",
|
|
||||||
Label: pgettext("input", "City", locale),
|
|
||||||
Type: "text",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
Province: &InputField{
|
|
||||||
Name: "province",
|
|
||||||
Label: pgettext("input", "Province", locale),
|
|
||||||
Type: "text",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
PostalCode: &InputField{
|
|
||||||
Name: "postal_code",
|
|
||||||
Label: pgettext("input", "Postal code", locale),
|
|
||||||
Type: "text",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="postal-code"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Country: &SelectField{
|
|
||||||
Name: "country",
|
|
||||||
Label: pgettext("input", "Country", locale),
|
|
||||||
Options: mustGetCountryOptions(ctx, conn, locale),
|
|
||||||
Selected: "ES",
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="country"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *contactForm) Parse(r *http.Request) error {
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
form.BusinessName.FillValue(r)
|
|
||||||
form.VATIN.FillValue(r)
|
|
||||||
form.TradeName.FillValue(r)
|
|
||||||
form.Phone.FillValue(r)
|
|
||||||
form.Email.FillValue(r)
|
|
||||||
form.Web.FillValue(r)
|
|
||||||
form.Address.FillValue(r)
|
|
||||||
form.City.FillValue(r)
|
|
||||||
form.Province.FillValue(r)
|
|
||||||
form.PostalCode.FillValue(r)
|
|
||||||
form.Country.FillValue(r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool {
|
|
||||||
validator := newFormValidator()
|
|
||||||
validator.CheckRequiredInput(form.BusinessName, gettext("Business name can not be empty.", form.locale))
|
|
||||||
if validator.CheckRequiredInput(form.VATIN, gettext("VAT number can not be empty.", form.locale)) {
|
|
||||||
validator.CheckValidVATINInput(form.VATIN, form.Country.Selected, gettext("This value is not a valid VAT number.", form.locale))
|
|
||||||
}
|
|
||||||
if validator.CheckRequiredInput(form.Phone, gettext("Phone can not be empty.", form.locale)) {
|
|
||||||
validator.CheckValidPhoneInput(form.Phone, form.Country.Selected, gettext("This value is not a valid phone number.", form.locale))
|
|
||||||
}
|
|
||||||
if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", form.locale)) {
|
|
||||||
validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale))
|
|
||||||
}
|
|
||||||
if form.Web.Val != "" {
|
|
||||||
validator.checkValidURL(form.Web, gettext("This value is not a valid web address. It should be like https://domain.com/.", form.locale))
|
|
||||||
}
|
|
||||||
validator.CheckRequiredInput(form.Address, gettext("Address can not be empty.", form.locale))
|
|
||||||
validator.CheckRequiredInput(form.City, gettext("City can not be empty.", form.locale))
|
|
||||||
validator.CheckRequiredInput(form.Province, gettext("Province can not be empty.", form.locale))
|
|
||||||
if validator.CheckRequiredInput(form.PostalCode, gettext("Postal code can not be empty.", form.locale)) {
|
|
||||||
validator.CheckValidPostalCode(ctx, conn, form.PostalCode, form.Country.Selected, gettext("This value is not a valid postal code.", form.locale))
|
|
||||||
}
|
|
||||||
validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale))
|
|
||||||
return validator.AllOK()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContactHandler() http.Handler {
|
func NewContactHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
form := newContactForm(r.Context(), conn, locale)
|
page := &NewContactPage{
|
||||||
mustRenderAppTemplate(w, r, "contacts-new.gohtml", form)
|
CountryCode: "ES",
|
||||||
|
Countries: mustGetCountryOptions(r.Context(), conn, locale),
|
||||||
|
}
|
||||||
|
mustRenderAppTemplate(w, r, "contacts-new.gohtml", page)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
135
pkg/form.go
135
pkg/form.go
|
@ -2,52 +2,29 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql/driver"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Attribute struct {
|
|
||||||
Key, Val string
|
|
||||||
}
|
|
||||||
|
|
||||||
type InputField struct {
|
type InputField struct {
|
||||||
Name string
|
Name string
|
||||||
Label string
|
Label string
|
||||||
Type string
|
Type string
|
||||||
Val string
|
Value string
|
||||||
Required bool
|
Required bool
|
||||||
Attributes []template.HTMLAttr
|
|
||||||
Errors []error
|
Errors []error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (field *InputField) Scan(value interface{}) error {
|
func (field *InputField) Equals(other *InputField) bool {
|
||||||
if value == nil {
|
return field.Value == other.Value
|
||||||
field.Val = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
field.Val = fmt.Sprintf("%v", value)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (field *InputField) Value() (driver.Value, error) {
|
func (field *InputField) IsEmpty() bool {
|
||||||
return field.Val, nil
|
return field.Value == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (field *InputField) FillValue(r *http.Request) {
|
func (field *InputField) HasValidEmail() bool {
|
||||||
field.Val = strings.TrimSpace(r.FormValue(field.Name))
|
_, err := mail.ParseAddress(field.Value)
|
||||||
}
|
return err == nil
|
||||||
|
|
||||||
func (field *InputField) Integer() int {
|
|
||||||
value, _ := strconv.Atoi(field.Val)
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectOption struct {
|
type SelectOption struct {
|
||||||
|
@ -60,27 +37,9 @@ type SelectField struct {
|
||||||
Label string
|
Label string
|
||||||
Selected string
|
Selected string
|
||||||
Options []*SelectOption
|
Options []*SelectOption
|
||||||
Attributes []template.HTMLAttr
|
|
||||||
Errors []error
|
Errors []error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (field *SelectField) Scan(value interface{}) error {
|
|
||||||
if value == nil {
|
|
||||||
field.Selected = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
field.Selected = fmt.Sprintf("%v", value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (field *SelectField) Value() (driver.Value, error) {
|
|
||||||
return field.Selected, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (field *SelectField) FillValue(r *http.Request) {
|
|
||||||
field.Selected = r.FormValue(field.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (field *SelectField) HasValidOption() bool {
|
func (field *SelectField) HasValidOption() bool {
|
||||||
for _, option := range field.Options {
|
for _, option := range field.Options {
|
||||||
if option.Value == field.Selected {
|
if option.Value == field.Selected {
|
||||||
|
@ -112,81 +71,3 @@ func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interfa
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
|
|
||||||
return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormValidator struct {
|
|
||||||
Valid bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFormValidator() *FormValidator {
|
|
||||||
return &FormValidator{true}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) AllOK() bool {
|
|
||||||
return v.Valid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckRequiredInput(field *InputField, message string) bool {
|
|
||||||
return v.checkInput(field, field.Val != "", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
|
|
||||||
_, err := mail.ParseAddress(field.Val)
|
|
||||||
return v.checkInput(field, err == nil, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidVATINInput(field *InputField, country string, message string) bool {
|
|
||||||
// TODO: actual VATIN validation
|
|
||||||
return v.checkInput(field, true, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidPhoneInput(field *InputField, country string, message string) bool {
|
|
||||||
// TODO: actual phone validation
|
|
||||||
return v.checkInput(field, true, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
|
|
||||||
return v.checkInput(confirm, password.Val == confirm.Val, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidSelectOption(field *SelectField, message string) bool {
|
|
||||||
return v.checkSelect(field, field.HasValidOption(), message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) checkValidURL(field *InputField, message string) bool {
|
|
||||||
_, err := url.ParseRequestURI(field.Val)
|
|
||||||
return v.checkInput(field, err == nil, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
|
||||||
pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$"
|
|
||||||
match, err := regexp.MatchString(pattern, field.Val)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v.checkInput(field, match, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidInteger(field *InputField, min int, max int, message string) bool {
|
|
||||||
value, err := strconv.Atoi(field.Val)
|
|
||||||
return v.checkInput(field, err == nil && value >= min && value <= max, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) checkInput(field *InputField, ok bool, message string) bool {
|
|
||||||
if !ok {
|
|
||||||
field.Errors = append(field.Errors, errors.New(message))
|
|
||||||
v.Valid = false
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string) bool {
|
|
||||||
if !ok {
|
|
||||||
field.Errors = append(field.Errors, errors.New(message))
|
|
||||||
v.Valid = false
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
87
pkg/login.go
87
pkg/login.go
|
@ -2,8 +2,6 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"html/template"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
@ -19,56 +17,17 @@ const (
|
||||||
defaultRole = "guest"
|
defaultRole = "guest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginForm struct {
|
type LoginPage struct {
|
||||||
locale *Locale
|
LoginError bool
|
||||||
Errors []error
|
Email string
|
||||||
Email *InputField
|
Password string
|
||||||
Password *InputField
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLoginForm(locale *Locale) *loginForm {
|
type AppUser struct {
|
||||||
return &loginForm{
|
Email string
|
||||||
locale: locale,
|
LoggedIn bool
|
||||||
Email: &InputField{
|
Role string
|
||||||
Name: "email",
|
Language language.Tag
|
||||||
Label: pgettext("input", "Email", locale),
|
|
||||||
Type: "email",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autofocus="autofocus"`,
|
|
||||||
`autocomplete="username"`,
|
|
||||||
`autocapitalize="none"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Password: &InputField{
|
|
||||||
Name: "password",
|
|
||||||
Label: pgettext("input", "Password", locale),
|
|
||||||
Type: "password",
|
|
||||||
Required: true,
|
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="current-password"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *loginForm) Parse(r *http.Request) error {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
form.Email.FillValue(r)
|
|
||||||
form.Password.FillValue(r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (form *loginForm) Validate() bool {
|
|
||||||
validator := newFormValidator()
|
|
||||||
if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", form.locale)) {
|
|
||||||
validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale))
|
|
||||||
}
|
|
||||||
validator.CheckRequiredInput(form.Password, gettext("Password can not be empty.", form.locale))
|
|
||||||
return validator.AllOK()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoginHandler() http.Handler {
|
func LoginHandler() http.Handler {
|
||||||
|
@ -78,28 +37,25 @@ func LoginHandler() http.Handler {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
locale := getLocale(r)
|
r.ParseForm()
|
||||||
form := newLoginForm(locale)
|
page := LoginPage{
|
||||||
if r.Method == "POST" {
|
Email: r.FormValue("email"),
|
||||||
if err := form.Parse(r); err != nil {
|
Password: r.FormValue("password"),
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if form.Validate() {
|
if r.Method == "POST" {
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", form.Email, form.Password, remoteAddr(r))
|
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r))
|
||||||
if cookie != "" {
|
if cookie != "" {
|
||||||
setSessionCookie(w, 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)))
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
page.LoginError = true
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
mustRenderWebTemplate(w, r, "login.gohtml", page)
|
||||||
mustRenderWebTemplate(w, r, "login.gohtml", form)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,13 +91,6 @@ func createSessionCookie(value string, duration time.Duration) *http.Cookie {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppUser struct {
|
|
||||||
Email string
|
|
||||||
LoggedIn bool
|
|
||||||
Role string
|
|
||||||
Language language.Tag
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckLogin(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()
|
||||||
|
|
|
@ -2,8 +2,9 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"html/template"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LanguageOption struct {
|
type LanguageOption struct {
|
||||||
|
@ -12,60 +13,44 @@ type LanguageOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type profileForm struct {
|
type profileForm struct {
|
||||||
locale *Locale
|
|
||||||
Name *InputField
|
Name *InputField
|
||||||
Email *InputField
|
Email *InputField
|
||||||
Password *InputField
|
Password *InputField
|
||||||
PasswordConfirm *InputField
|
PasswordConfirm *InputField
|
||||||
Language *SelectField
|
Language *SelectField
|
||||||
|
Valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileForm {
|
func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileForm {
|
||||||
automaticOption := pgettext("language option", "Automatic", locale)
|
automaticOption := pgettext("language option", "Automatic", locale)
|
||||||
languages := MustGetOptions(ctx, conn, "select 'und', $1 union all select lang_tag, endonym from language where selectable", automaticOption)
|
languages := MustGetOptions(ctx, conn, "select 'und', $1 union all select lang_tag, endonym from language where selectable", automaticOption)
|
||||||
return &profileForm{
|
return &profileForm{
|
||||||
locale: locale,
|
|
||||||
Name: &InputField{
|
Name: &InputField{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Label: pgettext("input", "User name", locale),
|
Label: pgettext("input", "User name", locale),
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Required: true,
|
Required: true,
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="name"`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Email: &InputField{
|
Email: &InputField{
|
||||||
Name: "email",
|
Name: "email",
|
||||||
Label: pgettext("input", "Email", locale),
|
Label: pgettext("input", "Email", locale),
|
||||||
Type: "email",
|
Type: "email",
|
||||||
Required: true,
|
Required: true,
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="username"`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Password: &InputField{
|
Password: &InputField{
|
||||||
Name: "password",
|
Name: "password",
|
||||||
Label: pgettext("input", "Password", locale),
|
Label: pgettext("input", "Password", locale),
|
||||||
Type: "password",
|
Type: "password",
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="new-password"`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
PasswordConfirm: &InputField{
|
PasswordConfirm: &InputField{
|
||||||
Name: "password_confirm",
|
Name: "password_confirm",
|
||||||
Label: pgettext("input", "Password Confirmation", locale),
|
Label: pgettext("input", "Password Confirmation", locale),
|
||||||
Type: "password",
|
Type: "password",
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="new-password"`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Language: &SelectField{
|
Language: &SelectField{
|
||||||
Name: "language",
|
Name: "language",
|
||||||
Label: pgettext("input", "Language", locale),
|
Label: pgettext("input", "Language", locale),
|
||||||
Options: languages,
|
Options: languages,
|
||||||
Attributes: []template.HTMLAttr{
|
|
||||||
`autocomplete="language"`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,23 +59,41 @@ func (form *profileForm) Parse(r *http.Request) error {
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
form.Email.FillValue(r)
|
form.Email.Value = strings.TrimSpace(r.FormValue("email"))
|
||||||
form.Name.FillValue(r)
|
form.Name.Value = strings.TrimSpace(r.FormValue("name"))
|
||||||
form.Password.FillValue(r)
|
form.Password.Value = strings.TrimSpace(r.FormValue("password"))
|
||||||
form.PasswordConfirm.FillValue(r)
|
form.PasswordConfirm.Value = strings.TrimSpace(r.FormValue("password_confirm"))
|
||||||
form.Language.FillValue(r)
|
form.Language.Selected = r.FormValue("language")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (form *profileForm) Validate() bool {
|
func (form *profileForm) Validate(locale *Locale) bool {
|
||||||
validator := newFormValidator()
|
form.Valid = true
|
||||||
if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", form.locale)) {
|
if form.Email.IsEmpty() {
|
||||||
validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale))
|
form.AppendInputError(form.Email, errors.New(gettext("Email can not be empty.", locale)))
|
||||||
|
} else if !form.Email.HasValidEmail() {
|
||||||
|
form.AppendInputError(form.Email, errors.New(gettext("This value is not a valid email. It should be like name@domain.com.", locale)))
|
||||||
}
|
}
|
||||||
validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale))
|
if form.Name.IsEmpty() {
|
||||||
validator.CheckPasswordConfirmation(form.Password, form.PasswordConfirm, gettext("Confirmation does not match password.", form.locale))
|
form.AppendInputError(form.Name, errors.New(gettext("Name can not be empty.", locale)))
|
||||||
validator.CheckValidSelectOption(form.Language, gettext("Selected language is not valid.", form.locale))
|
}
|
||||||
return validator.AllOK()
|
if !form.PasswordConfirm.Equals(form.Password) {
|
||||||
|
form.AppendInputError(form.PasswordConfirm, errors.New(gettext("Confirmation does not match password.", locale)))
|
||||||
|
}
|
||||||
|
if !form.Language.HasValidOption() {
|
||||||
|
form.AppendSelectError(form.Language, errors.New(gettext("Selected language is not valid.", locale)))
|
||||||
|
}
|
||||||
|
return form.Valid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *profileForm) AppendInputError(field *InputField, err error) {
|
||||||
|
field.Errors = append(field.Errors, err)
|
||||||
|
form.Valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *profileForm) AppendSelectError(field *SelectField, err error) {
|
||||||
|
field.Errors = append(field.Errors, err)
|
||||||
|
form.Valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProfileHandler() http.Handler {
|
func ProfileHandler() http.Handler {
|
||||||
|
@ -104,12 +107,12 @@ func ProfileHandler() http.Handler {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ok := form.Validate(); ok {
|
if ok := form.Validate(locale); ok {
|
||||||
//goland:noinspection SqlWithoutWhere
|
//goland:noinspection SqlWithoutWhere
|
||||||
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)
|
cookie := conn.MustGetText(r.Context(), "", "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", form.Name.Value, form.Email.Value, form.Language.Selected)
|
||||||
setSessionCookie(w, cookie)
|
setSessionCookie(w, cookie)
|
||||||
if form.Password.Val != "" {
|
if form.Password.Value != "" {
|
||||||
conn.MustExec(r.Context(), "select change_password($1)", form.Password)
|
conn.MustExec(r.Context(), "select change_password($1)", form.Password.Value)
|
||||||
}
|
}
|
||||||
company := getCompany(r)
|
company := getCompany(r)
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/profile", http.StatusSeeOther)
|
http.Redirect(w, r, "/company/"+company.Slug+"/profile", http.StatusSeeOther)
|
||||||
|
@ -117,8 +120,8 @@ func ProfileHandler() http.Handler {
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
} else {
|
} else {
|
||||||
form.Name.Val = conn.MustGetText(r.Context(), "", "select name from user_profile")
|
form.Name.Value = conn.MustGetText(r.Context(), "", "select name from user_profile")
|
||||||
form.Email.Val = user.Email
|
form.Email.Value = user.Email
|
||||||
form.Language.Selected = user.Language.String()
|
form.Language.Selected = user.Language.String()
|
||||||
}
|
}
|
||||||
mustRenderAppTemplate(w, r, "profile.gohtml", form)
|
mustRenderAppTemplate(w, r, "profile.gohtml", form)
|
||||||
|
|
|
@ -26,14 +26,6 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
|
||||||
}
|
}
|
||||||
return "/company/" + company.Slug + uri
|
return "/company/" + company.Slug + uri
|
||||||
},
|
},
|
||||||
"addInputAttr": func(attr string, field *InputField) *InputField {
|
|
||||||
field.Attributes = append(field.Attributes, template.HTMLAttr(attr))
|
|
||||||
return field
|
|
||||||
},
|
|
||||||
"addSelectAttr": func(attr string, field *SelectField) *SelectField {
|
|
||||||
field.Attributes = append(field.Attributes, template.HTMLAttr(attr))
|
|
||||||
return field
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
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)
|
||||||
|
|
91
po/ca.po
91
po/ca.po
|
@ -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-01 11:26+0100\n"
|
"POT-Creation-Date: 2023-01-31 15:17+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"
|
||||||
|
@ -44,15 +44,30 @@ msgstr "Tauler"
|
||||||
|
|
||||||
#: web/template/app.gohtml:43
|
#: web/template/app.gohtml:43
|
||||||
msgctxt "nav"
|
msgctxt "nav"
|
||||||
msgid "Contacts"
|
msgid "Customers"
|
||||||
msgstr "Contactes"
|
msgstr "Clients"
|
||||||
|
|
||||||
#: web/template/login.gohtml:2 web/template/login.gohtml:15
|
#: web/template/login.gohtml:2 web/template/login.gohtml:13
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrada"
|
msgstr "Entrada"
|
||||||
|
|
||||||
#: web/template/login.gohtml:19
|
#: web/template/login.gohtml:9
|
||||||
|
msgid "Invalid user or password"
|
||||||
|
msgstr "Nom d’usuari o contrasenya incorrectes"
|
||||||
|
|
||||||
|
#: web/template/login.gohtml:17 web/template/tax-details.gohtml:27
|
||||||
|
#: web/template/contacts-new.gohtml:27 pkg/profile.go:37
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Correu-e"
|
||||||
|
|
||||||
|
#: web/template/login.gohtml:22 pkg/profile.go:43
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Contrasenya"
|
||||||
|
|
||||||
|
#: web/template/login.gohtml:25
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entra"
|
msgstr "Entra"
|
||||||
|
@ -84,8 +99,8 @@ msgstr "Desa canvis"
|
||||||
|
|
||||||
#: web/template/contacts-index.gohtml:2
|
#: web/template/contacts-index.gohtml:2
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Contacts"
|
msgid "Customers"
|
||||||
msgstr "Contactes"
|
msgstr "Clients"
|
||||||
|
|
||||||
#: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
|
#: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
|
@ -113,8 +128,8 @@ msgid "Phone"
|
||||||
msgstr "Telèfon"
|
msgstr "Telèfon"
|
||||||
|
|
||||||
#: web/template/contacts-index.gohtml:29
|
#: web/template/contacts-index.gohtml:29
|
||||||
msgid "No contacts added yet."
|
msgid "No customers added yet."
|
||||||
msgstr "No hi ha cap contacte."
|
msgstr "No hi ha cap client."
|
||||||
|
|
||||||
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
|
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
|
@ -141,12 +156,6 @@ msgctxt "input"
|
||||||
msgid "Phone"
|
msgid "Phone"
|
||||||
msgstr "Telèfon"
|
msgstr "Telèfon"
|
||||||
|
|
||||||
#: 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
|
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Web"
|
msgid "Web"
|
||||||
|
@ -221,66 +230,42 @@ msgctxt "title"
|
||||||
msgid "New Contact"
|
msgid "New Contact"
|
||||||
msgstr "Nou contacte"
|
msgstr "Nou contacte"
|
||||||
|
|
||||||
#: pkg/login.go:44 pkg/profile.go:41
|
#: pkg/profile.go:26
|
||||||
msgctxt "input"
|
|
||||||
msgid "Password"
|
|
||||||
msgstr "Contrasenya"
|
|
||||||
|
|
||||||
#: pkg/login.go:66 pkg/profile.go:71
|
|
||||||
msgid "Email can not be empty."
|
|
||||||
msgstr "No podeu deixar el correu-e en blanc."
|
|
||||||
|
|
||||||
#: pkg/login.go:67 pkg/profile.go:72
|
|
||||||
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: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 d’usuari o contrasenya incorrectes."
|
|
||||||
|
|
||||||
#: pkg/profile.go:23
|
|
||||||
msgctxt "language option"
|
msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automàtic"
|
msgstr "Automàtic"
|
||||||
|
|
||||||
#: pkg/profile.go:29
|
#: pkg/profile.go:31
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "User name"
|
msgid "User name"
|
||||||
msgstr "Nom d’usuari"
|
msgstr "Nom d’usuari"
|
||||||
|
|
||||||
#: pkg/profile.go:46
|
#: pkg/profile.go:48
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password Confirmation"
|
msgid "Password Confirmation"
|
||||||
msgstr "Confirmació contrasenya"
|
msgstr "Confirmació contrasenya"
|
||||||
|
|
||||||
#: pkg/profile.go:51
|
#: pkg/profile.go:53
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Idioma"
|
msgstr "Idioma"
|
||||||
|
|
||||||
#: pkg/profile.go:74
|
#: pkg/profile.go:74
|
||||||
|
msgid "Email can not be empty."
|
||||||
|
msgstr "No podeu deixar el correu-e en blanc."
|
||||||
|
|
||||||
|
#: pkg/profile.go:76
|
||||||
|
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/profile.go:79
|
||||||
msgid "Name can not be empty."
|
msgid "Name can not be empty."
|
||||||
msgstr "No podeu deixar el nom en blanc."
|
msgstr "No podeu deixar el nom en blanc."
|
||||||
|
|
||||||
#: pkg/profile.go:75
|
#: pkg/profile.go:82
|
||||||
msgid "Confirmation does not match password."
|
msgid "Confirmation does not match password."
|
||||||
msgstr "La confirmació no és igual a la contrasenya."
|
msgstr "La confirmació no és igual a la contrasenya."
|
||||||
|
|
||||||
#: pkg/profile.go:76
|
#: pkg/profile.go:85
|
||||||
msgid "Selected language is not valid."
|
msgid "Selected language is not valid."
|
||||||
msgstr "Heu seleccionat un idioma que no és vàlid."
|
msgstr "Heu seleccionat un idioma que no és vàlid."
|
||||||
|
|
||||||
#~ msgctxt "nav"
|
|
||||||
#~ msgid "Customers"
|
|
||||||
#~ msgstr "Clients"
|
|
||||||
|
|
||||||
#~ msgctxt "title"
|
|
||||||
#~ msgid "Customers"
|
|
||||||
#~ msgstr "Clients"
|
|
||||||
|
|
||||||
#~ msgid "No customers added yet."
|
|
||||||
#~ msgstr "No hi ha cap client."
|
|
||||||
|
|
91
po/es.po
91
po/es.po
|
@ -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-01 11:26+0100\n"
|
"POT-Creation-Date: 2023-01-31 15:17+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"
|
||||||
|
@ -44,15 +44,30 @@ msgstr "Panel"
|
||||||
|
|
||||||
#: web/template/app.gohtml:43
|
#: web/template/app.gohtml:43
|
||||||
msgctxt "nav"
|
msgctxt "nav"
|
||||||
msgid "Contacts"
|
msgid "Customers"
|
||||||
msgstr "Contactos"
|
msgstr "Clientes"
|
||||||
|
|
||||||
#: web/template/login.gohtml:2 web/template/login.gohtml:15
|
#: web/template/login.gohtml:2 web/template/login.gohtml:13
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrada"
|
msgstr "Entrada"
|
||||||
|
|
||||||
#: web/template/login.gohtml:19
|
#: web/template/login.gohtml:9
|
||||||
|
msgid "Invalid user or password"
|
||||||
|
msgstr "Nombre de usuario o contraseña inválido"
|
||||||
|
|
||||||
|
#: web/template/login.gohtml:17 web/template/tax-details.gohtml:27
|
||||||
|
#: web/template/contacts-new.gohtml:27 pkg/profile.go:37
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Correo-e"
|
||||||
|
|
||||||
|
#: web/template/login.gohtml:22 pkg/profile.go:43
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Contraseña"
|
||||||
|
|
||||||
|
#: web/template/login.gohtml:25
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrar"
|
msgstr "Entrar"
|
||||||
|
@ -84,8 +99,8 @@ msgstr "Guardar cambios"
|
||||||
|
|
||||||
#: web/template/contacts-index.gohtml:2
|
#: web/template/contacts-index.gohtml:2
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Contacts"
|
msgid "Customers"
|
||||||
msgstr "Contactos"
|
msgstr "Clientes"
|
||||||
|
|
||||||
#: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
|
#: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
|
@ -113,8 +128,8 @@ msgid "Phone"
|
||||||
msgstr "Teléfono"
|
msgstr "Teléfono"
|
||||||
|
|
||||||
#: web/template/contacts-index.gohtml:29
|
#: web/template/contacts-index.gohtml:29
|
||||||
msgid "No contacts added yet."
|
msgid "No customers added yet."
|
||||||
msgstr "No hay contactos."
|
msgstr "No hay clientes."
|
||||||
|
|
||||||
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
|
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
|
@ -141,12 +156,6 @@ msgctxt "input"
|
||||||
msgid "Phone"
|
msgid "Phone"
|
||||||
msgstr "Teléfono"
|
msgstr "Teléfono"
|
||||||
|
|
||||||
#: 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
|
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Web"
|
msgid "Web"
|
||||||
|
@ -221,66 +230,42 @@ msgctxt "title"
|
||||||
msgid "New Contact"
|
msgid "New Contact"
|
||||||
msgstr "Nuevo contacto"
|
msgstr "Nuevo contacto"
|
||||||
|
|
||||||
#: pkg/login.go:44 pkg/profile.go:41
|
#: pkg/profile.go:26
|
||||||
msgctxt "input"
|
|
||||||
msgid "Password"
|
|
||||||
msgstr "Contraseña"
|
|
||||||
|
|
||||||
#: pkg/login.go:66 pkg/profile.go:71
|
|
||||||
msgid "Email can not be empty."
|
|
||||||
msgstr "No podéis dejar el correo-e en blanco."
|
|
||||||
|
|
||||||
#: pkg/login.go:67 pkg/profile.go:72
|
|
||||||
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: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"
|
msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automático"
|
msgstr "Automático"
|
||||||
|
|
||||||
#: pkg/profile.go:29
|
#: pkg/profile.go:31
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "User name"
|
msgid "User name"
|
||||||
msgstr "Nombre de usuario"
|
msgstr "Nombre de usuario"
|
||||||
|
|
||||||
#: pkg/profile.go:46
|
#: pkg/profile.go:48
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password Confirmation"
|
msgid "Password Confirmation"
|
||||||
msgstr "Confirmación contrasenya"
|
msgstr "Confirmación contrasenya"
|
||||||
|
|
||||||
#: pkg/profile.go:51
|
#: pkg/profile.go:53
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Idioma"
|
msgstr "Idioma"
|
||||||
|
|
||||||
#: pkg/profile.go:74
|
#: pkg/profile.go:74
|
||||||
|
msgid "Email can not be empty."
|
||||||
|
msgstr "No podéis dejar el correo-e en blanco."
|
||||||
|
|
||||||
|
#: pkg/profile.go:76
|
||||||
|
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/profile.go:79
|
||||||
msgid "Name can not be empty."
|
msgid "Name can not be empty."
|
||||||
msgstr "No podéis dejar el nombre en blanco."
|
msgstr "No podéis dejar el nombre en blanco."
|
||||||
|
|
||||||
#: pkg/profile.go:75
|
#: pkg/profile.go:82
|
||||||
msgid "Confirmation does not match password."
|
msgid "Confirmation does not match password."
|
||||||
msgstr "La confirmación no corresponde con la contraseña."
|
msgstr "La confirmación no corresponde con la contraseña."
|
||||||
|
|
||||||
#: pkg/profile.go:76
|
#: pkg/profile.go:85
|
||||||
msgid "Selected language is not valid."
|
msgid "Selected language is not valid."
|
||||||
msgstr "Habéis escogido un idioma que no es válido."
|
msgstr "Habéis escogido un idioma que no es válido."
|
||||||
|
|
||||||
#~ msgctxt "nav"
|
|
||||||
#~ msgid "Customers"
|
|
||||||
#~ msgstr "Clientes"
|
|
||||||
|
|
||||||
#~ msgctxt "title"
|
|
||||||
#~ msgid "Customers"
|
|
||||||
#~ msgstr "Clientes"
|
|
||||||
|
|
||||||
#~ msgid "No customers added yet."
|
|
||||||
#~ msgstr "No hay clientes."
|
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<nav aria-label="{{( pgettext "Main" "title" )}}">
|
<nav aria-label="{{( pgettext "Main" "title" )}}">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
|
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
|
||||||
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
|
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Customers" "nav" )}}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{ define "title" -}}
|
{{ define "title" -}}
|
||||||
{{( pgettext "Contacts" "title" )}}
|
{{( pgettext "Customers" "title" )}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">{{( gettext "No contacts added yet." )}}</td>
|
<td colspan="4">{{( gettext "No customers added yet." )}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -1,27 +1,65 @@
|
||||||
{{ define "title" -}}
|
{{ define "title" -}}
|
||||||
{{( pgettext "New Contact" "title" )}}
|
{{( pgettext "New Contact" "title" )}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<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" }}">
|
||||||
{{ template "input-field" .BusinessName | addInputAttr "autofocus" }}
|
<div class="input">
|
||||||
{{ template "input-field" .VATIN }}
|
<input type="text" name="business_name" id="business_name" required="required" autofocus value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "input" )}}">
|
||||||
{{ template "input-field" .TradeName }}
|
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
|
||||||
{{ template "input-field" .Phone }}
|
</div>
|
||||||
{{ template "input-field" .Email }}
|
<div class="input">
|
||||||
{{ template "input-field" .Web }}
|
<input type="text" name="vatin" id="vatin" required="required" value="{{ .VATIN }}" placeholder="{{( pgettext "VAT number" "input" )}}">
|
||||||
{{ template "input-field" .Address | addInputAttr `class="width-2x"` }}
|
<label for="vatin">{{( pgettext "VAT number" "input" )}}</label>
|
||||||
{{ template "input-field" .City }}
|
</div>
|
||||||
{{ template "input-field" .Province }}
|
<div class="input">
|
||||||
{{ template "input-field" .PostalCode }}
|
<input type="text" name="trade_name" id="trade_name" value="{{ .TradeName }}" placeholder="{{( pgettext "Trade name" "input" )}}">
|
||||||
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
|
<label for="trade_name">{{( pgettext "Trade name" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="tel" name="phone" id="phone" required="required" value="{{ .Phone }}" placeholder="{{( pgettext "Phone" "input" )}}">
|
||||||
|
<label for="phone">{{( pgettext "Phone" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="email" name="email" id="email" required="required" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||||
|
<label for="email">{{( pgettext "Email" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="url" name="web" id="web" value="{{ .Web }}" placeholder="{{( pgettext "Web" "input" )}}">
|
||||||
|
<label for="web">{{( pgettext "Web" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="address" id="address" class="width-2x" required="required" value="{{ .Address }}" placeholder="{{( pgettext "Address" "input" )}}">
|
||||||
|
<label for="address">{{( pgettext "Address" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="city" id="city" required="required" value="{{ .City }}" placeholder="{{( pgettext "City" "input" )}}">
|
||||||
|
<label for="city">{{( pgettext "City" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="province" id="province" required="required" value="{{ .City }}" placeholder="{{( pgettext "Province" "input" )}}">
|
||||||
|
<label for="province">{{( pgettext "Province" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="postal_code" id="postal_code" required="required" value="{{ .PostalCode }}" placeholder="{{( pgettext "Postal code" "input" )}}">
|
||||||
|
<label for="postal_code">{{( pgettext "Postal code" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input">
|
||||||
|
<select id="country" name="country" class="width-fixed">
|
||||||
|
{{- range $country := .Countries }}
|
||||||
|
<option value="{{ .Code }}" {{ if eq .Code $.CountryCode }}selected="selected"{{ end }}>{{ .Name }}</option>
|
||||||
|
{{- end }}
|
||||||
|
</select>
|
||||||
|
<label for="country">{{( pgettext "Country" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<button type="submit">{{( pgettext "New contact" "action" )}}</button>
|
<button type="submit">{{( pgettext "New contact" "action" )}}</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -1,36 +1,33 @@
|
||||||
{{ define "input-field" -}}
|
{{ define "input-field" -}}
|
||||||
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
||||||
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
|
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
|
||||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
{{ if .Required }}required="required"{{ end }} value="{{ .Value }}" placeholder="{{ .Label }}">
|
||||||
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}">
|
|
||||||
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
||||||
{{- if .Errors }}
|
{{ if .Errors }}
|
||||||
<ul>
|
<ul>
|
||||||
{{- range $error := .Errors }}
|
{{- range $error := .Errors }}
|
||||||
<li>{{ . }}</li>
|
<li>{{ . }}</li>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{- end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ define "select-field" -}}
|
{{ define "select-field" -}}
|
||||||
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
||||||
<select id="{{ .Name }}-field" name="{{ .Name }}"
|
<select id="{{ .Name }}-field" name="{{ .Name }}">
|
||||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end -}}
|
|
||||||
>
|
|
||||||
{{- range $option := .Options }}
|
{{- range $option := .Options }}
|
||||||
<option value="{{ .Value }}"
|
<option value="{{ .Value }}"
|
||||||
{{- if eq .Value $.Selected }} selected="selected"{{ end }}>{{ .Label }}</option>
|
{{ if eq .Value $.Selected }}selected="selected"{{ end }}>{{ .Label }}</option>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</select>
|
</select>
|
||||||
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
||||||
{{- if .Errors }}
|
{{ if .Errors }}
|
||||||
<ul>
|
<ul>
|
||||||
{{- range $error := .Errors }}
|
{{- range $error := .Errors }}
|
||||||
<li>{{ . }}</li>
|
<li>{{ . }}</li>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{- end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -1,21 +1,27 @@
|
||||||
{{ define "title" -}}
|
{{ define "title" -}}
|
||||||
{{( pgettext "Login" "title" )}}
|
{{( pgettext "Login" "title" )}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h1><img src="/static/numerus.svg" alt="Numerus" width="620" height="77"></h1>
|
<h1><img src="/static/numerus.svg" alt="Numerus" width="620" height="77"></h1>
|
||||||
{{ if .Errors -}}
|
{{ if .LoginError -}}
|
||||||
<div class="error" role="alert">
|
<div class="error" role="alert">
|
||||||
{{ range $error := .Errors -}}
|
<p>{{( gettext "Invalid user or password" )}}</p>
|
||||||
<p>{{ $error }}</p>
|
|
||||||
{{- end }}
|
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
<section id="login">
|
<section id="login">
|
||||||
<h2>{{( pgettext "Login" "title" )}}</h2>
|
<h2>{{( pgettext "Login" "title" )}}</h2>
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
{{ template "input-field" .Email }}
|
<div class="input">
|
||||||
{{ template "input-field" .Password }}
|
<input id="user_email" type="email" required autofocus name="email" autocapitalize="none" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||||
|
<label for="user_email">{{( pgettext "Email" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input">
|
||||||
|
<input id="user_password" type="password" required name="password" autocomplete="current-password" value="{{ .Password }}" placeholder="{{( pgettext "Password" "input" )}}">
|
||||||
|
<label for="user_password">{{( pgettext "Password" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit">{{( pgettext "Login" "action" )}}</button>
|
<button type="submit">{{( pgettext "Login" "action" )}}</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,31 +1,71 @@
|
||||||
{{ define "title" -}}
|
{{ define "title" -}}
|
||||||
{{( pgettext "Tax Details" "title" )}}
|
{{( pgettext "Tax Details" "title" )}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<section class="dialog-content">
|
<section class="dialog-content">
|
||||||
<h2>{{(pgettext "Tax Details" "title")}}</h2>
|
<h2>{{(pgettext "Tax Details" "title")}}</h2>
|
||||||
{{ with .DetailsForm }}
|
|
||||||
<form id="details" method="POST">
|
<form id="details" method="POST">
|
||||||
{{ template "input-field" .BusinessName }}
|
<div class="input">
|
||||||
{{ template "input-field" .VATIN }}
|
<input type="text" name="business_name" id="business_name" required="required" value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "input" )}}">
|
||||||
{{ template "input-field" .TradeName }}
|
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
|
||||||
{{ template "input-field" .Phone }}
|
</div>
|
||||||
{{ template "input-field" .Email }}
|
<div class="input">
|
||||||
{{ template "input-field" .Web }}
|
<input type="text" name="vatin" id="vatin" required="required" value="{{ .VATIN }}" placeholder="{{( pgettext "VAT number" "input" )}}">
|
||||||
{{ template "input-field" .Address | addInputAttr `class="width-2x"` }}
|
<label for="vatin">{{( pgettext "VAT number" "input" )}}</label>
|
||||||
{{ template "input-field" .City }}
|
</div>
|
||||||
{{ template "input-field" .Province }}
|
<div class="input">
|
||||||
{{ template "input-field" .PostalCode }}
|
<input type="text" name="trade_name" id="trade_name" value="{{ .TradeName }}" placeholder="{{( pgettext "Trade name" "input" )}}">
|
||||||
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
|
<label for="trade_name">{{( pgettext "Trade name" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="tel" name="phone" id="phone" required="required" value="{{ .Phone }}" placeholder="{{( pgettext "Phone" "input" )}}">
|
||||||
|
<label for="phone">{{( pgettext "Phone" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="email" name="email" id="email" required="required" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||||
|
<label for="email">{{( pgettext "Email" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="url" name="web" id="web" value="{{ .Web }}" placeholder="{{( pgettext "Web" "input" )}}">
|
||||||
|
<label for="web">{{( pgettext "Web" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="address" id="address" class="width-2x" required="required" value="{{ .Address }}" placeholder="{{( pgettext "Address" "input" )}}">
|
||||||
|
<label for="address">{{( pgettext "Address" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="city" id="city" required="required" value="{{ .City }}" placeholder="{{( pgettext "City" "input" )}}">
|
||||||
|
<label for="city">{{( pgettext "City" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="province" id="province" required="required" value="{{ .City }}" placeholder="{{( pgettext "Province" "input" )}}">
|
||||||
|
<label for="province">{{( pgettext "Province" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="postal_code" id="postal_code" required="required" value="{{ .PostalCode }}" placeholder="{{( pgettext "Postal code" "input" )}}">
|
||||||
|
<label for="postal_code">{{( pgettext "Postal code" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input">
|
||||||
|
<select id="country" name="country" class="width-fixed">
|
||||||
|
{{- range $country := .Countries }}
|
||||||
|
<option value="{{ .Code }}" {{ if eq .Code $.CountryCode }}selected="selected"{{ end }}>{{ .Name }}</option>
|
||||||
|
{{- end }}
|
||||||
|
</select>
|
||||||
|
<label for="country">{{( pgettext "Country" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend id="currency-legend">{{( pgettext "Currency" "title" )}}</legend>
|
<legend id="currency-legend">{{( pgettext "Currency" "input" )}}</legend>
|
||||||
|
|
||||||
{{ template "select-field" .Currency }}
|
<select id="currency" name="currency" aria-labelledby="currency-legend">
|
||||||
|
{{- range $currency := .Currencies }}
|
||||||
|
<option value="{{ .Code }}" {{ if eq .Code $.CurrencyCode }}selected="selected"{{ end }}>{{ .Symbol }} ({{ .Code }})</option>
|
||||||
|
{{- end }}
|
||||||
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<form id="newtax" method="POST" action="{{ companyURI "/tax" }}">
|
<form id="newtax" method="POST" action="{{ companyURI "/tax" }}">
|
||||||
</form>
|
</form>
|
||||||
|
@ -50,8 +90,7 @@
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}">
|
<form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}">
|
||||||
<input type="hidden" name="_method" value="DELETE"/>
|
<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>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -66,10 +105,18 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{( pgettext "New Line" "title")}}</th>
|
<th scope="row">{{( pgettext "New Line" "title")}}</th>
|
||||||
<td>
|
<td>
|
||||||
{{ template "input-field" .NewTaxForm.Name | addInputAttr `form="newtax"` }}
|
<div class="input">
|
||||||
|
<input form="newtax" type="text" name="name" id="tax_name" required="required"
|
||||||
|
placeholder="{{( pgettext "Tax name" "input" )}}">
|
||||||
|
<label for="tax_name">{{( pgettext "Tax name" "input" )}}</label>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
{{ template "input-field" .NewTaxForm.Rate | addInputAttr `form="newtax"` }}
|
<div class="input">
|
||||||
|
<input form="newtax" type="number" name="rate" id="tax_rate" min="-99" max="99"
|
||||||
|
required="required" placeholder="{{( pgettext "Rate (%)" "input" )}}">
|
||||||
|
<label for="tax_rate">{{( pgettext "Rate (%)" "input" )}}</label>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
Loading…
Reference in New Issue