Commit Graph

30 Commits

Author SHA1 Message Date
jordi fita mas 674cdff87b Add a new Cursor form type
To hold the common logic of detecting pagination, forming the key, and
splitting its values later on.

I can take advantage that a form with action="get" already adds its
fields to the query string to have a common template for pagination. The
only problem is that i have different column spans for different tables,
therefore had to add a colspan to the struct.
2024-05-03 19:00:02 +02:00
jordi fita mas 17f7520876 Add customer and invoices sections
Copied as much as i could from Numerus, and made as few modifications as
i could to adapt to this code base; it is, quite frankly, a piece of
shit.

We need to be able to create invoices from scratch “just in case”,
apparently, but it is not yet possible to create an invoice from a
booking.
2024-04-28 20:28:45 +02:00
jordi fita mas f2b24a83a3 Add check-in form
I based the form from the documentation given by the Mossos
d’Esquadra[0], required by law.

https://registreviatgers.mossos.gencat.cat/mossos_hotels/AppJava/fitxaviatger.do?reqCode=create
2024-04-26 17:09:36 +02:00
jordi fita mas e726bde025 Replace admin’s campsite map with a booking grid
Customer told us that they are used to a view of the booking status of
each campsite in the form of a grid: each campsite is a row, and each
day a column; bookings are show as boxes from the first day to the last
day on the corresponding campsite’s row.

I do not yet show the booking boxes, but at least now i have the grid
and date selector form in place.

In the form i would need a couple of input[type=month], but this is not
yet supported in Firefox and Safari. According to MDN, one common way
to bypass that problem is to have two fields, one for the month and the
other for the year; i just did that, but had to create a new input type
in the `form` package just for this.
2024-04-19 11:29:52 +02:00
jordi fita mas e5023a2a41 Handle the booking cart entirely with HTMx
Besides the dynamic final cart, that was already handled by HTMx, i had
to check the maximum number of guests, whether the accommodation allows
“overflow”, whether dogs are allowed, and that the booking dates were
within the campground’s opening and closing dates.

I could do all of this with AlpineJS, but then i would have to add the
same validation to the backend, prior to accept the payment.  Would not
make more sense to have them in a single place, namely the backend? With
HTMx i can do that.

However, i now have to create the form “piecemeal”, because i may not
have the whole information when the visitor arrives to the booking page,
and i still had the same problem as in commit d2858302efa—parsing the
whole form as is would leave guests and options field empty, rather than
at their minimum values.

One of the fieldsets in that booking form are the arrival and departure
dates, that are the sames we use in the campsite type’s page to
“preselect” these values.  Since now are in a separate struct, i can
reuse the same type and validation logic for both pages, making my
JavaScript code useless, but requiring HTMx.  I think this is a good
tradeoff, in fact.
2024-02-10 03:49:44 +01:00
jordi fita mas 51540151ff Refactor ISO date, and datestamp format in constant 2024-01-31 19:58:46 +01:00
jordi fita mas bd124581cc Handle null strings in I18nInput.FillArray
Until now i always had the translations be empty strings if some columns
did not have the full translation, but this is going too far on the
non-NULL policy: surely they have a translations, but we do not know it
yet; this is the exact type of situations NULL values are for.

Besides the philosophical distinction, having empty strings instead of
NULLs is less practical, because i no longer can user coalesce() to
retrieve the default language text in case the translation for that
particular field is not available, even if the row for a locale exists.

In time i will change all _i18n relations, but for now only these from
campsite follow the “new policy”.
2024-01-26 22:31:11 +01:00
jordi fita mas c284230436 Add public pages for each individual accommodation
A small page with a brief description, carousel, and feature list of
each individual accommodation.

Most of the relations and functions for carousel and features are like
the ones for campsite types, but i had to use the accommodation’s label
to find them, because they do not have slugs; i did not even though
these would be public, and they already have a label, although not
unique for all companies, like UUID slugs are.
2024-01-26 22:27:54 +01:00
jordi fita mas 4138eda5cb Add Error method to I18nInput
Otherwise, we can not show the error message to the user, although it
assumes only the default language has error message, which is the case
for now, but….
2024-01-22 20:15:26 +01:00
jordi fita mas 4e126237a0 Replace L10nInput with I18nInput in service
locale.Translation and form.L10nInput are no longer used.

