Handle tax details and new tax forms with structs and validation
I implemented the Valuer and Scanner interfaces to InputField and SelectField for better passing values between the database and Go. I had a conflict with the Value name and renamed the struct member to Val. I also had to change the attributes array to be of type template.HTMLAttr or html/template would replace `form="newtax"` attribute to `zgotmplz="newtax"` because it deems it “unsafe”. I do not like having to use template.HTMLAttr when assigning values, but i do not know what else i can do now.
This commit is contained in:
parent
4f13fa58dc
commit
2883438157
351
pkg/company.go
351
pkg/company.go
|
@ -3,6 +3,7 @@ package pkg
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -82,54 +83,202 @@ type Tax struct {
|
||||||
Rate int
|
Rate int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type taxDetailsForm struct {
|
||||||
|
locale *Locale
|
||||||
|
BusinessName *InputField
|
||||||
|
VATIN *InputField
|
||||||
|
TradeName *InputField
|
||||||
|
Phone *InputField
|
||||||
|
Email *InputField
|
||||||
|
Web *InputField
|
||||||
|
Address *InputField
|
||||||
|
City *InputField
|
||||||
|
Province *InputField
|
||||||
|
PostalCode *InputField
|
||||||
|
Country *SelectField
|
||||||
|
Currency *SelectField
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDetailsForm {
|
||||||
|
return &taxDetailsForm{
|
||||||
|
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"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 := 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)
|
||||||
|
form.Currency.FillValue(r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *taxDetailsForm) 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))
|
||||||
|
validator.CheckValidSelectOption(form.Currency, gettext("Selected currency is not valid.", form.locale))
|
||||||
|
return 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 {
|
||||||
BusinessName string
|
DetailsForm *taxDetailsForm
|
||||||
VATIN string
|
NewTaxForm *newTaxForm
|
||||||
TradeName string
|
Taxes []*Tax
|
||||||
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)
|
||||||
page := &TaxDetailsPage{}
|
|
||||||
company := mustGetCompany(r)
|
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
if r.Method == "POST" {
|
page := &TaxDetailsPage{
|
||||||
r.ParseForm()
|
DetailsForm: newTaxDetailsForm(r.Context(), conn, locale),
|
||||||
page.BusinessName = r.FormValue("business_name")
|
NewTaxForm: newNewTaxForm(locale),
|
||||||
page.CountryCode = r.FormValue("country")
|
}
|
||||||
page.VATIN = page.CountryCode + r.FormValue("vatin")
|
company := mustGetCompany(r)
|
||||||
page.TradeName = r.FormValue("trade_name")
|
if r.Method == "POST" {
|
||||||
page.Phone = r.FormValue("phone")
|
if err := page.DetailsForm.Parse(r); err != nil {
|
||||||
page.Email = r.FormValue("email")
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
page.Web = r.FormValue("web")
|
return
|
||||||
page.Address = r.FormValue("address")
|
}
|
||||||
page.City = r.FormValue("city")
|
if ok := page.DetailsForm.Validate(r.Context(), conn); ok {
|
||||||
page.Province = r.FormValue("province")
|
form := page.DetailsForm
|
||||||
page.PostalCode = r.FormValue("postal_code")
|
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.CurrencyCode = r.FormValue("currency")
|
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
||||||
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)
|
return
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
}
|
||||||
} else {
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
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)
|
} else {
|
||||||
if err != nil {
|
page.DetailsForm.mustFillFromDatabase(r.Context(), conn, company)
|
||||||
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)
|
||||||
})
|
})
|
||||||
|
@ -143,62 +292,16 @@ func mustGetCompany(r *http.Request) *Company {
|
||||||
return company
|
return company
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []CountryOption {
|
func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax {
|
||||||
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() {
|
||||||
var tax Tax
|
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)
|
||||||
|
@ -212,6 +315,52 @@ 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
|
||||||
|
@ -222,12 +371,26 @@ 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 {
|
||||||
r.ParseForm()
|
locale := getLocale(r)
|
||||||
name := r.FormValue("name")
|
form := newNewTaxForm(locale)
|
||||||
rate, _ := strconv.Atoi(r.FormValue("rate"))
|
if err := form.Parse(r); err != nil {
|
||||||
conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, name, rate)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
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)
|
||||||
|
} 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)
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ type NewContactPage struct {
|
||||||
Province string
|
Province string
|
||||||
PostalCode string
|
PostalCode string
|
||||||
CountryCode string
|
CountryCode string
|
||||||
Countries []CountryOption
|
Countries []*SelectOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContactHandler() http.Handler {
|
func NewContactHandler() http.Handler {
|
||||||
|
|
93
pkg/form.go
93
pkg/form.go
|
@ -2,9 +2,15 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql/driver"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,14 +22,32 @@ type InputField struct {
|
||||||
Name string
|
Name string
|
||||||
Label string
|
Label string
|
||||||
Type string
|
Type string
|
||||||
Value string
|
Val string
|
||||||
Required bool
|
Required bool
|
||||||
Attributes []*Attribute
|
Attributes []template.HTMLAttr
|
||||||
Errors []error
|
Errors []error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (field *InputField) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
field.Val = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
field.Val = fmt.Sprintf("%v", value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) Value() (driver.Value, error) {
|
||||||
|
return field.Val, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (field *InputField) FillValue(r *http.Request) {
|
func (field *InputField) FillValue(r *http.Request) {
|
||||||
field.Value = strings.TrimSpace(r.FormValue(field.Name))
|
field.Val = strings.TrimSpace(r.FormValue(field.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) Integer() int {
|
||||||
|
value, _ := strconv.Atoi(field.Val)
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectOption struct {
|
type SelectOption struct {
|
||||||
|
@ -32,11 +56,25 @@ type SelectOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectField struct {
|
type SelectField struct {
|
||||||
Name string
|
Name string
|
||||||
Label string
|
Label string
|
||||||
Selected string
|
Selected string
|
||||||
Options []*SelectOption
|
Options []*SelectOption
|
||||||
Errors []error
|
Attributes []template.HTMLAttr
|
||||||
|
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) {
|
func (field *SelectField) FillValue(r *http.Request) {
|
||||||
|
@ -75,6 +113,10 @@ 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 {
|
type FormValidator struct {
|
||||||
Valid bool
|
Valid bool
|
||||||
}
|
}
|
||||||
|
@ -88,22 +130,51 @@ func (v *FormValidator) AllOK() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FormValidator) CheckRequiredInput(field *InputField, message string) bool {
|
func (v *FormValidator) CheckRequiredInput(field *InputField, message string) bool {
|
||||||
return v.checkInput(field, field.Value != "", message)
|
return v.checkInput(field, field.Val != "", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
|
func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
|
||||||
_, err := mail.ParseAddress(field.Value)
|
_, err := mail.ParseAddress(field.Val)
|
||||||
return v.checkInput(field, err == nil, message)
|
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 {
|
func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
|
||||||
return v.checkInput(confirm, password.Value == confirm.Value, message)
|
return v.checkInput(confirm, password.Val == confirm.Val, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidSelectOption(field *SelectField, message string) bool {
|
func (v *FormValidator) CheckValidSelectOption(field *SelectField, message string) bool {
|
||||||
return v.checkSelect(field, field.HasValidOption(), message)
|
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 {
|
func (v *FormValidator) checkInput(field *InputField, ok bool, message string) bool {
|
||||||
if !ok {
|
if !ok {
|
||||||
field.Errors = append(field.Errors, errors.New(message))
|
field.Errors = append(field.Errors, errors.New(message))
|
||||||
|
|
15
pkg/login.go
15
pkg/login.go
|
@ -3,6 +3,7 @@ package pkg
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
@ -33,10 +34,10 @@ func newLoginForm(locale *Locale) *loginForm {
|
||||||
Label: pgettext("input", "Email", locale),
|
Label: pgettext("input", "Email", locale),
|
||||||
Type: "email",
|
Type: "email",
|
||||||
Required: true,
|
Required: true,
|
||||||
Attributes: []*Attribute{
|
Attributes: []template.HTMLAttr{
|
||||||
{"autofocus", "autofocus"},
|
`autofocus="autofocus"`,
|
||||||
{"autocomplete", "username"},
|
`autocomplete="username"`,
|
||||||
{"autocapitalize", "none"},
|
`autocapitalize="none"`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Password: &InputField{
|
Password: &InputField{
|
||||||
|
@ -44,8 +45,8 @@ func newLoginForm(locale *Locale) *loginForm {
|
||||||
Label: pgettext("input", "Password", locale),
|
Label: pgettext("input", "Password", locale),
|
||||||
Type: "password",
|
Type: "password",
|
||||||
Required: true,
|
Required: true,
|
||||||
Attributes: []*Attribute{
|
Attributes: []template.HTMLAttr{
|
||||||
{"autocomplete", "current-password"},
|
`autocomplete="current-password"`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -86,7 +87,7 @@ func LoginHandler() http.Handler {
|
||||||
}
|
}
|
||||||
if form.Validate() {
|
if form.Validate() {
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", form.Email.Value, form.Password.Value, remoteAddr(r))
|
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", form.Email, form.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)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,8 +30,8 @@ func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileFor
|
||||||
Label: pgettext("input", "User name", locale),
|
Label: pgettext("input", "User name", locale),
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Required: true,
|
Required: true,
|
||||||
Attributes: []*Attribute{
|
Attributes: []template.HTMLAttr{
|
||||||
{"autocomplete", "name"},
|
`autocomplete="name"`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Email: &InputField{
|
Email: &InputField{
|
||||||
|
@ -38,30 +39,33 @@ func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileFor
|
||||||
Label: pgettext("input", "Email", locale),
|
Label: pgettext("input", "Email", locale),
|
||||||
Type: "email",
|
Type: "email",
|
||||||
Required: true,
|
Required: true,
|
||||||
Attributes: []*Attribute{
|
Attributes: []template.HTMLAttr{
|
||||||
{"autocomplete", "username"},
|
`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: []*Attribute{
|
Attributes: []template.HTMLAttr{
|
||||||
{"autocomplete", "new-password"},
|
`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: []*Attribute{
|
Attributes: []template.HTMLAttr{
|
||||||
{"autocomplete", "new-password"},
|
`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"`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,10 +106,10 @@ func ProfileHandler() http.Handler {
|
||||||
}
|
}
|
||||||
if ok := form.Validate(); ok {
|
if ok := form.Validate(); 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.Value, form.Email.Value, form.Language.Selected)
|
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)
|
setSessionCookie(w, cookie)
|
||||||
if form.Password.Value != "" {
|
if form.Password.Val != "" {
|
||||||
conn.MustExec(r.Context(), "select change_password($1)", form.Password.Value)
|
conn.MustExec(r.Context(), "select change_password($1)", form.Password)
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
@ -113,8 +117,8 @@ func ProfileHandler() http.Handler {
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
} else {
|
} else {
|
||||||
form.Name.Value = conn.MustGetText(r.Context(), "", "select name from user_profile")
|
form.Name.Val = conn.MustGetText(r.Context(), "", "select name from user_profile")
|
||||||
form.Email.Value = user.Email
|
form.Email.Val = 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,6 +26,14 @@ 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)
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<select id="country" name="country" class="width-fixed">
|
<select id="country" name="country" class="width-fixed">
|
||||||
{{- range $country := .Countries }}
|
{{- range $country := .Countries }}
|
||||||
<option value="{{ .Code }}" {{ if eq .Code $.CountryCode }}selected="selected"{{ end }}>{{ .Name }}</option>
|
<option value="{{ .Value }}" {{ if eq .Value $.CountryCode }}selected="selected"{{ end }}>{{ .Label }}</option>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</select>
|
</select>
|
||||||
<label for="country">{{( pgettext "Country" "input" )}}</label>
|
<label for="country">{{( pgettext "Country" "input" )}}</label>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{{ 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.Key}}="{{$attribute.Val}}" {{ end -}}
|
{{- 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>
|
||||||
|
@ -16,19 +16,21 @@
|
||||||
|
|
||||||
{{ 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 $option := .Options }}
|
{{- range $attribute := .Attributes }} {{$attribute}} {{ end -}}
|
||||||
<option value="{{ .Value }}"
|
>
|
||||||
{{ if eq .Value $.Selected }}selected="selected"{{ end }}>{{ .Label }}</option>
|
{{- range $option := .Options }}
|
||||||
{{- end }}
|
<option value="{{ .Value }}"
|
||||||
|
{{- if eq .Value $.Selected }} selected="selected"{{ end }}>{{ .Label }}</option>
|
||||||
|
{{- 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,136 +1,89 @@
|
||||||
{{ 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>
|
||||||
<form id="details" method="POST">
|
{{ with .DetailsForm }}
|
||||||
<div class="input">
|
<form id="details" method="POST">
|
||||||
<input type="text" name="business_name" id="business_name" required="required" value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "input" )}}">
|
{{ template "input-field" .BusinessName }}
|
||||||
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
|
{{ template "input-field" .VATIN }}
|
||||||
</div>
|
{{ template "input-field" .TradeName }}
|
||||||
<div class="input">
|
{{ template "input-field" .Phone }}
|
||||||
<input type="text" name="vatin" id="vatin" required="required" value="{{ .VATIN }}" placeholder="{{( pgettext "VAT number" "input" )}}">
|
{{ template "input-field" .Email }}
|
||||||
<label for="vatin">{{( pgettext "VAT number" "input" )}}</label>
|
{{ template "input-field" .Web }}
|
||||||
</div>
|
{{ template "input-field" .Address | addInputAttr `class="width-2x"` }}
|
||||||
<div class="input">
|
{{ template "input-field" .City }}
|
||||||
<input type="text" name="trade_name" id="trade_name" value="{{ .TradeName }}" placeholder="{{( pgettext "Trade name" "input" )}}">
|
{{ template "input-field" .Province }}
|
||||||
<label for="trade_name">{{( pgettext "Trade name" "input" )}}</label>
|
{{ template "input-field" .PostalCode }}
|
||||||
</div>
|
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
|
||||||
<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">
|
<fieldset>
|
||||||
<select id="country" name="country" class="width-fixed">
|
<legend id="currency-legend">{{( pgettext "Currency" "title" )}}</legend>
|
||||||
{{- 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>
|
{{ template "select-field" .Currency }}
|
||||||
<legend id="currency-legend">{{( pgettext "Currency" "input" )}}</legend>
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<select id="currency" name="currency" aria-labelledby="currency-legend">
|
<form id="newtax" method="POST" action="{{ companyURI "/tax" }}">
|
||||||
{{- range $currency := .Currencies }}
|
</form>
|
||||||
<option value="{{ .Code }}" {{ if eq .Code $.CurrencyCode }}selected="selected"{{ end }}>{{ .Symbol }} ({{ .Code }})</option>
|
|
||||||
{{- end }}
|
|
||||||
</select>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form id="newtax" method="POST" action="{{ companyURI "/tax" }}">
|
<fieldset>
|
||||||
</form>
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50%"></th>
|
||||||
|
<th>{{( pgettext "Tax Name" "title" )}}</th>
|
||||||
|
<th>{{( pgettext "Rate (%)" "title" )}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ with .Taxes }}
|
||||||
|
{{- range $tax := . }}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ .Name }}</td>
|
||||||
|
<td>{{ .Rate }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}">
|
||||||
|
<input type="hidden" name="_method" value="DELETE"/>
|
||||||
|
<button class="icon" aria-label="{{( gettext "Delete tax" )}}" type="submit"><i
|
||||||
|
class="ri-delete-back-2-line"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ else }}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">{{( gettext "No taxes added yet." )}}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{( pgettext "New Line" "title")}}</th>
|
||||||
|
<td>
|
||||||
|
{{ template "input-field" .NewTaxForm.Name | addInputAttr `form="newtax"` }}
|
||||||
|
</td>
|
||||||
|
<td colspan="2">
|
||||||
|
{{ template "input-field" .NewTaxForm.Rate | addInputAttr `form="newtax"` }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"></td>
|
||||||
|
<td colspan="2">
|
||||||
|
<button form="newtax" type="submit">{{( pgettext "Add new tax" "action" )}}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<table>
|
<button form="details" type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
||||||
<thead>
|
</fieldset>
|
||||||
<tr>
|
</section>
|
||||||
<th width="50%"></th>
|
|
||||||
<th>{{( pgettext "Tax Name" "title" )}}</th>
|
|
||||||
<th>{{( pgettext "Rate (%)" "title" )}}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ with .Taxes }}
|
|
||||||
{{- range $tax := . }}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>{{ .Name }}</td>
|
|
||||||
<td>{{ .Rate }}</td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}">
|
|
||||||
<input type="hidden" name="_method" value="DELETE"/>
|
|
||||||
<button class="icon" aria-label="{{( gettext "Delete tax" )}}" type="submit"><i class="ri-delete-back-2-line"></i></button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{- end }}
|
|
||||||
{{ else }}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4">{{( gettext "No taxes added yet." )}}</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{( pgettext "New Line" "title")}}</th>
|
|
||||||
<td>
|
|
||||||
<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 colspan="2">
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"></td>
|
|
||||||
<td colspan="2">
|
|
||||||
<button form="newtax" type="submit">{{( pgettext "Add new tax" "action" )}}</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<button form="details" type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
Loading…
Reference in New Issue