Add the contact relation and a rough first form
This commit is contained in:
parent
9968b4296a
commit
5a199a3d8e
|
@ -0,0 +1,54 @@
|
||||||
|
-- Deploy numerus:contact to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
-- requires: company
|
||||||
|
-- requires: extension_vat
|
||||||
|
-- requires: email
|
||||||
|
-- requires: extension_pg_libphonenumber
|
||||||
|
-- requires: extension_uri
|
||||||
|
-- requires: currency_code
|
||||||
|
-- requires: currency
|
||||||
|
-- requires: country_code
|
||||||
|
-- requires: country
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
create table contact (
|
||||||
|
contact_id serial primary key,
|
||||||
|
company_id integer not null references company,
|
||||||
|
slug uuid not null unique default gen_random_uuid(),
|
||||||
|
business_name text not null,
|
||||||
|
vatin vatin not null,
|
||||||
|
trade_name text not null,
|
||||||
|
phone packed_phone_number not null,
|
||||||
|
email email not null,
|
||||||
|
web uri not null,
|
||||||
|
address text not null,
|
||||||
|
city text not null,
|
||||||
|
province text not null,
|
||||||
|
postal_code text not null,
|
||||||
|
country_code country_code not null references country,
|
||||||
|
created_at timestamptz not null default current_timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select, insert, update, delete on table contact to invoicer;
|
||||||
|
grant select, insert, update, delete on table contact to admin;
|
||||||
|
|
||||||
|
grant usage on sequence contact_contact_id_seq to invoicer;
|
||||||
|
grant usage on sequence contact_contact_id_seq to admin;
|
||||||
|
|
||||||
|
alter table contact enable row level security;
|
||||||
|
|
||||||
|
create policy company_policy
|
||||||
|
on contact
|
||||||
|
using (
|
||||||
|
exists(
|
||||||
|
select 1
|
||||||
|
from company_user
|
||||||
|
join user_profile using (user_id)
|
||||||
|
where company_user.company_id = contact.company_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,101 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContactEntry struct {
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
Phone string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactsIndexPage struct {
|
||||||
|
Title string
|
||||||
|
Contacts []*ContactEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContactsHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn := getConn(r)
|
||||||
|
company := getCompany(r)
|
||||||
|
if r.Method == "POST" {
|
||||||
|
r.ParseForm()
|
||||||
|
page := &NewContactPage{
|
||||||
|
BusinessName: r.FormValue("business_name"),
|
||||||
|
VATIN: r.FormValue("vatin"),
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
http.Redirect(w, r, "/company/"+company.Slug+"/contacts", http.StatusSeeOther)
|
||||||
|
} else {
|
||||||
|
locale := getLocale(r)
|
||||||
|
page := &ContactsIndexPage{
|
||||||
|
Title: pgettext("title", "Contacts", locale),
|
||||||
|
Contacts: mustGetContactEntries(r.Context(), conn, company),
|
||||||
|
}
|
||||||
|
mustRenderAppTemplate(w, r, "contacts-index.html", page)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []*ContactEntry {
|
||||||
|
rows, err := conn.Query(ctx, "select business_name, email, phone from contact where company_id = $1 order by business_name", company.Id)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var entries []*ContactEntry
|
||||||
|
for rows.Next() {
|
||||||
|
entry := &ContactEntry{}
|
||||||
|
err = rows.Scan(&entry.Name, &entry.Email, &entry.Phone)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewContactPage struct {
|
||||||
|
Title string
|
||||||
|
BusinessName string
|
||||||
|
VATIN string
|
||||||
|
TradeName string
|
||||||
|
Phone string
|
||||||
|
Email string
|
||||||
|
Web string
|
||||||
|
Address string
|
||||||
|
City string
|
||||||
|
Province string
|
||||||
|
PostalCode string
|
||||||
|
CountryCode string
|
||||||
|
Countries []CountryOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContactHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
locale := getLocale(r)
|
||||||
|
conn := getConn(r)
|
||||||
|
page := &NewContactPage{
|
||||||
|
Title: pgettext("title", "New Contact", locale),
|
||||||
|
CountryCode: "ES",
|
||||||
|
Countries: mustGetCountryOptions(r.Context(), conn, locale),
|
||||||
|
}
|
||||||
|
mustRenderAppTemplate(w, r, "contacts-new.html", page)
|
||||||
|
})
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ func NewRouter(db *Db) http.Handler {
|
||||||
companyRouter.Handle("/tax/", CompanyTaxHandler())
|
companyRouter.Handle("/tax/", CompanyTaxHandler())
|
||||||
companyRouter.Handle("/tax", CompanyTaxHandler())
|
companyRouter.Handle("/tax", CompanyTaxHandler())
|
||||||
companyRouter.Handle("/profile", ProfileHandler())
|
companyRouter.Handle("/profile", ProfileHandler())
|
||||||
|
companyRouter.Handle("/contacts/new", NewContactHandler())
|
||||||
|
companyRouter.Handle("/contacts", ContactsHandler())
|
||||||
companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Revert numerus:contact from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop policy if exists company_policy on numerus.contact;
|
||||||
|
drop table if exists numerus.contact;
|
||||||
|
|
||||||
|
commit;
|
|
@ -38,3 +38,4 @@ company [schema_numerus extension_vat email extension_pg_libphonenumber extensio
|
||||||
company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <jordi@tandem.blog> # Add the relation of companies and their users
|
company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <jordi@tandem.blog> # Add the relation of companies and their users
|
||||||
tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates
|
tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates
|
||||||
tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
|
tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
|
||||||
|
contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
-- Test contact
|
||||||
|
set client_min_messages to warning;
|
||||||
|
create extension if not exists pgtap;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select plan(84);
|
||||||
|
|
||||||
|
set search_path to numerus, auth, public;
|
||||||
|
|
||||||
|
select has_table('contact');
|
||||||
|
select has_pk('contact' );
|
||||||
|
select table_privs_are('contact', 'guest', array []::text[]);
|
||||||
|
select table_privs_are('contact', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||||
|
select table_privs_are('contact', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||||
|
select table_privs_are('contact', 'authenticator', array []::text[]);
|
||||||
|
|
||||||
|
select has_sequence('contact_contact_id_seq');
|
||||||
|
select sequence_privs_are('contact_contact_id_seq', 'guest', array[]::text[]);
|
||||||
|
select sequence_privs_are('contact_contact_id_seq', 'invoicer', array['USAGE']);
|
||||||
|
select sequence_privs_are('contact_contact_id_seq', 'admin', array['USAGE']);
|
||||||
|
select sequence_privs_are('contact_contact_id_seq', 'authenticator', array[]::text[]);
|
||||||
|
|
||||||
|
select has_column('contact', 'contact_id');
|
||||||
|
select col_is_pk('contact', 'contact_id');
|
||||||
|
select col_type_is('contact', 'contact_id', 'integer');
|
||||||
|
select col_not_null('contact', 'contact_id');
|
||||||
|
select col_has_default('contact', 'contact_id');
|
||||||
|
select col_default_is('contact', 'contact_id', 'nextval(''contact_contact_id_seq''::regclass)');
|
||||||
|
|
||||||
|
select has_column('contact', 'company_id');
|
||||||
|
select col_is_fk('contact', 'company_id');
|
||||||
|
select fk_ok('contact', 'company_id', 'company', 'company_id');
|
||||||
|
select col_type_is('contact', 'company_id', 'integer');
|
||||||
|
select col_not_null('contact', 'company_id');
|
||||||
|
select col_hasnt_default('contact', 'company_id');
|
||||||
|
|
||||||
|
select has_column('contact', 'slug');
|
||||||
|
select col_is_unique('contact', 'slug');
|
||||||
|
select col_type_is('contact', 'slug', 'uuid');
|
||||||
|
select col_not_null('contact', 'slug');
|
||||||
|
select col_has_default('contact', 'slug');
|
||||||
|
select col_default_is('contact', 'slug', 'gen_random_uuid()');
|
||||||
|
|
||||||
|
select has_column('contact', 'business_name');
|
||||||
|
select col_type_is('contact', 'business_name', 'text');
|
||||||
|
select col_not_null('contact', 'business_name');
|
||||||
|
select col_hasnt_default('contact', 'business_name');
|
||||||
|
|
||||||
|
select has_column('contact', 'vatin');
|
||||||
|
select col_type_is('contact', 'vatin', 'vatin');
|
||||||
|
select col_not_null('contact', 'vatin');
|
||||||
|
select col_hasnt_default('contact', 'vatin');
|
||||||
|
|
||||||
|
select has_column('contact', 'trade_name');
|
||||||
|
select col_type_is('contact', 'trade_name', 'text');
|
||||||
|
select col_not_null('contact', 'trade_name');
|
||||||
|
select col_hasnt_default('contact', 'trade_name');
|
||||||
|
|
||||||
|
select has_column('contact', 'phone');
|
||||||
|
select col_type_is('contact', 'phone', 'packed_phone_number');
|
||||||
|
select col_not_null('contact', 'phone');
|
||||||
|
select col_hasnt_default('contact', 'phone');
|
||||||
|
|
||||||
|
select has_column('contact', 'email');
|
||||||
|
select col_type_is('contact', 'email', 'email');
|
||||||
|
select col_not_null('contact', 'email');
|
||||||
|
select col_hasnt_default('contact', 'email');
|
||||||
|
|
||||||
|
select has_column('contact', 'web');
|
||||||
|
select col_type_is('contact', 'web', 'uri');
|
||||||
|
select col_not_null('contact', 'web');
|
||||||
|
select col_hasnt_default('contact', 'web');
|
||||||
|
|
||||||
|
select has_column('contact', 'address');
|
||||||
|
select col_type_is('contact', 'address', 'text');
|
||||||
|
select col_not_null('contact', 'address');
|
||||||
|
select col_hasnt_default('contact', 'address');
|
||||||
|
|
||||||
|
select has_column('contact', 'city');
|
||||||
|
select col_type_is('contact', 'city', 'text');
|
||||||
|
select col_not_null('contact', 'city');
|
||||||
|
select col_hasnt_default('contact', 'city');
|
||||||
|
|
||||||
|
select has_column('contact', 'province');
|
||||||
|
select col_type_is('contact', 'province', 'text');
|
||||||
|
select col_not_null('contact', 'province');
|
||||||
|
select col_hasnt_default('contact', 'province');
|
||||||
|
|
||||||
|
select has_column('contact', 'postal_code');
|
||||||
|
select col_type_is('contact', 'postal_code', 'text');
|
||||||
|
select col_not_null('contact', 'postal_code');
|
||||||
|
select col_hasnt_default('contact', 'postal_code');
|
||||||
|
|
||||||
|
select has_column('contact', 'country_code');
|
||||||
|
select col_type_is('contact', 'country_code', 'country_code');
|
||||||
|
select col_is_fk('contact', 'country_code');
|
||||||
|
select col_type_is('contact', 'country_code', 'country_code');
|
||||||
|
select col_not_null('contact', 'country_code');
|
||||||
|
select col_hasnt_default('contact', 'country_code');
|
||||||
|
|
||||||
|
select has_column('contact', 'created_at');
|
||||||
|
select col_type_is('contact', 'created_at', 'timestamp with time zone');
|
||||||
|
select col_not_null('contact', 'created_at');
|
||||||
|
select col_has_default('contact', 'created_at');
|
||||||
|
select col_default_is('contact', 'created_at', current_timestamp);
|
||||||
|
|
||||||
|
set client_min_messages to warning;
|
||||||
|
truncate contact cascade;
|
||||||
|
truncate company_user cascade;
|
||||||
|
truncate company cascade;
|
||||||
|
truncate auth."user" cascade;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
|
||||||
|
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
|
||||||
|
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
|
||||||
|
;
|
||||||
|
|
||||||
|
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code)
|
||||||
|
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR')
|
||||||
|
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD')
|
||||||
|
;
|
||||||
|
|
||||||
|
insert into company_user (company_id, user_id)
|
||||||
|
values (2, 1)
|
||||||
|
, (4, 5)
|
||||||
|
;
|
||||||
|
|
||||||
|
insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||||
|
values (2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||||
|
, (4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||||
|
;
|
||||||
|
|
||||||
|
prepare contact_data as
|
||||||
|
select company_id, business_name
|
||||||
|
from contact
|
||||||
|
order by company_id, business_name;
|
||||||
|
|
||||||
|
set role invoicer;
|
||||||
|
select is_empty('contact_data', 'Should show no data when cookie is not set yet');
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
|
||||||
|
select bag_eq(
|
||||||
|
'contact_data',
|
||||||
|
$$ values (2, 'Contact 1')
|
||||||
|
$$,
|
||||||
|
'Should only list contacts of the companies where demo@tandem.blog is user of'
|
||||||
|
);
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
|
||||||
|
select bag_eq(
|
||||||
|
'contact_data',
|
||||||
|
$$ values (4, 'Contact 2')
|
||||||
|
$$,
|
||||||
|
'Should only list contacts of the companies where admin@tandem.blog is user of'
|
||||||
|
);
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select set_cookie('not-a-cookie');
|
||||||
|
select throws_ok(
|
||||||
|
'contact_data',
|
||||||
|
'42501', 'permission denied for table contact',
|
||||||
|
'Should not allow select to guest users'
|
||||||
|
);
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select *
|
||||||
|
from finish();
|
||||||
|
|
||||||
|
rollback;
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- Verify numerus:contact on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select contact_id
|
||||||
|
, company_id
|
||||||
|
, slug
|
||||||
|
, business_name
|
||||||
|
, vatin
|
||||||
|
, trade_name
|
||||||
|
, phone
|
||||||
|
, email
|
||||||
|
, web
|
||||||
|
, address
|
||||||
|
, city
|
||||||
|
, province
|
||||||
|
, postal_code
|
||||||
|
, country_code
|
||||||
|
, created_at
|
||||||
|
from numerus.contact
|
||||||
|
where false;
|
||||||
|
|
||||||
|
select 1 / count(*) from pg_class where oid = 'numerus.contact'::regclass and relrowsecurity;
|
||||||
|
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact'::regclass;
|
||||||
|
|
||||||
|
rollback;
|
|
@ -244,9 +244,34 @@ header {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--numerus--header--background-color);
|
background-color: var(--numerus--header--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
header, nav {
|
||||||
padding: 0rem 3rem;
|
padding: 0rem 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
border-bottom: 1px solid var(--numerus--color--light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
min-height: 8rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
}
|
}
|
||||||
|
@ -374,7 +399,6 @@ fieldset {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: none;
|
border: none;
|
||||||
list-style: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#profilemenu summary::-webkit-details-marker {
|
#profilemenu summary::-webkit-details-marker {
|
||||||
|
@ -434,7 +458,12 @@ fieldset {
|
||||||
color: var(--numerus--color--dark-gray);
|
color: var(--numerus--color--dark-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
#profilemenu summary:hover, #profilemenu summary:focus, #profilemenu button:hover, #profilemenu a:hover {
|
#profilemenu summary:hover
|
||||||
|
, #profilemenu summary:focus
|
||||||
|
, #profilemenu button:hover
|
||||||
|
, #profilemenu a:hover
|
||||||
|
, nav a:hover
|
||||||
|
{
|
||||||
background-color: var(--numerus--color--light-gray);
|
background-color: var(--numerus--color--light-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,11 @@
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</header>
|
</header>
|
||||||
|
<nav aria-label="{{( pgettext "Main" "title" )}}">
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Customers" "nav" )}}</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
{{- template "content" . }}
|
{{- template "content" . }}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<a class="primary button" href="{{ companyURI "/contacts/new" }}">{{( pgettext "New contact" "action" )}}</a>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{( pgettext "All" "contact" )}}</th>
|
||||||
|
<th>{{( pgettext "Customer" "title" )}}</th>
|
||||||
|
<th>{{( pgettext "Email" "title" )}}</th>
|
||||||
|
<th>{{( pgettext "Phone" "title" )}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ with .Contacts }}
|
||||||
|
{{- range $tax := . }}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ .Name }}</td>
|
||||||
|
<td>{{ .Email }}</td>
|
||||||
|
<td>{{ .Phone }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ else }}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">{{( gettext "No customers added yet." )}}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,61 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="dialog-content">
|
||||||
|
<h2>{{(pgettext "New Contact" "title")}}</h2>
|
||||||
|
<form method="POST" action="{{ companyURI "/contacts" }}">
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="business_name" id="business_name" required="required" autofocus 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<button type="submit">{{( pgettext "New contact" "action" )}}</button>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{- end }}
|
Loading…
Reference in New Issue