The translation type in Postgres is now also useless, and i believe it
was never used, but i keep it because I already have a tag and i can not
just remove it, meaning that dropping it is more trouble that worth it.
2024-01-12 21:06:12 +01:00
jordi fita mas 3d0f7b0dc1 Handle empty array_agg in I18nInput.FillArray
array_agg returns [null] instead of [] when it can not find any rows,
thus the range enters the loop, but it fails to convert the null to
string.
2024-01-12 19:28:13 +01:00
jordi fita mas 03c20fec88 Add management of legal texts 2023-12-22 02:23:18 +01:00
jordi fita mas ff6e9497b5 Replace contact page with location
Customer does not want a contact page, but a page where they can write
the direction on how to reach the campground, with a Google map embed
instead of using Leaflet, because Google Maps shows the reviews right
in the map.

That means i had to replace the GPS locations with XML fields for the
customer to write.  In all four languages.

This time i tried a translation approach inspired by PrestaShop: instead
of opening a new page for each language, i have all languages in the
same page and use AlpineJS to show just a single language.  It is far
easier to write the translations, even though you do not have the source
text visible, specially in this section that there is no place for me
to put the language links.
2023-12-21 21:17:04 +01:00
jordi fita mas 0cbf973cbb Add the payment form to admin
Had to change setup_redsys because admins can not read the current
encrypt key, thus it is not possible to `set encrypt_key =
coalesce(…, encrypt_key)`.

Not that it did much sense, anyway, as i was already inside the branch
of the if when encrpty_key is null.

However, it seems that this also affects in the `on conflict` update. I
assume this is because `excluded` is some kind of row of the relation
and has the same restrictions.
2023-10-27 16:08:13 +02:00
jordi fita mas 302ce29e4a Add the first draft of the booking and payment forms
The form is based on the one in the current website, but in a single
page instead of split into many pages; possibly each <fieldset> should
be in a separate page/view.  The idea is for Oriol to check the design
and decide how it would be presented to the user, so i needed something
to show him first.

I hardcoded the **test** data for the customer’s Redsys account.  Is
this bad? I hope not, but i am not really, really sure.

The data sent to Redsys is just a placeholder because there are booking
details that i do not know, like what i have to do with the “teenagers”
field or the area preferences, thus i can not yet have a booking
relation.  Nevertheless, had to generate a random order number up to
12-chars in length or Redsys would refuse the payment, claiming that
the order is duplicated.

The Redsys package is based on the PHP code provided by Redsys
themselves, plus some hints at the implementations from various Go
packages that did not know why they were so complicated.

Had to grant select on table country to guest in order to show the
select with the country options.

I have changed the “Postal code” input in taxDetails for “Postcode”
because this is the spell that it is used in the current web, i did not
see a reason to change it—it is an accepted form—, and i did not want to
have inconsistencies between forms.
2023-10-19 21:37:34 +02:00
jordi fita mas 59fe8dd131 Add Go functions for campsite_type PostgreSQL functions
I want these because when there are changes in the signature i then have
to find where it is used, and it is easier to do when the compiler tells
you.

For relations it is less necessary because GoLand knows how to validate
SQL strings for them, but it seems to not work with functions,
apparently due to the lack of the “FROM” keyword.

Besides, it tx.FunctionName(ctx, params...) is shorter than
tx.Exec("select functions_name($1, $2…)", params...).
2023-10-13 12:53:30 +02:00
jordi fita mas ef6a8f5aee Add the campsite type cost per season 2023-10-01 21:14:39 +02:00
jordi fita mas 680d51e704 Add max campers and dogs allowed fields to campsite types 2023-09-29 20:17:39 +02:00
jordi fita mas e584e29f46 Make seasons’ calendar dynamic and allow to set/unset ranges
The CSS is not very good, but for testing purposes it will work.
2023-09-27 14:21:27 +02:00
jordi fita mas 44526b1efb Add the edit form for services
This one has an input to select the icon.  It makes no sense to choose
an icon only by name, thus a <select> is not appropriate, and had to
use a hidden input with a row of button to choose the icon from.  This
works now only because there are very few icons; we’ll need to choose
a different approach when there are many more icons.

Since now the icons have to be defined in CSS for both the public and
admin sections, i had to split it into a separate file that both sites
can use.  I considered the option to “include” that CSS with m4, like
i do for images in demo.sql, but it made everything too complicated
(e.g., having to call make for each change in the CSS), and decided to
load that CSS in a separate <link>.
2023-09-25 20:10:33 +02:00
jordi fita mas b5d40cc262 Add the upload form to the media picker
It makes easier to upload new images from the place where we need it,
instead of having to go to the media section each time.

It was a little messy, this one.

