It is inside the “user menu” only because this is where Numerus has the same option, although it makes less sense in this case, because Numerus is geared toward individual freelancers while Camper is for companies. But, since it is easy to change afterward, this will do for now. However, it should be only shown to admin users, because regular employees have no UPDATE privilege on the company relation. Thus, the need for a new template function to check if the user is admin. Part of #17.
284 lines
8.2 KiB
Go
284 lines
8.2 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package company
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
|
"dev.tandem.ws/tandem/camper/pkg/form"
|
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
|
)
|
|
|
|
type AdminHandler struct {
|
|
}
|
|
|
|
func NewAdminHandler() *AdminHandler {
|
|
return &AdminHandler{}
|
|
}
|
|
|
|
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var head string
|
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
|
|
|
switch head {
|
|
case "":
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
f := newTaxDetailsForm(r.Context(), conn, user.Locale)
|
|
if err := f.FillFromDatabase(r.Context(), company, conn); err != nil {
|
|
panic(err)
|
|
}
|
|
f.MustRender(w, r, user, company)
|
|
case http.MethodPut:
|
|
editTaxDetails(w, r, user, company, conn)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
}
|
|
|
|
type taxDetailsForm struct {
|
|
BusinessName *form.Input
|
|
VATIN *form.Input
|
|
TradeName *form.Input
|
|
Phone *form.Input
|
|
Email *form.Input
|
|
Web *form.Input
|
|
Address *form.Input
|
|
City *form.Input
|
|
Province *form.Input
|
|
PostalCode *form.Input
|
|
Country *form.Select
|
|
Currency *form.Select
|
|
DefaultLanguage *form.Select
|
|
InvoiceNumberFormat *form.Input
|
|
LegalDisclaimer *form.Input
|
|
}
|
|
|
|
func newTaxDetailsForm(ctx context.Context, conn *database.Conn, l *locale.Locale) *taxDetailsForm {
|
|
return &taxDetailsForm{
|
|
BusinessName: &form.Input{
|
|
Name: "business_name",
|
|
},
|
|
VATIN: &form.Input{
|
|
Name: "vatin",
|
|
},
|
|
TradeName: &form.Input{
|
|
Name: "trade_name",
|
|
},
|
|
Phone: &form.Input{
|
|
Name: "phone",
|
|
},
|
|
Email: &form.Input{
|
|
Name: "email",
|
|
},
|
|
Web: &form.Input{
|
|
Name: "web",
|
|
},
|
|
Address: &form.Input{
|
|
Name: "address",
|
|
},
|
|
City: &form.Input{
|
|
Name: "city",
|
|
},
|
|
Province: &form.Input{
|
|
Name: "province",
|
|
},
|
|
PostalCode: &form.Input{
|
|
Name: "postal_code",
|
|
},
|
|
Country: &form.Select{
|
|
Name: "country",
|
|
Options: form.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", l.Language),
|
|
},
|
|
Currency: &form.Select{
|
|
Name: "currency",
|
|
Options: form.MustGetOptions(ctx, conn, "select currency_code, currency_symbol from currency order by currency_code"),
|
|
},
|
|
DefaultLanguage: &form.Select{
|
|
Name: "default_language",
|
|
Options: form.MustGetOptions(ctx, conn, "select lang_tag, endonym from language where selectable"),
|
|
},
|
|
InvoiceNumberFormat: &form.Input{
|
|
Name: "invoice_number_format",
|
|
},
|
|
LegalDisclaimer: &form.Input{
|
|
Name: "legal_disclaimer",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (f *taxDetailsForm) FillFromDatabase(ctx context.Context, company *auth.Company, conn *database.Conn) error {
|
|
return conn.QueryRow(ctx, `
|
|
select business_name
|
|
, substr(vatin::text, 3)
|
|
, trade_name
|
|
, phone
|
|
, email
|
|
, web
|
|
, address
|
|
, city
|
|
, province
|
|
, postal_code
|
|
, array[country_code::text]
|
|
, array[currency_code::text]
|
|
, array[default_lang_tag]
|
|
, invoice_number_format
|
|
, legal_disclaimer
|
|
from company
|
|
where company.company_id = $1`, company.ID).Scan(
|
|
&f.BusinessName.Val,
|
|
&f.VATIN.Val,
|
|
&f.TradeName.Val,
|
|
&f.Phone.Val,
|
|
&f.Email.Val,
|
|
&f.Web.Val,
|
|
&f.Address.Val,
|
|
&f.City.Val,
|
|
&f.Province.Val,
|
|
&f.PostalCode.Val,
|
|
&f.Country.Selected,
|
|
&f.Currency.Selected,
|
|
&f.DefaultLanguage.Selected,
|
|
&f.InvoiceNumberFormat.Val,
|
|
&f.LegalDisclaimer.Val,
|
|
)
|
|
}
|
|
|
|
func (f *taxDetailsForm) Parse(r *http.Request) error {
|
|
if err := r.ParseForm(); err != nil {
|
|
return err
|
|
}
|
|
f.BusinessName.FillValue(r)
|
|
f.VATIN.FillValue(r)
|
|
f.TradeName.FillValue(r)
|
|
f.Phone.FillValue(r)
|
|
f.Email.FillValue(r)
|
|
f.Web.FillValue(r)
|
|
f.Address.FillValue(r)
|
|
f.City.FillValue(r)
|
|
f.Province.FillValue(r)
|
|
f.PostalCode.FillValue(r)
|
|
f.Country.FillValue(r)
|
|
f.Currency.FillValue(r)
|
|
f.DefaultLanguage.FillValue(r)
|
|
f.InvoiceNumberFormat.FillValue(r)
|
|
f.LegalDisclaimer.FillValue(r)
|
|
return nil
|
|
}
|
|
|
|
func (f *taxDetailsForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
|
|
v := form.NewValidator(l)
|
|
|
|
var country string
|
|
if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) {
|
|
country = f.Country.Selected[0]
|
|
}
|
|
|
|
if v.CheckRequired(f.BusinessName, l.GettextNoop("Business name can not be empty.")) {
|
|
v.CheckMinLength(f.BusinessName, 2, l.GettextNoop("Business name must have at least two letters."))
|
|
}
|
|
if v.CheckRequired(f.VATIN, l.GettextNoop("VAT number can not be empty.")) {
|
|
if _, err := v.CheckValidVATIN(ctx, conn, f.VATIN, country, l.GettextNoop("This VAT number is not valid.")); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
if v.CheckRequired(f.Phone, l.GettextNoop("Phone can not be empty.")) {
|
|
if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) {
|
|
v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
|
|
}
|
|
if f.Web.Val != "" {
|
|
v.CheckValidURL(f.Web, l.GettextNoop("This web address is not valid. It should be like https://domain.com/."))
|
|
}
|
|
v.CheckRequired(f.Address, l.GettextNoop("Address can not be empty."))
|
|
v.CheckRequired(f.City, l.GettextNoop("City can not be empty."))
|
|
v.CheckRequired(f.Province, l.GettextNoop("Province can not be empty."))
|
|
if v.CheckRequired(f.PostalCode, l.GettextNoop("Postal code can not be empty.")) {
|
|
if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postal code is not valid.")); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
v.CheckSelectedOptions(f.Currency, l.GettextNoop("Selected currency is not valid."))
|
|
v.CheckSelectedOptions(f.DefaultLanguage, l.GettextNoop("Selected language is not valid."))
|
|
v.CheckRequired(f.InvoiceNumberFormat, l.GettextNoop("Invoice number format can not be empty."))
|
|
|
|
return v.AllOK, nil
|
|
}
|
|
|
|
func (f *taxDetailsForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
|
template.MustRenderAdmin(w, r, user, company, "taxDetails.gohtml", f)
|
|
}
|
|
|
|
func editTaxDetails(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
|
f := newTaxDetailsForm(r.Context(), conn, user.Locale)
|
|
if err := f.Parse(r); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := user.VerifyCSRFToken(r); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
|
|
panic(err)
|
|
} else if !ok {
|
|
if !httplib.IsHTMxRequest(r) {
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
}
|
|
f.MustRender(w, r, user, company)
|
|
return
|
|
}
|
|
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
|
|
, default_lang_tag = $13
|
|
, invoice_number_format = $14
|
|
, legal_disclaimer = $15
|
|
where company_id = $16
|
|
`,
|
|
f.BusinessName,
|
|
f.VATIN,
|
|
f.TradeName,
|
|
f.Phone,
|
|
f.Email,
|
|
f.Web,
|
|
f.Address,
|
|
f.City,
|
|
f.Province,
|
|
f.PostalCode,
|
|
f.Country,
|
|
f.Currency,
|
|
f.DefaultLanguage,
|
|
f.InvoiceNumberFormat,
|
|
f.LegalDisclaimer,
|
|
company.ID)
|
|
httplib.Redirect(w, r, "/admin/company", http.StatusSeeOther)
|
|
}
|