Commit Graph

84 Commits

Author SHA1 Message Date
jordi fita mas 041017adc3 Add missing ARIA attributes and keyboard controls to multiselect
I use MDN’s documentation[0] as guid for both.

[0]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role
2023-03-18 07:17:28 +01:00
jordi fita mas 2dde25c862 Reimplement the multiselect as a custom element
What i really set off on was to refactor the multiselect’s x-data
context to a separate JavaScript file.

I did not see the need at first, thinking that it would not matter
because it was used only in a template and i was not duplicating the
code in my files.  However, i then realized that having the context
in the template means the visitor has to download it each and every time
it accesses a form with a multiselect, even if nothing changed, and,
worse, it would download it multiple times if there were many
multiselect controls.

It makes more sense to put all that into a file that the browser would
only download and parse once, if the proper caching is set.

Once i realized that, it was a shame that AlpineJS has no way to do
the same for the HTML structure[0], for the exact same reasons: not
wanting to download many times the same extra <template> and other
markup required to build the control for JavaScript users.  And then i
remembered that this is supposed to be custom element’s main selling
point.

At first i tried to create a shadow DOW to replace the <select> with
the same <div> and <ul> that i used with Alpine, but it turns out that
<select> is not one of the allowed elements that can have a shadow root
attached[0].

Therefore, i changed the custom element to extend the <div> for the
<select> and <label> instead—the same element that had the x-init
context—, but i would have to define or include all the styles inside
the shadow DOM, and bring the lang attribute, for it to look like it
did before.   Out with the shadow DOM, and modify the <div>’s contents
instead.

At this point the code was so far removed from the declarative way that
AlpineJS promotes that i did not see much value on using it, except for
its reactivity.   But, given that this is such a small component, at the
end decided to write it all in plain JavaScript.

It is more code, at least looking only at the code i had to write, but
i love how i only have to add an is="numerus-multiselect" attribute to
HTML for it to work.

[0]: https://github.com/alpinejs/alpine/discussions/1205
[1]: https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow
2023-03-17 14:55:12 +01:00
jordi fita mas 1c9fe14ab9 Improve multiselect’s usability with keyboard handling
Had to replace the tags <ul> with a div with an input, so that the
browser can focus the keywoard there.  For now i do not have a
focus-within CSS rule because we do no yet have a style for focus
highlight.

I have replaced the template for-loop to fill the options with the
JavaScript equivalent for two reasons.  The first is that GoLand is very
stupid and can not handle that templating code inside the JavaScript
function and complains of non-existing problemes all the time.

The second is that, taking advantage of the input, i now have filtering
of options and have to remove accents from the label and convert it to
lowercase into a separate property just for that.  I could do that with
a Go function, but it is something that i also have to do for the
input’s value when it changes, therefore i am forced to use JavaScript
and, if i am already using it for one string, it makes no sense to have
duplicate functionality in Go code.

The control still has missing aria attributes, and the list of options
is not yet navigable with the keyboard.
2023-03-16 12:52:44 +01:00
jordi fita mas f93d557aa9 Move the multiselect “component” to the select-field template
I had in the product edit page only because it was easier to test there
while i was developing it, but it is something that should be done for
all select[multiple], of course.

I removed the whole x-cloak thing because i am not sure what would
happen if i do something wrong and Alpine can not initialize the
multiselect; probably show nothing to the user.  Now it shows the
native select a fraction of a second, but if i fuck it up at least the
user can still use the app.
2023-03-15 11:44:18 +01:00
jordi fita mas 5702f0d198 Start “improving” the user interface with AlpineJS: tax selector
It is a shitty component, but i do not have more time today to do it
better.
2023-03-14 18:07:38 +01:00
jordi fita mas 8efae0485e Add the edit form for invoices
I had to change the way /invoices/new and /invoices/batch are handled,
because httprouter was not happy with the new POST /invoices/:slug/edit
route, claiming that /invoices/:slug conflicts with the previously
existing routes.

I also could not make it work with the PATCH method, even though i
correctly added the patchMethod override function, therefore editing
invoices is also weird because i have to take into account the “quick”
invoice status change.