First of all, I realized that POSTint to /admin/media/picker to get the
new media field was wrong: i was not asking the server to “accept an
entity”, but only requesting a new HTML value, just like a GET to
/admin/media/upload requests the form to upload a new media, thus here
i should do the same, except i needed the query parameters to change the
field, which is fine—it is actually a different resource, thus a
different URL.

Then, i thought that i could not POST the upload to /admin/media,
because i returned a different HTML —the media field—, so i reused the
recently unused POST to /admin/media/picker to upload that file and
return the HTML for the field.  It was wrong, because i was not
requesting the server to put the file as a subordinate of
/admin/media/picker, only /admin/media, but i did not come up with any
other solution.

Since i had two different upload functions now, i created uploadForm’s
Handle method to refactorize the duplicated logic to a single place.
Unfortunately, i did not work as i expected because uploadForm’s and
mediaPicker’s MustRender methods are different, and mediaPicker has to
embed uploadForm to render the form in the picker.  That made me change
Handle’s output to a boolean and error in order for the HTTP handler
function know when to render the form with the error messages with the
proper MustRender handler.

However, I saw the opportunity of reusing that Handler method for
editMedia, that was doing mostly the same job, but had to call a
different Validate than uploadForm’s, because editMedia does not require
the uploaded file.  That’s when i realized that i could use an interface
and that this interface could be reused not only within media but
throughout the application, and added HandleMultipart in form.

Had to create a different interface for multipart forms because they
need different parameters in Parse that non-multipart form, when i add
that interface, hence had to also change Parse to ParseForm to account
for the difference in signature; not a big deal.

After all that, i realized that i **could** POST to /admin/media in both
cases, because i always return “an HTML entity”, it just happens that
for the media section it is empty with a redirect, and for the picker is
the field.  That made the whole Handle method a bit redundant, but i
left it nevertheless, as i find it slightly easier to read the
uploadMedia function now.
2023-09-22 01:40:22 +02:00
jordi fita mas 97cf117da3 Manage all media uploads in a single place
It made no sense to have a file upload in each form that needs a media,
because to reuse an existing media users would need to upload the exact
same file again; this is very unusual and unfriendly.

A better option is to have a “centralized” media section, where people
can upload files there, and then have a picker to select from there.
Ideally, there would be an upload option in the picker, but i did not
add it yet.

I’ve split the content from the media because i want users to have the
option to update a media, for instance when they need to upload a
reduced or cropped version of the same photo, without an edit they would
need to upload the file as a new media and then update all places where
the old version was used.  And i did not want to trouble people that
uploads the same photo twice: without the separate relation, doing so
would throw a constraint error.

I do not believe there is any security problem to have all companies
link their media to the same file, as they were already readable by
everyone and could upload the data from a different company to their
own; in other words, it is not worse than it was now.
2023-09-21 01:56:44 +02:00
jordi fita mas f746c82b46 Make home page’s carousel manageable via the database
I debated with myself whether to create the home_carousel relation or
rather if it would be better to have a single carousel relation for all
pages.  However, i thought that it would be actually harder to maintain
a single relation because i would need an additional column to tell one
carrousel from another, and what would that column be? An enum? A
foreign key to another relation? home_carousel carries no such issues.

I was starting to duplicate logic all over the packages, such as the
way to encode media paths or “localization” (l10n) input fields.
Therefore, i refactorized them.

In the case of media path, i added a function that accepts rows of
media, because always need the same columns from the row, and it was
yet another repetition if i needed to pass them all the time.  Plus,
these kind of functions can be called as `table.function`, that make
them look like columns from the table; if PostgreSQL implemented virtual
generated columns, i would have used that instead.

I am not sure whether that media_path function can be immutable. An
immutable function is “guaranteed to return the same results given the
same arguments forever”, which would be true if the inputs where the
hash and the original_filename columns, instead of the whole rows, but
i left it as static because i did not know whether PostgreSQL interprets
the “same row but with different values” as a different input.  That is,
whether PostgreSQL’s concept of row is the actual tuple or the space
that has a rowid, irrespective of contents; in the latter case, the
function can not be immutable.  Just to be in the safe side, i left it
stable.

The home page was starting to grow a bit too much inside the app
package, new that it has its own admin handler, and moved it all to a
separate package.
2023-09-15 01:05:38 +02:00
jordi fita mas da127124a1 Add cover media to campsite types
This is the image that is shown at the home page, and maybe other pages
in the future.  We can not use a static file because this image can be
changed by the customer, not us; just like name and description.

