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:
jordi fita mas 2023-02-01 14:15:02 +01:00
parent 4f13fa58dc
commit 2883438157
9 changed files with 464 additions and 262 deletions

View File

@ -3,6 +3,7 @@ package pkg
import (
"context"
"errors"
"html/template"
"net/http"
"net/url"
"strconv"
@ -82,54 +83,202 @@ type Tax struct {
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 {
BusinessName string
VATIN string
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
DetailsForm *taxDetailsForm
NewTaxForm *newTaxForm
Taxes []*Tax
}
func CompanyTaxDetailsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
locale := getLocale(r)
page := &TaxDetailsPage{}
company := mustGetCompany(r)
conn := getConn(r)
if r.Method == "POST" {
r.ParseForm()
page.BusinessName = r.FormValue("business_name")
page.CountryCode = r.FormValue("country")
page.VATIN = page.CountryCode + r.FormValue("vatin")
page.TradeName = r.FormValue("trade_name")
page.Phone = r.FormValue("phone")
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)
} else {
err := conn.QueryRow(r.Context(), "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code, 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 := &TaxDetailsPage{
DetailsForm: newTaxDetailsForm(r.Context(), conn, locale),
NewTaxForm: newNewTaxForm(locale),
}
company := mustGetCompany(r)
if r.Method == "POST" {
if err := page.DetailsForm.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if ok := page.DetailsForm.Validate(r.Context(), conn); ok {
form := page.DetailsForm
conn.MustExec(r.Context(), "update company set business_name = $1, vatin = ($11 || $2)::vatin, trade_name = $3, phone = parse_packed_phone_number($4, $11), email = $5, web = $6, address = $7, city = $8, province = $9, postal_code = $10, country_code = $11, currency_code = $12 where company_id = $13", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency, company.Id)
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
return
}
w.WriteHeader(http.StatusUnprocessableEntity)
} else {
page.DetailsForm.mustFillFromDatabase(r.Context(), conn, company)
}
page.Countries = mustGetCountryOptions(r.Context(), conn, locale)
page.Currencies = mustGetCurrencyOptions(r.Context(), conn)
page.Taxes = mustGetTaxes(r.Context(), conn, company)
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
})
@ -143,62 +292,16 @@ func mustGetCompany(r *http.Request) *Company {
return company
}
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(&currency.Code, &currency.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 {
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)
if err != nil {
panic(err)
}
defer rows.Close()
var taxes []Tax
var taxes []*Tax
for rows.Next() {
var tax Tax
tax := &Tax{}
err = rows.Scan(&tax.Id, &tax.Name, &tax.Rate)
if err != nil {
panic(err)
@ -212,6 +315,52 @@ func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []Tax {
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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
param := r.URL.Path
@ -222,12 +371,26 @@ func CompanyTaxHandler() http.Handler {
company := mustGetCompany(r)
if taxId, err := strconv.Atoi(param); err == nil {
conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId)
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
} else {
r.ParseForm()
name := r.FormValue("name")
rate, _ := strconv.Atoi(r.FormValue("rate"))
conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, name, rate)
locale := getLocale(r)
form := newNewTaxForm(locale)
if err := form.Parse(r); err != nil {
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)
})
}

View File

@ -80,7 +80,7 @@ type NewContactPage struct {
Province string
PostalCode string
CountryCode string
Countries []CountryOption
Countries []*SelectOption
}
func NewContactHandler() http.Handler {

View File

@ -2,9 +2,15 @@ package pkg
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"html/template"
"net/http"
"net/mail"
"net/url"
"regexp"
"strconv"
"strings"
)
@ -16,14 +22,32 @@ type InputField struct {
Name string
Label string
Type string
Value string
Val string
Required bool
Attributes []*Attribute
Attributes []template.HTMLAttr
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) {
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 {
@ -32,11 +56,25 @@ type SelectOption struct {
}
type SelectField struct {
Name string
Label string
Selected string
Options []*SelectOption
Errors []error
Name string
Label string
Selected string
Options []*SelectOption
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) {
@ -75,6 +113,10 @@ func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interfa
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
}
@ -88,22 +130,51 @@ func (v *FormValidator) AllOK() 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 {
_, err := mail.ParseAddress(field.Value)
_, 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.Value == confirm.Value, message)
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))

View File

@ -3,6 +3,7 @@ package pkg
import (
"context"
"errors"
"html/template"
"net"
"net/http"
"time"
@ -33,10 +34,10 @@ func newLoginForm(locale *Locale) *loginForm {
Label: pgettext("input", "Email", locale),
Type: "email",
Required: true,
Attributes: []*Attribute{
{"autofocus", "autofocus"},
{"autocomplete", "username"},
{"autocapitalize", "none"},
Attributes: []template.HTMLAttr{
`autofocus="autofocus"`,
`autocomplete="username"`,
`autocapitalize="none"`,
},
},
Password: &InputField{
@ -44,8 +45,8 @@ func newLoginForm(locale *Locale) *loginForm {
Label: pgettext("input", "Password", locale),
Type: "password",
Required: true,
Attributes: []*Attribute{
{"autocomplete", "current-password"},
Attributes: []template.HTMLAttr{
`autocomplete="current-password"`,
},
},
}
@ -86,7 +87,7 @@ func LoginHandler() http.Handler {
}
if form.Validate() {
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 != "" {
setSessionCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusSeeOther)

View File

@ -2,6 +2,7 @@ package pkg
import (
"context"
"html/template"
"net/http"
)
@ -29,8 +30,8 @@ func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileFor
Label: pgettext("input", "User name", locale),
Type: "text",
Required: true,
Attributes: []*Attribute{
{"autocomplete", "name"},
Attributes: []template.HTMLAttr{
`autocomplete="name"`,
},
},
Email: &InputField{
@ -38,30 +39,33 @@ func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileFor
Label: pgettext("input", "Email", locale),
Type: "email",
Required: true,
Attributes: []*Attribute{
{"autocomplete", "username"},
Attributes: []template.HTMLAttr{
`autocomplete="username"`,
},
},
Password: &InputField{
Name: "password",
Label: pgettext("input", "Password", locale),
Type: "password",
Attributes: []*Attribute{
{"autocomplete", "new-password"},
Attributes: []template.HTMLAttr{
`autocomplete="new-password"`,
},
},
PasswordConfirm: &InputField{
Name: "password_confirm",
Label: pgettext("input", "Password Confirmation", locale),
Type: "password",
Attributes: []*Attribute{
{"autocomplete", "new-password"},
Attributes: []template.HTMLAttr{
`autocomplete="new-password"`,
},
},
Language: &SelectField{
Name: "language",
Label: pgettext("input", "Language", locale),
Options: languages,
Attributes: []template.HTMLAttr{
`autocomplete="language"`,
},
},
}
}
@ -102,10 +106,10 @@ func ProfileHandler() http.Handler {
}
if ok := form.Validate(); ok {
//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)
if form.Password.Value != "" {
conn.MustExec(r.Context(), "select change_password($1)", form.Password.Value)
if form.Password.Val != "" {
conn.MustExec(r.Context(), "select change_password($1)", form.Password)
}
company := getCompany(r)
http.Redirect(w, r, "/company/"+company.Slug+"/profile", http.StatusSeeOther)
@ -113,8 +117,8 @@ func ProfileHandler() http.Handler {
}
w.WriteHeader(http.StatusUnprocessableEntity)
} else {
form.Name.Value = conn.MustGetText(r.Context(), "", "select name from user_profile")
form.Email.Value = user.Email
form.Name.Val = conn.MustGetText(r.Context(), "", "select name from user_profile")
form.Email.Val = user.Email
form.Language.Selected = user.Language.String()
}
mustRenderAppTemplate(w, r, "profile.gohtml", form)

View File

@ -26,6 +26,14 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
}
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 {
panic(err)

View File

@ -50,7 +50,7 @@
<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>
<option value="{{ .Value }}" {{ if eq .Value $.CountryCode }}selected="selected"{{ end }}>{{ .Label }}</option>
{{- end }}
</select>
<label for="country">{{( pgettext "Country" "input" )}}</label>

View File

@ -1,8 +1,8 @@
{{ define "input-field" -}}
<div class="input {{ if .Errors }}has-errors{{ end }}">
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
{{- range $attribute := .Attributes }} {{$attribute.Key}}="{{$attribute.Val}}" {{ end -}}
{{ if .Required }}required="required"{{ end }} value="{{ .Value }}" placeholder="{{ .Label }}">
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}">
<label for="{{ .Name }}-field">{{ .Label }}</label>
{{- if .Errors }}
<ul>
@ -16,19 +16,21 @@
{{ define "select-field" -}}
<div class="input {{ if .Errors }}has-errors{{ end }}">
<select id="{{ .Name }}-field" name="{{ .Name }}">
{{- range $option := .Options }}
<option value="{{ .Value }}"
{{ if eq .Value $.Selected }}selected="selected"{{ end }}>{{ .Label }}</option>
{{- end }}
<select id="{{ .Name }}-field" name="{{ .Name }}"
{{- range $attribute := .Attributes }} {{$attribute}} {{ end -}}
>
{{- range $option := .Options }}
<option value="{{ .Value }}"
{{- if eq .Value $.Selected }} selected="selected"{{ end }}>{{ .Label }}</option>
{{- end }}
</select>
<label for="{{ .Name }}-field">{{ .Label }}</label>
{{ if .Errors }}
{{- if .Errors }}
<ul>
{{- range $error := .Errors }}
<li>{{ . }}</li>
{{- end }}
</ul>
{{ end }}
{{- end }}
</div>
{{- end }}

View File

@ -1,136 +1,89 @@
{{ define "title" -}}
{{( pgettext "Tax Details" "title" )}}
{{( pgettext "Tax Details" "title" )}}
{{- end }}
{{ define "content" }}
<section class="dialog-content">
<h2>{{(pgettext "Tax Details" "title")}}</h2>
<form id="details" method="POST">
<div class="input">
<input type="text" name="business_name" id="business_name" required="required" value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "input" )}}">
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="vatin" id="vatin" required="required" value="{{ .VATIN }}" placeholder="{{( pgettext "VAT number" "input" )}}">
<label for="vatin">{{( pgettext "VAT number" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="trade_name" id="trade_name" value="{{ .TradeName }}" placeholder="{{( pgettext "Trade name" "input" )}}">
<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>
<section class="dialog-content">
<h2>{{(pgettext "Tax Details" "title")}}</h2>
{{ with .DetailsForm }}
<form id="details" method="POST">
{{ template "input-field" .BusinessName }}
{{ template "input-field" .VATIN }}
{{ template "input-field" .TradeName }}
{{ template "input-field" .Phone }}
{{ template "input-field" .Email }}
{{ template "input-field" .Web }}
{{ template "input-field" .Address | addInputAttr `class="width-2x"` }}
{{ template "input-field" .City }}
{{ template "input-field" .Province }}
{{ template "input-field" .PostalCode }}
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
<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>
<legend id="currency-legend">{{( pgettext "Currency" "title" )}}</legend>
<fieldset>
<legend id="currency-legend">{{( pgettext "Currency" "input" )}}</legend>
{{ template "select-field" .Currency }}
</fieldset>
</form>
{{ end }}
<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>
</form>
<form id="newtax" method="POST" action="{{ companyURI "/tax" }}">
</form>
<form id="newtax" method="POST" action="{{ companyURI "/tax" }}">
</form>
<fieldset>
<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>
<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>
<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>
<fieldset>
<button form="details" type="submit">{{( pgettext "Save changes" "action" )}}</button>
</fieldset>
</section>
{{- end }}