I use the same form for both new and edit invoices, because the only
changes are that we can not edit the invoice date and number, by
Oriol’s design, but must be able to change the status; very similar
forms.
2023-03-13 15:00:35 +01:00
jordi fita mas 1ab48d2947 Add the String() method to InputField 2023-03-13 14:55:10 +01:00
jordi fita mas dab03e2b4f Change the color of the menu separator
I did not use the correct color according to the design.
2023-03-12 15:54:11 +01:00
jordi fita mas 2bc05e948c Add invoice tags
I followed the same restrictions as Gitea’s topics, arbitrarily, because
if it is enough for repositories it should be for invoices too,
apparently.
2023-03-10 14:02:55 +01:00
jordi fita mas 5dedaefc22 Add button to download many invoices as PDF in a ZIP archive 2023-03-09 12:11:53 +01:00
jordi fita mas f3b841473f Add the context menu with the duplicate option
As per the design document.
2023-03-08 11:54:06 +01:00
jordi fita mas 0c8edb9cae Add option to duplicate an invoice
With Oriol we agreed that a duplicate is just the new invoice form
prefilled with the data from another invoices, but without the number or
the date.
2023-03-08 11:26:02 +01:00
jordi fita mas 3dd0b0136e Adjust the design of invoice status menu to the one made by Oriol 2023-03-08 10:49:03 +01:00
jordi fita mas 039bf3abbd Add the “menu” to change invoice statuses 2023-03-07 11:52:09 +01:00
jordi fita mas f77f933e4a Add the payment method to invoices 2023-03-05 18:50:57 +01:00
jordi fita mas 31ef3ea47a Add company’s default payment method
I had to use a deferrable foreign key because the payment methods have
a reference to the company, and the company now a circular reference to
payment method.
2023-03-04 22:15:52 +01:00
jordi fita mas 9894925742 Add the payment method relation and corresponding form 2023-03-03 16:49:06 +01:00
jordi fita mas b84f1774f9 Replace static legal disclaimer with a database field 2023-03-02 10:24:44 +01:00
jordi fita mas d6034ad732 Add discount and tax classes columns to invoice
This was actually the (first) reason we added the tax classes: to show
them in columns on the invoice—without the class we would need a column
for each tax rate, even though they are the same tax.

The invoice design has the product total with taxes at the last column,
above the tax base, that i am not so sure about, but it seems that it
has not brought any problem whatsoever so far, so it remains as is.

Had to reduce the invoice’s font size to give more space to the table
or the columns would be right next to each other.  Oriol also told me
to add more vertical spacing to the table’s footer.
2023-03-01 14:08:12 +01:00
jordi fita mas 6a8ebab686 Add “bottom margin” to the invoice’s legal text
I did not see it in the original design.
2023-03-01 11:43:06 +01:00
jordi fita mas 79ea2f366a Add grouping for form’s select field
We will only allow to select a tax from each of the tax classes, but
the user needs to know what class each tax belongs to, and grouping
the taxes by class in the select helps with that.
2023-03-01 11:40:23 +01:00
jordi fita mas 11d51df7fa Introduce the concept of tax class
We want to show the percentage of the tax as columns in the invoice,
but until now it was not possible to have a single VAT column when
products have different VAT (e.g., 4 % and 10 %), because, as far
as the application is concerned, these where ”different taxes”.  We
also think it would be hard later on to compute the tax due to the
government.

So, tax classes is just a taxonomy to be able to have different names
and rates for the same type of tax, mostly VAT and retention in our
case.
2023-02-28 12:02:27 +01:00
jordi fita mas 0d4fb124b4 Keep all “new invoice actions” on the same /new URI 2023-02-27 13:13:28 +01:00
jordi fita mas 4d2379555e Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.

I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.

I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance.   This works, but i would need a Chrome running as a
daemon.

I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards.  Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.

To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option.  I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.

Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 17:26:09 +01:00
jordi fita mas 843f7746cf Adapt the invoice design to Oriol’s 2023-02-25 13:48:57 +01:00
jordi fita mas 419ac3ed46 Adjust invoice.css to work with WeasyPrint too
I am planning to use WeasyPrint to “generate PDF” from the same HTML
that the user view, but it seems that it does not support flex’s gap
and some other properties that i had to change to work in both user
agents.

