Compare commits
3 Commits
41ce5af2ed
...
4131602fa3
Author | SHA1 | Date |
---|---|---|
jordi fita mas | 4131602fa3 | |
jordi fita mas | 6b73acafe6 | |
jordi fita mas | 7e8ec539ff |
|
@ -41,15 +41,14 @@ values (1, 1, 'Retenció 15 %', -0.15)
|
|||
, (1, 2, 'IVA 4 %', 0.04)
|
||||
;
|
||||
|
||||
alter sequence tag_tag_id_seq restart;
|
||||
alter sequence contact_contact_id_seq restart;
|
||||
insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (1, 'Melcior', 'IR1', 'Rei Blanc', parse_packed_phone_number('0732621', 'IR'), 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR')
|
||||
, (1, 'Gaspar', 'IN2', 'Rei Ros', parse_packed_phone_number('111', 'IN'), 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN')
|
||||
, (1, 'Baltasar', 'YE3', 'Rei Negre', parse_packed_phone_number('1-111-111', 'YE'), 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE')
|
||||
, (1, 'Caganera', 'ES41414141L', '', parse_packed_phone_number('222 222 222', 'ES'), 'caganera@pesebre.cat', '', 'C/ De l’Hort, 4', 'Olot', 'Girona', '17800', 'ES')
|
||||
, (1, 'Bou', 'ES41414142C', '', parse_packed_phone_number('333 333 333', 'ES'), 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES')
|
||||
, (1, 'Rabadà', 'ES41414143K', '', parse_packed_phone_number('444 444 444', 'ES'), 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES')
|
||||
;
|
||||
select add_contact (1, 'Melcior', '1', 'Rei Blanc', '0732621', 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR', array['pesebre', 'mag']);
|
||||
select add_contact (1, 'Gaspar', '2', 'Rei Ros', '111', 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN', array['pesebre', 'mag']);
|
||||
select add_contact (1, 'Baltasar', '3', 'Rei Negre', '1-111-111', 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE', array['pesebre', 'mag']);
|
||||
select add_contact (1, 'Caganera', '41414141L', '', '222 222 222', 'caganera@pesebre.cat', '', 'C/ De l’Hort, 4', 'Olot', 'Girona', '17800', 'ES', array['pesebre', 'persona']);
|
||||
select add_contact (1, 'Bou', '41414142C', '', '333 333 333', 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES', array['pesebre', 'bestia']);
|
||||
select add_contact (1, 'Rabadà', '41414143K', '', '444 444 444', 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES', array['pesebre', 'persona']);
|
||||
|
||||
alter sequence product_product_id_seq restart;
|
||||
select add_product(1, 'Or', 'Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.', '55.92', array[2]);
|
||||
|
@ -60,7 +59,6 @@ select add_product(1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de
|
|||
select add_product(1, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', '25.00', array[3]);
|
||||
select add_product(1, 'Teia', 'Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.', '7.00', array[2]);
|
||||
|
||||
alter sequence tag_tag_id_seq restart;
|
||||
alter sequence invoice_invoice_id_seq restart;
|
||||
alter sequence invoice_product_invoice_product_id_seq restart;
|
||||
select add_invoice(1, '', (current_date - '28 days'::interval)::date, 6, 'Vol esmorzar!', 1, '{producte}','{"(1,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
-- Deploy numerus:add_contact to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: extension_vat
|
||||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: extension_uri
|
||||
-- requires: country_code
|
||||
-- requires: contact
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
cslug uuid;
|
||||
begin
|
||||
insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code)
|
||||
returning contact_id, slug
|
||||
into cid, cslug;
|
||||
|
||||
perform tag_contact(company_id, cid, tags);
|
||||
|
||||
return cslug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
|
||||
grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
|
||||
grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -10,8 +10,7 @@
|
|||
-- requires: invoice_product_tax
|
||||
-- requires: next_invoice_number
|
||||
-- requires: tag_name
|
||||
-- requires: tag
|
||||
-- requires: invoice_tag
|
||||
-- requires: tag_invoice
|
||||
|
||||
begin;
|
||||
|
||||
|
@ -64,19 +63,7 @@ begin
|
|||
join unnest(product.tax) as ptax(tax_id) using (tax_id);
|
||||
end loop;
|
||||
|
||||
if array_length(tags, 1) > 0 then
|
||||
insert into tag (company_id, name)
|
||||
select add_invoice.company, new_tag.name
|
||||
from unnest (tags) as new_tag(name)
|
||||
on conflict (company_id, name) do nothing
|
||||
;
|
||||
|
||||
insert into invoice_tag (invoice_id, tag_id)
|
||||
select iid, tag_id
|
||||
from tag
|
||||
join unnest (tags) as new_tag(name) on company_id = add_invoice.company and tag.name = new_tag.name
|
||||
;
|
||||
end if;
|
||||
perform tag_invoice(company, iid, tags);
|
||||
|
||||
return pslug;
|
||||
end;
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: extension_uri
|
||||
-- requires: currency_code
|
||||
-- requires: currency
|
||||
-- requires: country_code
|
||||
-- requires: country
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
-- Deploy numerus:contact_tag to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: tag
|
||||
-- requires: contact
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table contact_tag (
|
||||
contact_id integer not null references contact,
|
||||
tag_id integer not null references tag,
|
||||
primary key (contact_id, tag_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_tag to invoicer;
|
||||
grant select, insert, update, delete on table contact_tag to admin;
|
||||
|
||||
alter table contact_tag enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_tag
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_tag.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,55 @@
|
|||
-- Deploy numerus:edit_contact to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: email
|
||||
-- requires: extension_uri
|
||||
-- requires: country_code
|
||||
-- requires: tag_name
|
||||
-- requires: contact
|
||||
-- requires: tag_contact
|
||||
-- requires: extension_vat
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
company integer;
|
||||
begin
|
||||
update contact
|
||||
set business_name = edit_contact.business_name
|
||||
, vatin = (edit_contact.country_code || edit_contact.vatin)::vatin
|
||||
, trade_name = edit_contact.trade_name
|
||||
, phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code)
|
||||
, email = edit_contact.email
|
||||
, web = edit_contact.web
|
||||
, address = edit_contact.address
|
||||
, city = edit_contact.city
|
||||
, province = edit_contact.province
|
||||
, postal_code = edit_contact.postal_code
|
||||
, country_code = edit_contact.country_code
|
||||
where slug = contact_slug
|
||||
returning contact_id, company_id
|
||||
into cid, company
|
||||
;
|
||||
|
||||
if cid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
perform tag_contact(company, cid, tags);
|
||||
|
||||
return contact_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
|
||||
grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
|
||||
grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -8,8 +8,7 @@
|
|||
-- requires: invoice_product
|
||||
-- requires: invoice_product_tax
|
||||
-- requires: tag_name
|
||||
-- requires: tag
|
||||
-- requires: invoice_tag
|
||||
-- requires: tag_invoice
|
||||
|
||||
begin;
|
||||
|
||||
|
@ -90,21 +89,7 @@ begin
|
|||
delete from invoice_product where invoice_product_id = any(products_to_delete);
|
||||
end if;
|
||||
|
||||
delete from invoice_tag where invoice_id = iid;
|
||||
|
||||
if array_length(tags, 1) > 0 then
|
||||
insert into tag (company_id, name)
|
||||
select company, new_tag.name
|
||||
from unnest (tags) as new_tag(name)
|
||||
on conflict (company_id, name) do nothing
|
||||
;
|
||||
|
||||
insert into invoice_tag (invoice_id, tag_id)
|
||||
select iid, tag_id
|
||||
from tag
|
||||
join unnest (tags) as new_tag(name) on company_id = company and tag.name = new_tag.name
|
||||
;
|
||||
end if;
|
||||
perform tag_invoice(company, iid, tags);
|
||||
|
||||
return invoice_slug;
|
||||
end;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy numerus:tag_contact to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: tag_name
|
||||
-- requires: tag_relation
|
||||
-- requires: contact_tag
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function tag_contact (company_id integer, contact_id integer, tags tag_name[]) returns void as
|
||||
$$
|
||||
select tag_relation('contact_tag', 'contact_id', company_id, contact_id, tags);
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function tag_contact(integer, integer, tag_name[]) from public;
|
||||
grant execute on function tag_contact(integer, integer, tag_name[]) to invoicer;
|
||||
grant execute on function tag_contact(integer, integer, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,21 @@
|
|||
-- Deploy numerus:tag_invoice to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: tag_name
|
||||
-- requires: tag_invoice
|
||||
-- requires: invoice_tag
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function tag_invoice(company_id integer, invoice_id integer, tags tag_name[]) returns void as
|
||||
$$
|
||||
select tag_relation('invoice_tag', 'invoice_id', company_id, invoice_id, tags);
|
||||
$$
|
||||
language sql;
|
||||
|
||||
revoke execute on function tag_invoice(integer, integer, tag_name[]) from public;
|
||||
grant execute on function tag_invoice(integer, integer, tag_name[]) to invoicer;
|
||||
grant execute on function tag_invoice(integer, integer, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,32 @@
|
|||
-- Deploy numerus:tag_relation to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: tag
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function tag_relation(relname regclass, attname name, company integer, rowid integer, tags tag_name[]) returns void as
|
||||
$$
|
||||
begin
|
||||
execute format('delete from %I where %I = $1', relname, attname) USING rowid;
|
||||
|
||||
if array_length(tags, 1) > 0 then
|
||||
insert into tag (company_id, name)
|
||||
select company, new_tag.name
|
||||
from unnest (tags) as new_tag(name)
|
||||
on conflict (company_id, name) do nothing
|
||||
;
|
||||
|
||||
execute format('insert into %I (%I, tag_id) select $1, tag_id from tag join unnest ($2) as new_tag(name) on company_id = $3 and tag.name = new_tag.name', relname, attname) USING rowid, tags, company;
|
||||
end if;
|
||||
end
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function tag_relation(regclass, name, integer, integer, tag_name[]) from public;
|
||||
grant execute on function tag_relation(regclass, name, integer, integer, tag_name[]) to invoicer;
|
||||
grant execute on function tag_relation(regclass, name, integer, integer, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -12,6 +12,7 @@ type ContactEntry struct {
|
|||
Name string
|
||||
Email string
|
||||
Phone string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type ContactsIndexPage struct {
|
||||
|
@ -21,8 +22,9 @@ type ContactsIndexPage struct {
|
|||
func IndexContacts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
tag := r.URL.Query().Get("tag")
|
||||
page := &ContactsIndexPage{
|
||||
Contacts: mustGetContactEntries(r.Context(), conn, company),
|
||||
Contacts: mustCollectContactEntries(r.Context(), conn, company, tag),
|
||||
}
|
||||
mustRenderMainTemplate(w, r, "contacts/index.gohtml", page)
|
||||
}
|
||||
|
@ -37,7 +39,7 @@ func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
|
|||
mustRenderNewContactForm(w, r, form)
|
||||
return
|
||||
}
|
||||
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code from contact where slug = $1", slug).Scan(form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country)) {
|
||||
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -84,7 +86,7 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param
|
|||
return
|
||||
}
|
||||
company := mustGetCompany(r)
|
||||
conn.MustExec(r.Context(), "insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) values ($1, $2, ($12 || $3)::vatin, $4, parse_packed_phone_number($5, $12), $6, $7, $8, $9, $10, $11, $12)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country)
|
||||
conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags)
|
||||
if IsHTMxRequest(r) {
|
||||
w.Header().Set("HX-Trigger", "closeModal")
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
|
@ -110,7 +112,7 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout
|
|||
mustRenderEditContactForm(w, r, params[0].Value, form)
|
||||
return
|
||||
}
|
||||
slug := conn.MustGetText(r.Context(), "", "update contact 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 where slug = $12 returning slug", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, params[0].Value)
|
||||
slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", params[0].Value, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags)
|
||||
if slug == "" {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
@ -123,8 +125,23 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout
|
|||
}
|
||||
}
|
||||
|
||||
func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []*ContactEntry {
|
||||
rows, err := conn.Query(ctx, "select slug, business_name, email, phone from contact where company_id = $1 order by business_name", company.Id)
|
||||
func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company, tag string) []*ContactEntry {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select slug
|
||||
, business_name
|
||||
, email
|
||||
, phone
|
||||
, array_agg(coalesce(tag.name::text, ''))
|
||||
from contact
|
||||
left join contact_tag using (contact_id)
|
||||
left join tag using(tag_id)
|
||||
where contact.company_id = $1 and (($2 = '') or (tag.name = $2))
|
||||
group by slug
|
||||
, business_name
|
||||
, email
|
||||
, phone
|
||||
order by business_name
|
||||
`, company.Id, tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -133,7 +150,7 @@ func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []
|
|||
var entries []*ContactEntry
|
||||
for rows.Next() {
|
||||
entry := &ContactEntry{}
|
||||
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Email, &entry.Phone)
|
||||
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Email, &entry.Phone, &entry.Tags)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -159,6 +176,7 @@ type contactForm struct {
|
|||
Province *InputField
|
||||
PostalCode *InputField
|
||||
Country *SelectField
|
||||
Tags *TagsField
|
||||
}
|
||||
|
||||
func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactForm {
|
||||
|
@ -250,6 +268,10 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor
|
|||
`autocomplete="country"`,
|
||||
},
|
||||
},
|
||||
Tags: &TagsField{
|
||||
Name: "tags",
|
||||
Label: pgettext("input", "Tags", locale),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,6 +290,7 @@ func (form *contactForm) Parse(r *http.Request) error {
|
|||
form.Province.FillValue(r)
|
||||
form.PostalCode.FillValue(r)
|
||||
form.Country.FillValue(r)
|
||||
form.Tags.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -300,3 +323,47 @@ func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool {
|
|||
}
|
||||
return validator.AllOK()
|
||||
}
|
||||
|
||||
func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
|
||||
return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
|
||||
select business_name
|
||||
, substr(vatin::text, 3)
|
||||
, trade_name
|
||||
, phone
|
||||
, email
|
||||
, web
|
||||
, address
|
||||
, city
|
||||
, province
|
||||
, postal_code
|
||||
, country_code
|
||||
, string_agg(tag.name, ',')
|
||||
from contact
|
||||
left join contact_tag using (contact_id)
|
||||
left join tag using(tag_id)
|
||||
where slug = $1
|
||||
group by business_name
|
||||
, substr(vatin::text, 3)
|
||||
, trade_name
|
||||
, phone
|
||||
, email
|
||||
, web
|
||||
, address
|
||||
, city
|
||||
, province
|
||||
, postal_code
|
||||
, country_code
|
||||
`, slug).Scan(
|
||||
form.BusinessName,
|
||||
form.VATIN,
|
||||
form.TradeName,
|
||||
form.Phone,
|
||||
form.Email,
|
||||
form.Web,
|
||||
form.Address,
|
||||
form.City,
|
||||
form.Province,
|
||||
form.PostalCode,
|
||||
form.Country,
|
||||
form.Tags))
|
||||
}
|
||||
|
|
45
pkg/form.go
45
pkg/form.go
|
@ -16,6 +16,8 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
var tagsRegex = regexp.MustCompile("[^a-z0-9-]+")
|
||||
|
||||
type Attribute struct {
|
||||
Key, Val string
|
||||
}
|
||||
|
@ -209,6 +211,49 @@ func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*S
|
|||
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 TagsField struct {
|
||||
Name string
|
||||
Label string
|
||||
Tags []string
|
||||
Attributes []template.HTMLAttr
|
||||
Required bool
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func (field *TagsField) Value() (driver.Value, error) {
|
||||
if field.Tags == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
return field.Tags, nil
|
||||
}
|
||||
|
||||
func (field *TagsField) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if str, ok := value.(string); ok {
|
||||
if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
|
||||
for _, element := range array.Elements {
|
||||
field.Tags = append(field.Tags, element)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
field.Tags = append(field.Tags, fmt.Sprintf("%v", value))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (field *TagsField) FillValue(r *http.Request) {
|
||||
field.Tags = strings.Split(tagsRegex.ReplaceAllString(r.FormValue(field.Name), ","), ",")
|
||||
if len(field.Tags) == 1 && len(field.Tags[0]) == 0 {
|
||||
field.Tags = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (field *TagsField) String() string {
|
||||
return strings.Join(field.Tags, ",")
|
||||
}
|
||||
|
||||
type FormValidator struct {
|
||||
Valid bool
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -365,7 +364,7 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
|
|||
mustRenderNewInvoiceForm(w, r, form)
|
||||
return
|
||||
}
|
||||
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), NewInvoiceProductArray(form.Products))
|
||||
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, form.Tags, NewInvoiceProductArray(form.Products))
|
||||
http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
@ -438,7 +437,7 @@ type invoiceForm struct {
|
|||
Date *InputField
|
||||
Notes *InputField
|
||||
PaymentMethod *SelectField
|
||||
Tags *InputField
|
||||
Tags *TagsField
|
||||
Products []*invoiceProductForm
|
||||
}
|
||||
|
||||
|
@ -475,10 +474,9 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
Label: pgettext("input", "Notes", locale),
|
||||
Type: "textarea",
|
||||
},
|
||||
Tags: &InputField{
|
||||
Tags: &TagsField{
|
||||
Name: "tags",
|
||||
Label: pgettext("input", "Tags", locale),
|
||||
Type: "text",
|
||||
},
|
||||
PaymentMethod: &SelectField{
|
||||
Name: "payment_method",
|
||||
|
@ -607,15 +605,6 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel
|
|||
return MustGetGroupedOptions(ctx, conn, "select tax_id::text, tax.name, tax_class.name from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by tax_class.name, tax.name", company.Id)
|
||||
}
|
||||
|
||||
func (form *invoiceForm) SplitTags() []string {
|
||||
reg := regexp.MustCompile("[^a-z0-9-]+")
|
||||
tags := strings.Split(reg.ReplaceAllString(form.Tags.Val, ","), ",")
|
||||
if len(tags) == 1 && len(tags[0]) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type invoiceProductForm struct {
|
||||
locale *Locale
|
||||
company *Company
|
||||
|
@ -759,7 +748,7 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout
|
|||
mustRenderEditInvoiceForm(w, r, slug, form)
|
||||
return
|
||||
}
|
||||
slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceStatus, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), EditedInvoiceProductArray(form.Products))
|
||||
slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceStatus, form.Customer, form.Notes, form.PaymentMethod, form.Tags, EditedInvoiceProductArray(form.Products))
|
||||
if slug == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:add_contact from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:contact_tag from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists numerus.contact_tag;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:edit_contact from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:tag_contact from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.tag_contact(integer, integer, numerus.tag_name[]);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:tag_invoice from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.tag_invoice(integer, integer, numerus.tag_name[]);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:tag_relation from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.tag_relation(regclass, name, integer, integer, numerus.tag_name[]);
|
||||
|
||||
commit;
|
12
sqitch.plan
12
sqitch.plan
|
@ -41,7 +41,7 @@ company_default_payment_method [schema_numerus company payment_method] 2023-03-0
|
|||
tax_class [schema_numerus company] 2023-02-28T10:13:14Z jordi fita mas <jordi@tandem.blog> # Add the relation for tax classes
|
||||
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 tax_class] 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
|
||||
contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
|
||||
product [schema_numerus company tax] 2023-02-04T09:17:24Z jordi fita mas <jordi@tandem.blog> # Add relation for products
|
||||
parse_price [schema_public] 2023-02-05T11:04:54Z jordi fita mas <jordi@tandem.blog> # Add function to convert from price to cents
|
||||
to_price [schema_numerus] 2023-02-05T11:46:31Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices
|
||||
|
@ -61,11 +61,17 @@ next_invoice_number [schema_numerus invoice_number_counter] 2023-02-17T13:21:48Z
|
|||
tag_name [schema_numerus] 2023-03-10T11:06:11Z jordi fita mas <jordi@tandem.blog> # Add domain for tag names
|
||||
tag [schema_numerus tag_name] 2023-03-10T11:04:24Z jordi fita mas <jordi@tandem.blog> # Add relation for tags
|
||||
invoice_tag [schema_numerus tag invoice] 2023-03-10T11:37:43Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice tag
|
||||
add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number tag_name tag invoice_tag] 2023-02-16T21:12:46Z jordi fita mas <jordi@tandem.blog> # Add function to create new invoices
|
||||
tag_relation [schema_numerus tag tag_name] 2023-03-25T17:40:52Z jordi fita mas <jordi@tandem.blog> # Add function to tag “relations”
|
||||
tag_invoice [schema_numerus tag_name tag_relation invoice_tag] 2023-03-25T18:04:02Z jordi fita mas <jordi@tandem.blog> # Add function to tag invoices
|
||||
add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number tag_name tag_invoice] 2023-02-16T21:12:46Z jordi fita mas <jordi@tandem.blog> # Add function to create new invoices
|
||||
invoice_tax_amount [schema_numerus invoice_product invoice_product_tax] 2023-02-22T12:08:35Z jordi fita mas <jordi@tandem.blog> # Add view for invoice tax amount
|
||||
invoice_product_amount [schema_numerus invoice_product invoice_product_tax] 2023-03-01T11:18:05Z jordi fita mas <jordi@tandem.blog> # Add view for invoice product subtotal and total
|
||||
invoice_amount [schema_numerus invoice_product invoice_product_amount] 2023-02-22T12:58:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices
|
||||
new_invoice_amount [schema_numerus] 2023-02-23T12:08:25Z jordi fita mas <jordi@tandem.blog> # Add type to return when computing new invoice amounts
|
||||
compute_new_invoice_amount [schema_numerus company currency tax new_invoice_product new_invoice_amount] 2023-02-23T12:20:13Z jordi fita mas <jordi@tandem.blog> # Add function to compute the subtotal, taxes, and total amounts for a new invoice
|
||||
edited_invoice_product [schema_numerus discount_rate] 2023-03-11T19:22:24Z jordi fita mas <jordi@tandem.blog> # Add typo for passing products to edited invoices
|
||||
edit_invoice [schema_numerus invoice currency parse_price edited_invoice_product tax invoice_product invoice_product_tax tag_name tag invoice_tag] 2023-03-11T18:30:50Z jordi fita mas <jordi@tandem.blog> # Add function to edit invoices
|
||||
edit_invoice [schema_numerus invoice currency parse_price edited_invoice_product tax invoice_product invoice_product_tax tag_name tag_invoice] 2023-03-11T18:30:50Z jordi fita mas <jordi@tandem.blog> # Add function to edit invoices
|
||||
contact_tag [schema_numerus tag contact] 2023-03-24T22:20:51Z jordi fita mas <jordi@tandem.blog> # Add relation for contact tag
|
||||
tag_contact [schema_numerus tag_name tag_relation contact_tag] 2023-03-25T22:16:42Z jordi fita mas <jordi@tandem.blog> # Add function to tag contacts
|
||||
add_contact [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri country_code tag_name contact] 2023-03-25T22:32:37Z jordi fita mas <jordi@tandem.blog> # Add function to create new contacts
|
||||
edit_contact [schema_numerus email extension_uri country_code tag_name contact tag_contact extension_vat extension_pg_libphonenumber] 2023-03-25T23:20:27Z jordi fita mas <jordi@tandem.blog> # Add function to edit contacts
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
-- Test add_contact
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(16);
|
||||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate contact_tag cascade;
|
||||
truncate tag cascade;
|
||||
truncate contact cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (222, 2, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_contact(1, 'Contact 2.1', '40404040D', 'Trade Contact 2.1', '777-777-777', 'c@c', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '17486', 'ES', '{tag1,tag2}') $$,
|
||||
'Should be able to insert a contact for the first company with two tags'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_contact(1, 'Contact 2.2', '41414141L', 'Trade Contact 2.2', '888-888-888', 'd@d', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17487', 'ES', '{}') $$,
|
||||
'Should be able to insert a second contact for the first company with no tag'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_contact(2, 'Contact 4.1', '42424242Y', '', '999-999-999', 'e@e', '', 'Yet Another Fake St., 123', 'City 4.1', 'Province 4.1', '17488', 'ES', '{tag2}') $$,
|
||||
'Should be able to insert a contact for the second company with a tag'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_contact(1, 'Contact 2.3', '43434343Q', '', '000-000-000', 'f@f', '', 'The Last Fake St., 123', '', '', '', 'ES', '{tag2}') $$,
|
||||
'Should be able to insert another contact with a repeated tag'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, business_name, vatin::text, trade_name, phone::text, email::text, web::text, address, city, province, postal_code, country_code::text, created_at from contact $$,
|
||||
$$ values (1, 'Contact 2.1', 'ES40404040D', 'Trade Contact 2.1', '+34 777 77 77 77', 'c@c', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '17486', 'ES', CURRENT_TIMESTAMP)
|
||||
, (1, 'Contact 2.2', 'ES41414141L', 'Trade Contact 2.2', '+34 888 88 88 88', 'd@d', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17487', 'ES', CURRENT_TIMESTAMP)
|
||||
, (2, 'Contact 4.1', 'ES42424242Y', '', '+34 999 99 99 99', 'e@e', '', 'Yet Another Fake St., 123', 'City 4.1', 'Province 4.1', '17488', 'ES', CURRENT_TIMESTAMP)
|
||||
, (1, 'Contact 2.3', 'ES43434343Q', '', '+34 000000000', 'f@f', '', 'The Last Fake St., 123', '', '', '', 'ES', CURRENT_TIMESTAMP)
|
||||
$$,
|
||||
'Should have created all contacts'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, name from tag $$,
|
||||
$$ values (1, 'tag1')
|
||||
, (1, 'tag2')
|
||||
, (2, 'tag2')
|
||||
$$,
|
||||
'Should have added all new tags once'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select business_name, tag.name from contact_tag join contact using (contact_id) join tag using (tag_id) $$,
|
||||
$$ values ('Contact 2.1', 'tag1')
|
||||
, ('Contact 2.1', 'tag2')
|
||||
, ('Contact 4.1', 'tag2')
|
||||
, ('Contact 2.3', 'tag2')
|
||||
$$,
|
||||
'Should have assigned the tags to contacts'
|
||||
);
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,130 @@
|
|||
-- Test contact_tag
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(23);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_table('contact_tag');
|
||||
select has_pk('contact_tag' );
|
||||
select col_is_pk('contact_tag', array['contact_id', 'tag_id']);
|
||||
select table_privs_are('contact_tag', 'guest', array []::text[]);
|
||||
select table_privs_are('contact_tag', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||
select table_privs_are('contact_tag', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||
select table_privs_are('contact_tag', 'authenticator', array []::text[]);
|
||||
|
||||
select has_column('contact_tag', 'contact_id');
|
||||
select col_is_fk('contact_tag', 'contact_id');
|
||||
select fk_ok('contact_tag', 'contact_id', 'contact', 'contact_id');
|
||||
select col_type_is('contact_tag', 'contact_id', 'integer');
|
||||
select col_not_null('contact_tag', 'contact_id');
|
||||
select col_hasnt_default('contact_tag', 'contact_id');
|
||||
|
||||
select has_column('contact_tag', 'tag_id');
|
||||
select col_is_fk('contact_tag', 'tag_id');
|
||||
select fk_ok('contact_tag', 'tag_id', 'tag', 'tag_id');
|
||||
select col_type_is('contact_tag', 'tag_id', 'integer');
|
||||
select col_not_null('contact_tag', 'tag_id');
|
||||
select col_hasnt_default('contact_tag', 'tag_id');
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate contact_tag cascade;
|
||||
truncate contact cascade;
|
||||
truncate tag cascade;
|
||||
truncate contact cascade;
|
||||
truncate company_user cascade;
|
||||
truncate company cascade;
|
||||
truncate payment_method 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')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
|
||||
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (444, 4, 'cash', 'cash')
|
||||
, (222, 2, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into company_user (company_id, user_id)
|
||||
values (2, 1)
|
||||
, (4, 5)
|
||||
;
|
||||
|
||||
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (6, 2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
, (8, 4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
insert into tag (tag_id, company_id, name)
|
||||
values (14, 2, 'web')
|
||||
, (15, 2, 'design')
|
||||
, (16, 4, 'product')
|
||||
, (17, 4, 'development')
|
||||
, (18, 4, 'something-else')
|
||||
, (19, 4, 'design')
|
||||
;
|
||||
|
||||
insert into contact_tag (contact_id, tag_id)
|
||||
values (6, 14)
|
||||
, (6, 15)
|
||||
, (8, 18)
|
||||
;
|
||||
|
||||
prepare contact_tag_data as
|
||||
select contact_id, tag_id
|
||||
from contact_tag
|
||||
;
|
||||
|
||||
set role invoicer;
|
||||
select is_empty('contact_tag_data', 'Should show no data when cookie is not set yet');
|
||||
reset role;
|
||||
|
||||
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
|
||||
select bag_eq(
|
||||
'contact_tag_data',
|
||||
$$ values ( 6, 14 )
|
||||
, ( 6, 15 )
|
||||
$$,
|
||||
'Should only list contact tags of the companies where demo@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
|
||||
select bag_eq(
|
||||
'contact_tag_data',
|
||||
$$ values ( 8, 18 )
|
||||
$$,
|
||||
'Should only list contact tags of the companies where admin@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('not-a-cookie');
|
||||
select throws_ok(
|
||||
'contact_tag_data',
|
||||
'42501', 'permission denied for table contact_tag',
|
||||
'Should not allow select to guest users'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
-- Test edit_contact
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(14);
|
||||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate contact_tag cascade;
|
||||
truncate tag cascade;
|
||||
truncate contact cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (112, 1, 'bank', 'send money to my bank account')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tag (tag_id, company_id, name)
|
||||
values (10, 1, 'tag1')
|
||||
, (11, 1, 'tag2')
|
||||
;
|
||||
-- edit_contact uses the sequence and sometimes it would confict
|
||||
alter sequence tag_tag_id_seq restart with 15;
|
||||
|
||||
insert into contact (contact_id, company_id, slug, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (12, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
, (13, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
insert into contact_tag (contact_id, tag_id)
|
||||
values (12, 10)
|
||||
, (13, 11)
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_contact('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 2.1', '40404040D', 'Trade Contact 2.1', '999-999-999', 'c1@c1', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES', array['tag1']) $$,
|
||||
'Should be able to edit the first contact'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_contact('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2.2', '41414141L', 'Trade Contact 2.2', '111-111-111', 'd2@d2', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES', array['tag1', 'tag3']) $$,
|
||||
'Should be able to edit the second contact'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, business_name, vatin::text, trade_name, phone::text, email::text, web::text, address, city, province, postal_code, country_code::text, created_at from contact $$,
|
||||
$$ values (1, 'Contact 2.1', 'ES40404040D', 'Trade Contact 2.1', '+34 999 99 99 99', 'c1@c1', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES', CURRENT_TIMESTAMP)
|
||||
, (1, 'Contact 2.2', 'ES41414141L', 'Trade Contact 2.2', '+34 111111111', 'd2@d2', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES', CURRENT_TIMESTAMP)
|
||||
$$,
|
||||
'Should have updated all contacts'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, name from tag $$,
|
||||
$$ values (1, 'tag1')
|
||||
, (1, 'tag2')
|
||||
, (1, 'tag3')
|
||||
$$,
|
||||
'Should have added all new tags'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select business_name, tag.name from contact_tag join contact using (contact_id) join tag using (tag_id) $$,
|
||||
$$ values ('Contact 2.1', 'tag1')
|
||||
, ('Contact 2.2', 'tag1')
|
||||
, ('Contact 2.2', 'tag3')
|
||||
$$,
|
||||
'Should have assigned the tags to contacts'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,124 @@
|
|||
-- Test tag_contact
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(17);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_function('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'sql');
|
||||
select function_returns('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'void');
|
||||
select isnt_definer('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate contact_tag cascade;
|
||||
truncate tag cascade;
|
||||
truncate contact cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (112, 1, 'bank', 'send money to my bank account')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tag (tag_id, company_id, name)
|
||||
values (10, 1, 'tag1')
|
||||
, (11, 1, 'tag2')
|
||||
;
|
||||
-- tag_contact uses the sequence and sometimes it would confict
|
||||
alter sequence tag_tag_id_seq restart with 15;
|
||||
|
||||
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
, (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
insert into contact_tag (contact_id, tag_id)
|
||||
values (12, 10)
|
||||
, (13, 11)
|
||||
;
|
||||
|
||||
prepare current_tags as
|
||||
select contact_id, tag.name
|
||||
from contact
|
||||
join contact_tag using (contact_id)
|
||||
join tag using (tag_id);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_contact(1, 12, array['tag1']) $$,
|
||||
'Should be able to keep the same tags to the contact'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (12, 'tag1')
|
||||
, (13, 'tag2')
|
||||
$$,
|
||||
'Should not have changed any contact tag'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_contact(1, 12, array['tag1', 'tag2']) $$,
|
||||
'Should be able to add tag2 contact'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (12, 'tag1')
|
||||
, (12, 'tag2')
|
||||
, (13, 'tag2')
|
||||
$$,
|
||||
'Should have added tag2 to contact'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_contact(1, 13, array['tag3']) $$,
|
||||
'Should be able to replace all tags of an contact with a new one'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (12, 'tag1')
|
||||
, (12, 'tag2')
|
||||
, (13, 'tag3')
|
||||
$$,
|
||||
'Should have set tag3 to contact'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_contact(1, 12, array[]::tag_name[]) $$,
|
||||
'Should be able to remove all tags from an contact'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (13, 'tag3')
|
||||
$$,
|
||||
'Should have remove all tags from contact'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,130 @@
|
|||
-- Test tag_invoice
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(17);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_function('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]'], 'sql');
|
||||
select function_returns('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]'], 'void');
|
||||
select isnt_definer('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_invoice', array ['integer', 'integer', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate invoice_tag cascade;
|
||||
truncate tag cascade;
|
||||
truncate invoice cascade;
|
||||
truncate contact cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (112, 1, 'bank', 'send money to my bank account')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tag (tag_id, company_id, name)
|
||||
values (10, 1, 'tag1')
|
||||
, (11, 1, 'tag2')
|
||||
;
|
||||
-- tag_invoice uses the sequence and sometimes it would confict
|
||||
alter sequence tag_tag_id_seq restart with 15;
|
||||
|
||||
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
, (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
insert into invoice (invoice_id, company_id, slug, invoice_number, invoice_date, contact_id, payment_method_id, currency_code)
|
||||
values (15, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'INV1', '2023-03-10', 12, 111, 'EUR')
|
||||
, (16, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'INV2', '2023-03-09', 13, 111, 'EUR')
|
||||
;
|
||||
|
||||
insert into invoice_tag (invoice_id, tag_id)
|
||||
values (15, 10)
|
||||
, (16, 11)
|
||||
;
|
||||
|
||||
prepare current_tags as
|
||||
select invoice_id, tag.name
|
||||
from invoice
|
||||
join invoice_tag using (invoice_id)
|
||||
join tag using (tag_id);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_invoice(1, 15, array['tag1']) $$,
|
||||
'Should be able to keep the same tags to the invoice'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (15, 'tag1')
|
||||
, (16, 'tag2')
|
||||
$$,
|
||||
'Should not have changed any invoice tag'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_invoice(1, 15, array['tag1', 'tag2']) $$,
|
||||
'Should be able to add tag2 invoice'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (15, 'tag1')
|
||||
, (15, 'tag2')
|
||||
, (16, 'tag2')
|
||||
$$,
|
||||
'Should have added tag2 to invoice'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_invoice(1, 16, array['tag3']) $$,
|
||||
'Should be able to replace all tags of an invoice with a new one'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (15, 'tag1')
|
||||
, (15, 'tag2')
|
||||
, (16, 'tag3')
|
||||
$$,
|
||||
'Should have set tag3 to invoice'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_invoice(1, 15, array[]::tag_name[]) $$,
|
||||
'Should be able to remove all tags from an invoice'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (16, 'tag3')
|
||||
$$,
|
||||
'Should have remove all tags from invoice'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,25 @@
|
|||
-- Test tag_relation
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
select plan(9);
|
||||
|
||||
select has_function('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]'], 'void');
|
||||
select isnt_definer('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_relation', array ['regclass', 'name', 'integer', 'integer', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:add_contact on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,13 @@
|
|||
-- Verify numerus:contact_tag on pg
|
||||
|
||||
begin;
|
||||
|
||||
select contact_id
|
||||
, tag_id
|
||||
from numerus.contact_tag
|
||||
where false;
|
||||
|
||||
select 1 / count(*) from pg_class where oid = 'numerus.contact_tag'::regclass and relrowsecurity;
|
||||
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_tag'::regclass;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:edit_contact on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:tag_contact on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.tag_contact(integer, integer, numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:tag_invoice on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.tag_invoice(integer, integer, numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:tag_relation on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.tag_relation(regclass, name, integer, integer, numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -702,6 +702,60 @@ tr.htmx-swapping td {
|
|||
transition: opacity 1s ease-out;
|
||||
}
|
||||
|
||||
/* Snackbar */
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"] {
|
||||
cursor: pointer;
|
||||
background-color: var(--numerus--color--black);
|
||||
color: var(--numerus--color--white);
|
||||
padding: 2rem;
|
||||
min-width: 28.8rem;
|
||||
max-width: 56.8rem;
|
||||
border-radius: 2px;
|
||||
position: fixed;
|
||||
translate: -50%;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].enter, div[x-data="snackbar"] div[role="alert"].leave {
|
||||
transition: transform;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].enter {
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].leave {
|
||||
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);;
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].enter.start, div[x-data="snackbar"] div[role="alert"].leave.end {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].enter p {
|
||||
transition: opacity;
|
||||
transition-delay: 150ms;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].enter.start p {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].enter.end p {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div[x-data="snackbar"] div[role="alert"].enter.end, div[x-data="snackbar"] div[role="alert"].leave.start {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Remix Icon */
|
||||
|
||||
@font-face {
|
||||
|
|
|
@ -426,3 +426,48 @@ htmx.on('closeModal', () => {
|
|||
openDialog.close();
|
||||
openDialog.remove();
|
||||
});
|
||||
|
||||
htmx.on(document, 'alpine:init', () => {
|
||||
Alpine.data('snackbar', () => ({
|
||||
show: false, toast: "", toasts: [], timeoutId: null, init() {
|
||||
htmx.on('htmx:error', (error) => {
|
||||
this.showError(error.detail.errorInfo.error);
|
||||
});
|
||||
},
|
||||
showError(message) {
|
||||
this.toasts.push(message);
|
||||
this.popUp();
|
||||
},
|
||||
popUp() {
|
||||
if (this.toasts.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (this.show) {
|
||||
this.dismiss();
|
||||
return;
|
||||
}
|
||||
if (this.toast !== "") {
|
||||
// It will show after remove calls popUp again.
|
||||
return;
|
||||
}
|
||||
this.toast = this.toasts[0];
|
||||
this.show = true;
|
||||
this.timeoutId = setTimeout(this.dismiss.bind(this), 4000);
|
||||
},
|
||||
dismiss() {
|
||||
if (!this.show) {
|
||||
// already dismissed
|
||||
return;
|
||||
}
|
||||
this.show = false;
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = setTimeout(this.remove.bind(this), 350);
|
||||
},
|
||||
remove() {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.toasts.splice(0, 1);
|
||||
this.toast = "";
|
||||
this.popUp();
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
|
||||
<script src="/static/htmx@1.8.6.min.js"></script>
|
||||
<script type="module" src="/static/numerus.js"></script>
|
||||
<script defer src="/static/alpinejs@3.12.0.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
@ -53,4 +54,18 @@
|
|||
{{- template "content" . }}
|
||||
</main>
|
||||
</body>
|
||||
<div x-data="snackbar">
|
||||
<div x-show="show"
|
||||
@click="dismiss"
|
||||
x-cloak
|
||||
x-transition:enter="enter"
|
||||
x-transition:enter-start="start"
|
||||
x-transition:enter-end="end"
|
||||
x-transition:leave="leave"
|
||||
x-transition:leave-start="start"
|
||||
x-transition:leave-end="end"
|
||||
role="alert">
|
||||
<p x-text="toast"></p>
|
||||
</div>
|
||||
</div>
|
||||
</html>
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
{{ template "input-field" .Province }}
|
||||
{{ template "input-field" .PostalCode }}
|
||||
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
{{ end }}
|
||||
|
||||
<fieldset>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<th>{{( pgettext "Customer" "title" )}}</th>
|
||||
<th>{{( pgettext "Email" "title" )}}</th>
|
||||
<th>{{( pgettext "Phone" "title" )}}</th>
|
||||
<th>{{( pgettext "Tags" "title" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-hx-push-url="false" data-hx-swap="beforeend">
|
||||
|
@ -36,11 +37,17 @@
|
|||
<td><a href="{{ companyURI "/contacts/"}}{{ .Slug }}" data-hx-boost="true">{{ .Name }}</a></td>
|
||||
<td><a href="mailto:{{ .Email }}">{{ .Email }}</a></td>
|
||||
<td><a href="tel:{{ .Phone }}">{{ .Phone }}</a></td>
|
||||
<td>
|
||||
{{- range $index, $tag := .Tags }}
|
||||
{{- if gt $index 0 }}, {{ end -}}
|
||||
<a href="?tag={{ . }}">{{ . }}</a>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="4">{{( gettext "No contacts added yet." )}}</td>
|
||||
<td colspan="5">{{( gettext "No contacts added yet." )}}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
{{ template "input-field" .Province }}
|
||||
{{ template "input-field" .PostalCode }}
|
||||
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
|
||||
<fieldset>
|
||||
<button class="primary" type="submit">{{( pgettext "New contact" "action" )}}</button>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<div class="input {{ if .Errors }}has-errors{{ end }}" is="numerus-tags">
|
||||
<input type="text" name="{{ .Name }}" id="{{ .Name }}-field"
|
||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
||||
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}">
|
||||
{{ if .Required }}required="required"{{ end }} value="{{ .String }}" placeholder="{{ .Label }}">
|
||||
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
||||
{{- if .Errors }}
|
||||
<ul>
|
||||
|
|
Loading…
Reference in New Issue