2023-07-31 12:22:57 +00:00
|
|
|
/*
|
|
|
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
|
|
|
package campsite
|
|
|
|
|
|
|
|
import (
|
2023-08-14 18:18:26 +00:00
|
|
|
"context"
|
2023-07-31 12:22:57 +00:00
|
|
|
"net/http"
|
2024-04-19 09:29:43 +00:00
|
|
|
"time"
|
2023-07-31 12:22:57 +00:00
|
|
|
|
2024-01-26 21:27:54 +00:00
|
|
|
"github.com/jackc/pgx/v4"
|
|
|
|
|
2023-07-31 12:22:57 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
2023-08-08 00:29:14 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/campsite/types"
|
2023-07-31 12:22:57 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
2023-08-14 18:18:26 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/form"
|
2023-07-31 12:22:57 +00:00
|
|
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
2023-08-14 18:18:26 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
2024-04-19 09:29:43 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/season"
|
2023-08-14 18:18:26 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
2023-07-31 12:22:57 +00:00
|
|
|
)
|
|
|
|
|
2023-08-08 00:29:14 +00:00
|
|
|
type AdminHandler struct {
|
|
|
|
types *types.AdminHandler
|
2023-07-31 12:22:57 +00:00
|
|
|
}
|
|
|
|
|
2024-01-15 21:47:16 +00:00
|
|
|
func NewAdminHandler() *AdminHandler {
|
2023-08-08 00:29:14 +00:00
|
|
|
return &AdminHandler{
|
2024-01-15 21:47:16 +00:00
|
|
|
types: types.NewAdminHandler(),
|
2023-07-31 12:22:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-08 00:29:14 +00:00
|
|
|
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc {
|
2023-07-31 12:22:57 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var head string
|
|
|
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
|
|
|
|
|
|
|
switch head {
|
2023-08-14 18:18:26 +00:00
|
|
|
case "new":
|
|
|
|
switch r.Method {
|
|
|
|
case http.MethodGet:
|
2024-01-26 21:27:54 +00:00
|
|
|
f := newCampsiteForm(r.Context(), conn, company)
|
2023-08-14 18:18:26 +00:00
|
|
|
f.MustRender(w, r, user, company)
|
|
|
|
default:
|
|
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
|
|
|
}
|
2023-07-31 12:22:57 +00:00
|
|
|
case "types":
|
Add the company’s slug in the URL before company-dependent handlers
I really doubt that they are going to use more than a single company,
but the application is based on Numerus, that **does** have multiple
company, and followed the same architecture and philosophy: use the URL
to choose the company to manage, even if the user has a single company.
The reason i use the slug instead of the ID is because i do not want to
make the ID public in case the application is really used by employees
of many unrelated companies: they need not need to guess how many
companies there are based on the ID.
I validate this slug to be a valid UUID instead of relaying on the
query’s empty result because casting a string with a malformed value to
UUID results in an error other than data not found. Not with that
select, but it would fail with a function parameter, and i want to add
that UUID check to all functions that do use slugs.
I based uuid.Valid function on Parse() from Google’s uuid package[0]
instead of using regular expression, as it was my first idea, because
that function is an order of magnitude faster in benchmarks:
goos: linux
goarch: amd64
pkg: dev.tandem.ws/tandem/numerus/pkg
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkValidUuid-4 36946050 29.37 ns/op
BenchmarkValidUuid_Re-4 3633169 306.70 ns/op
The regular expression used for the benchmark was:
var re = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
And the input parameter for both functions was the following valid UUID,
because most of the time the passed UUID will be valid:
"f47ac10b-58cc-0372-8567-0e02b2c3d479"
I did not use the uuid package as is, even though it is in Debian’s
repository, because i only need to check whether the value is valid,
not convert it to a byte array. As far as i know, that package can not
do that.
Adding the Company struct into auth was not my intention, as it makes
little sense name-wise, but i need to have the Company when rendering
templates and the company package has templates to render, thus using
the company package for the Company struct would create a dependency
loop between template and company. I’ve chosen the auth package only
because User is also there; User and Company are very much related in
this application, but not enough to include the company inside the user,
or vice versa, as the User comes from the cookie while the company from
the URL.
Finally, had to move methodNotAllowed to the http package, as an
exported function, because it is used now from other packages, namely
campsite.
[0]: https://github.com/google/uuid
2023-07-31 16:51:50 +00:00
|
|
|
h.types.Handler(user, company, conn).ServeHTTP(w, r)
|
2023-08-14 18:18:26 +00:00
|
|
|
case "":
|
|
|
|
switch r.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
serveCampsiteIndex(w, r, user, company, conn)
|
|
|
|
case http.MethodPost:
|
|
|
|
addCampsite(w, r, user, company, conn)
|
|
|
|
default:
|
|
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
|
|
|
}
|
2023-07-31 12:22:57 +00:00
|
|
|
default:
|
2024-01-26 21:27:54 +00:00
|
|
|
f := newCampsiteForm(r.Context(), conn, company)
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
if err := f.FillFromDatabase(r.Context(), conn, company, head); err != nil {
|
2023-08-14 18:18:26 +00:00
|
|
|
if database.ErrorIsNotFound(err) {
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
panic(err)
|
|
|
|
}
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
|
|
|
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
|
|
|
switch head {
|
|
|
|
case "":
|
|
|
|
switch r.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
f.MustRender(w, r, user, company)
|
|
|
|
case http.MethodPut:
|
|
|
|
editCampsite(w, r, user, company, conn, f)
|
|
|
|
default:
|
|
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
|
|
|
}
|
2024-01-26 21:27:54 +00:00
|
|
|
case "slides":
|
|
|
|
h.carouselHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r)
|
|
|
|
case "features":
|
|
|
|
h.featuresHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r)
|
2023-08-14 18:18:26 +00:00
|
|
|
default:
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
http.NotFound(w, r)
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
2023-07-31 12:22:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-14 18:18:26 +00:00
|
|
|
|
|
|
|
func serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
2024-04-19 19:09:28 +00:00
|
|
|
page := newCampsiteIndex()
|
|
|
|
if err := page.Parse(r); err != nil {
|
2023-08-14 18:18:26 +00:00
|
|
|
panic(err)
|
|
|
|
}
|
2024-04-19 19:09:28 +00:00
|
|
|
var err error
|
|
|
|
page.Campsites, err = collectCampsiteEntries(r.Context(), company, conn, page.From.Date(), page.To.Date())
|
|
|
|
if err != nil {
|
2024-04-19 09:29:43 +00:00
|
|
|
panic(err)
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
2024-04-19 09:29:43 +00:00
|
|
|
page.Months = collectMonths(page.From.Date(), page.To.Date())
|
2023-08-14 18:18:26 +00:00
|
|
|
page.MustRender(w, r, user, company)
|
|
|
|
}
|
|
|
|
|
2024-04-19 19:09:28 +00:00
|
|
|
func collectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time) ([]*campsiteEntry, error) {
|
2023-08-14 18:18:26 +00:00
|
|
|
rows, err := conn.Query(ctx, `
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
select campsite.label
|
2023-08-14 18:18:26 +00:00
|
|
|
, campsite_type.name
|
|
|
|
, campsite.active
|
|
|
|
from campsite
|
|
|
|
join campsite_type using (campsite_type_id)
|
|
|
|
where campsite.company_id = $1
|
|
|
|
order by label`, company.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
2024-04-19 19:09:28 +00:00
|
|
|
byLabel := make(map[string]*campsiteEntry)
|
2023-08-14 18:18:26 +00:00
|
|
|
var campsites []*campsiteEntry
|
|
|
|
for rows.Next() {
|
|
|
|
entry := &campsiteEntry{}
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
if err = rows.Scan(&entry.Label, &entry.Type, &entry.Active); err != nil {
|
2023-08-14 18:18:26 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
campsites = append(campsites, entry)
|
2024-04-19 19:09:28 +00:00
|
|
|
byLabel[entry.Label] = entry
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := collectBookingEntries(ctx, company, conn, from, to, byLabel); err != nil {
|
|
|
|
return nil, err
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return campsites, nil
|
|
|
|
}
|
|
|
|
|
2024-04-19 19:09:28 +00:00
|
|
|
func collectBookingEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time, campsites map[string]*campsiteEntry) error {
|
|
|
|
lastDay := to.AddDate(0, 1, 0)
|
|
|
|
rows, err := conn.Query(ctx, `
|
|
|
|
select campsite.label
|
Allow many campsites for each reservation
This is actually only used for plots, but, of course, it means that
every booking now can potentially have many booked campsites, and have
to create a relation for it.
I now have a conundrum regarding stay dates: i need them to be in the
same table as the campsite_id, because constraints only work on a single
relation and without the dates i can not make sure that i am not
overbooking a given campsite; but, on the other hand, all campsites
under the same booking must be for the same dates.
Where does stay belong, then? In booking or booking_campsite? If in
booking then i can not have a constraint that most assuredly will bite
me in the back, but if in booking_campsite then each campsite could
potentially have different dates.
As far as i can see, i can not use a exclude constraint with <> for
dates in booking_campsite to ensure that all rows with the same
booking_id have the same stay (i.e., exclude those that have a different
stay for the same booking_id).
For now, the say is in **both** relations: in booking, because i need it
when it is a prebooking, at least, and in booking_campsite for the
aforementioned constraint requirements.
Will this come back and bite me? Yes, it will. But what can i do?
2024-04-21 19:28:41 +00:00
|
|
|
, lower(booking_campsite.stay * daterange($2::date, $3::date))
|
2024-04-19 19:09:28 +00:00
|
|
|
, holder_name
|
|
|
|
, booking_status
|
Allow many campsites for each reservation
This is actually only used for plots, but, of course, it means that
every booking now can potentially have many booked campsites, and have
to create a relation for it.
I now have a conundrum regarding stay dates: i need them to be in the
same table as the campsite_id, because constraints only work on a single
relation and without the dates i can not make sure that i am not
overbooking a given campsite; but, on the other hand, all campsites
under the same booking must be for the same dates.
Where does stay belong, then? In booking or booking_campsite? If in
booking then i can not have a constraint that most assuredly will bite
me in the back, but if in booking_campsite then each campsite could
potentially have different dates.
As far as i can see, i can not use a exclude constraint with <> for
dates in booking_campsite to ensure that all rows with the same
booking_id have the same stay (i.e., exclude those that have a different
stay for the same booking_id).
For now, the say is in **both** relations: in booking, because i need it
when it is a prebooking, at least, and in booking_campsite for the
aforementioned constraint requirements.
Will this come back and bite me? Yes, it will. But what can i do?
2024-04-21 19:28:41 +00:00
|
|
|
, upper(booking_campsite.stay * daterange($2::date, $3::date)) - lower(booking_campsite.stay * daterange($2::date, $3::date))
|
|
|
|
, booking_campsite.stay &> daterange($2::date, $3::date)
|
|
|
|
, booking_campsite.stay &< daterange($2::date, $3::date)
|
|
|
|
from booking_campsite
|
|
|
|
join booking using (booking_id)
|
2024-04-19 19:09:28 +00:00
|
|
|
join campsite using (campsite_id)
|
|
|
|
where booking.company_id = $1
|
Allow many campsites for each reservation
This is actually only used for plots, but, of course, it means that
every booking now can potentially have many booked campsites, and have
to create a relation for it.
I now have a conundrum regarding stay dates: i need them to be in the
same table as the campsite_id, because constraints only work on a single
relation and without the dates i can not make sure that i am not
overbooking a given campsite; but, on the other hand, all campsites
under the same booking must be for the same dates.
Where does stay belong, then? In booking or booking_campsite? If in
booking then i can not have a constraint that most assuredly will bite
me in the back, but if in booking_campsite then each campsite could
potentially have different dates.
As far as i can see, i can not use a exclude constraint with <> for
dates in booking_campsite to ensure that all rows with the same
booking_id have the same stay (i.e., exclude those that have a different
stay for the same booking_id).
For now, the say is in **both** relations: in booking, because i need it
when it is a prebooking, at least, and in booking_campsite for the
aforementioned constraint requirements.
Will this come back and bite me? Yes, it will. But what can i do?
2024-04-21 19:28:41 +00:00
|
|
|
and booking_campsite.stay && daterange($2::date, $3::date)
|
2024-04-19 19:09:28 +00:00
|
|
|
order by label`, company.ID, from, lastDay)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
entry := &bookingEntry{}
|
|
|
|
var label string
|
|
|
|
var date time.Time
|
|
|
|
if err = rows.Scan(&label, &date, &entry.Holder, &entry.Status, &entry.Nights, &entry.Begin, &entry.End); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
campsite := campsites[label]
|
|
|
|
if campsite != nil {
|
|
|
|
if campsite.Bookings == nil {
|
|
|
|
campsite.Bookings = make(map[time.Time]*bookingEntry)
|
|
|
|
}
|
|
|
|
campsite.Bookings[date] = entry
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-14 18:18:26 +00:00
|
|
|
type campsiteEntry struct {
|
2024-04-19 19:09:28 +00:00
|
|
|
Label string
|
|
|
|
Type string
|
|
|
|
Active bool
|
|
|
|
Bookings map[time.Time]*bookingEntry
|
|
|
|
}
|
|
|
|
|
|
|
|
type bookingEntry struct {
|
|
|
|
Holder string
|
|
|
|
Status string
|
|
|
|
Nights int
|
|
|
|
Begin bool
|
|
|
|
End bool
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type campsiteIndex struct {
|
2024-04-19 09:29:43 +00:00
|
|
|
From *form.Month
|
|
|
|
To *form.Month
|
2023-08-14 18:18:26 +00:00
|
|
|
Campsites []*campsiteEntry
|
2024-04-19 09:29:43 +00:00
|
|
|
Months []*Month
|
|
|
|
}
|
|
|
|
|
2024-04-19 19:09:28 +00:00
|
|
|
func newCampsiteIndex() *campsiteIndex {
|
2024-04-19 09:29:43 +00:00
|
|
|
now := time.Now()
|
|
|
|
from := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
to := from.AddDate(0, 3, 0)
|
|
|
|
return &campsiteIndex{
|
|
|
|
From: &form.Month{
|
|
|
|
Name: "from",
|
|
|
|
Year: from.Year(),
|
|
|
|
Month: from.Month(),
|
|
|
|
},
|
|
|
|
To: &form.Month{
|
|
|
|
Name: "to",
|
|
|
|
Year: to.Year(),
|
|
|
|
Month: to.Month(),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (page *campsiteIndex) Parse(r *http.Request) error {
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
page.From.FillValue(r)
|
|
|
|
page.To.FillValue(r)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type Month struct {
|
|
|
|
Year int
|
|
|
|
Month time.Month
|
|
|
|
Name string
|
|
|
|
Days []time.Time
|
|
|
|
Spans []*Span
|
|
|
|
}
|
|
|
|
|
|
|
|
type Span struct {
|
|
|
|
Weekend bool
|
|
|
|
Count int
|
|
|
|
}
|
|
|
|
|
|
|
|
func isWeekend(t time.Time) bool {
|
|
|
|
switch t.Weekday() {
|
|
|
|
case time.Saturday, time.Sunday:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func collectMonths(from time.Time, to time.Time) []*Month {
|
|
|
|
current := time.Date(from.Year(), from.Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
numMonths := (to.Year()-from.Year())*12 + int(to.Month()) - int(from.Month()) + 1
|
|
|
|
var months []*Month
|
|
|
|
for i := 0; i < numMonths; i++ {
|
|
|
|
span := &Span{
|
|
|
|
Weekend: isWeekend(current),
|
|
|
|
}
|
|
|
|
month := &Month{
|
|
|
|
Year: current.Year(),
|
|
|
|
Month: current.Month(),
|
|
|
|
Name: season.LongMonthNames[current.Month()-1],
|
|
|
|
Days: make([]time.Time, 0, 31),
|
|
|
|
Spans: make([]*Span, 0, 10),
|
|
|
|
}
|
|
|
|
month.Spans = append(month.Spans, span)
|
|
|
|
for current.Month() == month.Month {
|
|
|
|
month.Days = append(month.Days, current)
|
|
|
|
if span.Weekend != isWeekend(current) {
|
|
|
|
span = &Span{
|
|
|
|
Weekend: !span.Weekend,
|
|
|
|
}
|
|
|
|
month.Spans = append(month.Spans, span)
|
|
|
|
}
|
|
|
|
span.Count = span.Count + 1
|
|
|
|
current = current.AddDate(0, 0, 1)
|
|
|
|
}
|
|
|
|
months = append(months, month)
|
|
|
|
}
|
|
|
|
return months
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (page *campsiteIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
2023-09-25 10:34:05 +00:00
|
|
|
template.MustRenderAdminFiles(w, r, user, company, page, "campsite/index.gohtml", "web/templates/campground_map.svg")
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
2024-01-26 21:27:54 +00:00
|
|
|
f := newCampsiteForm(r.Context(), conn, company)
|
2024-01-25 19:57:07 +00:00
|
|
|
processCampsiteForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
|
2024-01-26 21:27:54 +00:00
|
|
|
var err error
|
|
|
|
f.ID, err = tx.AddCampsite(ctx, f.CampsiteType.Int(), f.Label.Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return translateCampsite(ctx, tx, company, f)
|
2024-01-25 19:57:07 +00:00
|
|
|
})
|
2023-08-14 18:18:26 +00:00
|
|
|
httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
2024-01-26 21:27:54 +00:00
|
|
|
func translateCampsite(ctx context.Context, tx *database.Tx, company *auth.Company, f *campsiteForm) error {
|
|
|
|
for lang := range company.Locales {
|
|
|
|
l := lang.String()
|
|
|
|
if l == f.DefaultLang {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err := tx.TranslateCampsite(ctx, f.ID, lang, f.Info1[l].Val, f.Info2[l].Val); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-14 18:18:26 +00:00
|
|
|
func editCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm) {
|
2024-01-25 19:57:07 +00:00
|
|
|
processCampsiteForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
|
2024-01-26 21:27:54 +00:00
|
|
|
if err := tx.EditCampsite(ctx, f.ID, f.CampsiteType.Int(), f.Label.Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val, f.Active.Checked); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return translateCampsite(ctx, tx, company, f)
|
2024-01-25 19:57:07 +00:00
|
|
|
})
|
|
|
|
httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func processCampsiteForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm, act func(ctx context.Context, tx *database.Tx) error) {
|
|
|
|
if ok, err := form.Handle(f, w, r, user); err != nil {
|
2023-08-14 18:18:26 +00:00
|
|
|
return
|
2024-01-25 19:57:07 +00:00
|
|
|
} else if !ok {
|
2023-08-14 18:18:26 +00:00
|
|
|
f.MustRender(w, r, user, company)
|
|
|
|
return
|
|
|
|
}
|
2024-01-25 19:57:07 +00:00
|
|
|
|
|
|
|
tx := conn.MustBegin(r.Context())
|
|
|
|
defer tx.Rollback(r.Context())
|
|
|
|
if err := act(r.Context(), tx); err != nil {
|
2024-01-25 19:48:39 +00:00
|
|
|
panic(err)
|
|
|
|
}
|
2024-01-25 19:57:07 +00:00
|
|
|
tx.MustCommit(r.Context())
|
2023-08-14 18:18:26 +00:00
|
|
|
httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
type campsiteForm struct {
|
2024-01-26 21:27:54 +00:00
|
|
|
DefaultLang string
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
ID int
|
|
|
|
CurrentLabel string
|
2023-08-14 18:18:26 +00:00
|
|
|
Active *form.Checkbox
|
|
|
|
CampsiteType *form.Select
|
|
|
|
Label *form.Input
|
2024-01-26 21:27:54 +00:00
|
|
|
Info1 form.I18nInput
|
|
|
|
Info2 form.I18nInput
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
|
|
|
|
2024-01-26 21:27:54 +00:00
|
|
|
func newCampsiteForm(ctx context.Context, conn *database.Conn, company *auth.Company) *campsiteForm {
|
2023-08-14 18:18:26 +00:00
|
|
|
campsiteTypes := form.MustGetOptions(ctx, conn, "select campsite_type_id::text, name from campsite_type where active")
|
|
|
|
return &campsiteForm{
|
2024-01-26 21:27:54 +00:00
|
|
|
DefaultLang: company.DefaultLanguage.String(),
|
2023-08-14 18:18:26 +00:00
|
|
|
Active: &form.Checkbox{
|
|
|
|
Name: "active",
|
|
|
|
Checked: true,
|
|
|
|
},
|
|
|
|
CampsiteType: &form.Select{
|
|
|
|
Name: "description",
|
|
|
|
Options: campsiteTypes,
|
|
|
|
},
|
|
|
|
Label: &form.Input{
|
|
|
|
Name: "label",
|
|
|
|
},
|
2024-01-26 21:27:54 +00:00
|
|
|
Info1: form.NewI18nInput(company.Locales, "info1"),
|
|
|
|
Info2: form.NewI18nInput(company.Locales, "info2"),
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
func (f *campsiteForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, label string) error {
|
|
|
|
f.CurrentLabel = label
|
2024-01-26 21:27:54 +00:00
|
|
|
var info1 database.RecordArray
|
|
|
|
var info2 database.RecordArray
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
row := conn.QueryRow(ctx, `
|
|
|
|
select campsite_id
|
|
|
|
, array[campsite_type_id::text]
|
|
|
|
, label
|
2024-01-26 21:27:54 +00:00
|
|
|
, campsite.info1::text
|
|
|
|
, campsite.info2::text
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
, active
|
2024-01-26 21:27:54 +00:00
|
|
|
, array_agg((lang_tag, i18n.info1::text))
|
|
|
|
, array_agg((lang_tag, i18n.info2::text))
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
from campsite
|
2024-01-26 21:27:54 +00:00
|
|
|
left join campsite_i18n as i18n using (campsite_id)
|
Add campsite map in SVG
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
2023-09-24 01:17:13 +00:00
|
|
|
where company_id = $1
|
2024-01-26 21:27:54 +00:00
|
|
|
and label = $2
|
|
|
|
group by campsite_id
|
|
|
|
, campsite_type_id
|
|
|
|
, label
|
|
|
|
, campsite.info1::text
|
|
|
|
, campsite.info2::text
|
|
|
|
, active
|
|
|
|
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID, label)
|
|
|
|
if err := row.Scan(&f.ID, &f.CampsiteType.Selected, &f.Label.Val, &f.Info1[f.DefaultLang].Val, &f.Info2[f.DefaultLang].Val, &f.Active.Checked, &info1, &info2); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := f.Info1.FillArray(info1); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := f.Info2.FillArray(info2); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2023-08-14 18:18:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *campsiteForm) Parse(r *http.Request) error {
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
f.Active.FillValue(r)
|
|
|
|
f.CampsiteType.FillValue(r)
|
|
|
|
f.Label.FillValue(r)
|
2024-01-26 21:27:54 +00:00
|
|
|
f.Info1.FillValue(r)
|
|
|
|
f.Info2.FillValue(r)
|
2023-08-14 18:18:26 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *campsiteForm) Valid(l *locale.Locale) bool {
|
|
|
|
v := form.NewValidator(l)
|
|
|
|
v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid."))
|
|
|
|
v.CheckRequired(f.Label, l.GettextNoop("Label can not be empty."))
|
|
|
|
return v.AllOK
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *campsiteForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
|
|
|
template.MustRenderAdmin(w, r, user, company, "campsite/form.gohtml", f)
|
|
|
|
}
|