I also moved the invoice’s “footer” inside the last product’s body
because i do not want the footer to be a “widow”.
2023-02-25 03:16:20 +01:00
jordi fita mas 18fba2964f Add invoice view, with print CSS
Had to group name and description rows in tbody because i do not want
to break them on pagination.

I also could not use tfoot for subtotal, taxes, and total because then
they appear on every page.

The disclaimer should appear only at the very bottom of the last page,
but i do not know how to do that; using position fixed shows it on
every page.
2023-02-24 12:22:15 +01:00
jordi fita mas 985f843e8e Show the invoice subtotal, taxes, and total when creating it 2023-02-23 15:31:57 +01:00
jordi fita mas 8dbf8ef2d0 Add currency_pattern to language relation
The design calls for rendering all amounts with their currency symbol,
but golang.org/x/text’s currency package always render the symbol in
front, which is wrong in Catalan and Spanish, and a lot of other
languages.

Consulting the Internet, the most popular package for that is
accounting[0], which is almost as useless because they confuse locale
with the currency’s country of origin’s “usual locale” (e.g., en-US for
USD), which is also wrong: in Catalan i need to write USD prices as
"1.234,56 $" regardless of what Americans do.

With accounting i have the recourse of initializing the struct that
holds all the “locale” information, which is also wrong because i have
to define the decimal and thousands separators, something that depends
only on the locale, next to the currency’s precision, that is
locale-independent.  But, since all CLDR data from golang.org/x/text
is inside an internal package, i can not access it and would need to
define all that information myself, which defeats the purpose of using
an external package.

Since for now i only need the format pattern for currency, i just saved
it into the database of available languages, that i do not expect to
grow too much.

[0]: https://github.com/leekchan/accounting
2023-02-23 12:12:33 +01:00
jordi fita mas 97ef02b0f9 Add views to compute taxes and total amount of invoices
They are not functions because i need to join them with the main
invoice relation, and although possible is a bit more awkward with
functions.

The taxes have their own relation because i will need them grouped by
their name in the PDF, so it will probably be a select for that
relation.
2023-02-22 14:39:38 +01:00
jordi fita mas c1e443e3bc Move the new product description before the taxes
Because in the “default view” that position has a lot of space that is
best used by a large text area than a simple number input.
2023-02-21 13:55:59 +01:00
jordi fita mas 045bf7ff6a Add the formnovalidate attribute to update and add products buttons
They are to complete the invoice, so it can be in an invalid date, but
we do not want to force people to finish all required inputs before they
can add products or update quantities, do we?

Now had to add the empty option label for customer in all cases, because
it could be empty, although that should be done regardless in case
someone has a browser that does not validate fields.
2023-02-14 12:55:19 +01:00
jordi fita mas 4903c8a3b9 Add the form to add products to an invoice and create invoices too
Still missing: the invoice number, that requires more tables and
possibly a PL/pgSQL function to do it properly.
2023-02-12 21:06:48 +01:00
jordi fita mas 5c15b9de20 Add the bare-bones form for invoices 2023-02-11 22:16:48 +01:00
jordi fita mas 4be2597a86 Allow multiple taxes, and even not tax, for products
It seems that we do not agree en whether the IRPF tax should be
something of the product or the contact, so we decided to make the
product have multiple taxes, just in case, and if only one is needed,
then users can just select one; no need to limit to one.
2023-02-08 13:47:36 +01:00
jordi fita mas 73ca559209 Add template for InputField of type textarea 2023-02-07 15:28:22 +01:00
jordi fita mas ae1949024b Allow optional select with empty label
This is not yet necessary, but the empty label is because i do not want
to select a default tax for products—at least, not without a setting for
it.

Since i need to add the required attribute now to select, because
otherwise the browser would allow sending that empty value, i did not
want to do it unconditionally, just in case.
2023-02-05 14:06:33 +01:00
jordi fita mas 60f9792e58 Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.

That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.