I decided to keep the actual media content in the database, but to copy
this file out to the file system the first time it is accessed. This is
because we are going to replicate the database to a public instance that
must show exactly the same image, but the customer will update the image
from the private instance, behind a firewall.  We could also synchronize
the folder where they upload the images, the same way we will replicate,
but i thought that i would make the whole thing a little more brittle:
this way if it can replicate the update of the media, it is impossible
to not have its contents; dumping it to a file is to improve subsequent
requests to the same media.

I use the hex representation of the media’s hash as the URL to the
resource, because PostgreSQL’s base64 is not URL save (i.e., it uses
RFC2045’s charset that includes the forward slash[0]), and i did not
feel necessary write a new function just to slightly reduce the URLs’
length.

Before checking if the file exists, i make sure that the given hash is
an hex string, like i do for UUID, otherwise any other check is going
to fail for sure.  I moved out hex.Valid function from UUID to check for
valid hex values, but the actual hash check is inside app/media because
i doubt it will be used outside that module.

[0]: https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
2023-09-10 03:04:18 +02:00
jordi fita mas b4919db6c4 Add seasons’ relation, functions, and admin section
Seasons have a color to show on the calendar. I need them in HTML format
(e.g., #123abc) in order to set as value to `<input type="color">`, but
i did not want to save them as text in the database, as integers are
better representations of colors—in fact, that’s what the HTML syntax
also is: an integer.

I think the best would be to create an extension that adds an HTML color
type, with functions to convert from many representations (e.g., CSS’
rgb or even color names) to integer and back.  However, that’s a lot of
work and i can satisfy Camper’s needs with just a couple of functions
and a domain.

To show the color on the index, at first tried to use a read-only
`<input type="color">`, but seems that this type of input can not be
read-only and must be disabled instead.  However, i do not know whether
it makes sense to have a disabled input outside a form “just” to show
a color; i suspect it does not.  Thus, at the end i use SVG with a
single circle, which is better that a 50%-rounded div with a background
color, even if the result is the same—SVG **is** intended for showing
pictures, which is this case.
2023-08-16 20:15:57 +02:00
jordi fita mas 50fbfce9ee Add the form to update company’s tax details
It is inside the “user menu” only because this is where Numerus has the
same option, although it makes less sense in this case, because Numerus
is geared toward individual freelancers while Camper is for companies.
But, since it is easy to change afterward, this will do for now.

However, it should be only shown to admin users, because regular
employees have no UPDATE privilege on the company relation.  Thus, the
need for a new template function to check if the user is admin.

Part of #17.
2023-08-15 22:35:21 +02:00
jordi fita mas 208952b964 Add the Active field to the campsite type’s edit form and function
In the new form this field is hidden and always active, because it makes
no sense to add an inactive campsite type.
2023-08-14 11:43:58 +02:00
jordi fita mas b03ef35a95 Allow users to update their profile images
I do not see the profile image as an “integral part” of the user (i.e.,
no need for constraints), hence i do not want to store it in the
database, as would do for the identification image during check-in.

By default, i store the avatars in /var/lib/camper/avatars, but it is a
variable to allow packagers change this value using the linker.

This is also served as a test bed for uploading files to the server,
that now has a better interface and uses less resources that what i did
to Numerus.

Now the profile handler needs to keep a variable to know the path to the
avatars’ directory, thus i had to change it to a struct nested in app,
much like the fileHandler does.  It still has to return the HandlerFunc,
however, as this function needs to close over the user and connection
variables.

Part of #7.
2023-07-28 20:15:09 +02:00
jordi fita mas f963f54839 Add profile form, inside a user menu
This is the first form that uses HTMx, and can not return a 400 error
code because otherwise HTMx does not render the content.

Since now there are pages that do not render the whole html, with header
and body, i need a different layout for these, and moved the common code
to handle forms and such a new template file that both layouts can use.
I also need the request in template.MustRender to check which layout i
should use.

Closes #7.
2023-07-26 20:46:09 +02:00
jordi fita mas 01526bff1a Convert the login variables to a struct with parsing and validation
It is a lot of code having to check the login variables inside the POST
handler, and i could not mark each input field individually as invalid
because the generic errors array i was using did no identify which field
had the error.

Thus, i use more or less the same technique as with Numerus: a struct
with the value and the error message.  This time the input field does
not have the label and extra attributes because i believe this belongs
to the template: if i want do reuse the same form template, i should
create a common template rather than defining everything in Go.

The name is a bit different, however, because it has meaning both to the
front and back ends, as it needs to be exactly the same.  Writing it
twice is error-prone, as with a rename i could easily forget to change
one or the other, and here i see value in having that in Go, because
it is also used in code.
2023-07-24 17:17:15 +02:00