At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.

I still need to convert from string to float, however, when printing the
value to the user.  Because the string representation is in C, but i
need to format it according to the locale with golang/x/text.  That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 13:55:12 +01:00
jordi fita mas e9cc331ee0 Add products section
There is still some issues with the price field, because for now it is
in cents, but do not have time now to fix that.
2023-02-04 11:32:39 +01:00
jordi fita mas f611162b0e Move contacts templates in their own directory
This is what directories are for: namespacing; no need for cumbersome
file name prefixes.
2023-02-04 10:48:03 +01:00
jordi fita mas a0a3a5561d Add breadcrumbs 2023-02-03 13:58:10 +01:00
jordi fita mas 7d17620f48 Add the edit contact page 2023-02-03 13:57:43 +01:00
jordi fita mas 917db31227 Add cross-request forgery detection
I use the ten first digits of the cookie’s hash, that i believe it is
not a problem, has the advantage of not expiring until the user logs
out, and using a per user session token is explicitly allowed by
OWASP[0].

[0]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
2023-02-02 11:39:34 +01:00
jordi fita mas 7a439a40cc Use a proper struct for the contact’s form
Our company is a kind-of contact, although it does not appear in the
contact section, thus i could embed the contact form inside the tax
details form to reuse all the common fields.
2023-02-01 14:34:40 +01:00
jordi fita mas 2883438157 Handle tax details and new tax forms with structs and validation
I implemented the Valuer and Scanner interfaces to InputField and
SelectField for better passing values between the database and Go.  I
had a conflict with the Value name and renamed the struct member to Val.

I also had to change the attributes array to be of type
template.HTMLAttr or html/template would replace `form="newtax"`
attribute to `zgotmplz="newtax"` because it deems it “unsafe”.  I do
not like having to use template.HTMLAttr when assigning values, but
i do not know what else i can do now.
2023-02-01 14:15:02 +01:00
jordi fita mas ff5b76b4f5 Use a “proper” struct for the login form
Similar to the profile form, the login form now parses and validates
itself, with the InputField structs that the templates expect.

I realized that i was doing more work than necessary when parsing fields
fro the profile form because i was repeating the operation and the field
name, so now it is a function of InputField.

This time i needed extra attributes for the login form.  I am not sure
that the Go source code needs to know about HTML attributes, but it was
the easiest way to pass them to the template.
2023-02-01 11:02:32 +01:00
jordi fita mas 75fd12bf1c Rename Customer to Contact
That section is intended for both customers and suppliers, collectively
called “contacts”.
2023-02-01 10:14:26 +01:00
jordi fita mas e0abf98bb1 Add custom function to get the current locale from templates
This is just to set the correct `lang` attribute on the HTML, so that
text readers can do its job and the `(optional)` suffix of labels gets
the correct ”translation”.
2023-01-31 15:45:51 +01:00
jordi fita mas 3b3c3bd302 Fix “translation” of ‘(opcional)’ for fields in Spanish and Catalan 2023-01-31 15:43:47 +01:00
jordi fita mas 9f17f55547 Validate profile form and use templates for fields
Let’s start first with a non-fancy validation method with just if
conditionals instead of bringing yet another complicated library.  I
hope i do not regret it.

I wanted to move all the input field to a template because all that
gobbledygook with the .input div and repeating the label in the
placeholder was starting to annoy me.  Now with error messages was even
more concerning.

I did not know whether the label should be a part of the input fields
or something that the template should do.  At the end i decided that
it makes more sense to be part of the input field because in the error
messages i use that same label, thus the template does not have a say
in that, and, besides, it was just easier to write the template.

The same with the error messages: i’ve seen frameworks that have a map
with the field’s id/name to the error slice, but then it would be
a bit harder to write the template.

I added AddError functions instead of just using append inside the
validator function, and have a local variable for whether it all went
OK, because i was worried that i would leave out the `ok = false`
in some conditions.

I had started writing “constructors” functions for InputField and
SelectField, but then had to add other methods to change the required
field and who knows what else, and in the end it was easier to just
construct the field inline.
2023-01-31 15:40:12 +01:00