diff --git a/demo/demo.sql b/demo/demo.sql
index 05b065b..40a30f1 100644
--- a/demo/demo.sql
+++ b/demo/demo.sql
@@ -654,4 +654,18 @@ select translate_surroundings_highlight(120, 'fr', 'Pyrénées', '
Pyrénées
select translate_surroundings_highlight(121, 'fr', 'Gérone', '
Visite incontournable, principalement: le quartier juifs, les Rambles, la cathédrale et les jardins qui l’entourent … Sans oublier ses nombreuses boutiques !
Turisme Girona
');
select translate_surroundings_highlight(122, 'fr', 'Barcelone', 'Barcelone c’est plus que des boutiques et le Barça … Découvrez la richesse de ces quartiers: Gràcia, Barceloneta, …
Turisme Barcelona
');
+alter table booking alter column booking_id restart with 122;
+
+insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card, booking_status)
+values (52, 72, 'Juli Verd', current_date + interval '23 days', current_date + interval '25 days', 0, false, 'created')
+ , (52, 72, 'Pere Gil', current_date + interval '24 days', current_date + interval '25 days', 1, true, 'cancelled')
+ , (52, 73, 'Calèndula Groga', current_date + interval '24 days', current_date + interval '25 days', 0, false, 'confirmed')
+ , (52, 73, 'Rosa Blava', current_date + interval '15 days', current_date + interval '22 days', 0, false, 'checked-in')
+ , (52, 74, 'Margarita Blanca', current_date + interval '7 days', current_date + interval '8 days', 0, false, 'invoiced')
+ , (52, 74, 'Camèlia Vermella', current_date + interval '7 days', current_date + interval '8 days', 0, false, 'created')
+ , (52, 74, 'Valeriana Rosa', current_date + interval '3 days', current_date + interval '8 days', 0, true, 'cancelled')
+ , (52, 75, 'Jacint Violeta', current_date + interval '30 days', current_date + interval '33 days', 0, false, 'checked-in')
+ , (52, 76, 'Hortènsia Grisa', current_date + interval '29 days', current_date + interval '34 days', 0, false, 'invoiced')
+;
+
commit;
diff --git a/deploy/available_booking_status.sql b/deploy/available_booking_status.sql
new file mode 100644
index 0000000..89b638f
--- /dev/null
+++ b/deploy/available_booking_status.sql
@@ -0,0 +1,33 @@
+-- Deploy camper:available_booking_status to pg
+-- requires: booking_status
+-- requires: booking_status_i18n
+
+begin;
+
+insert into camper.booking_status (booking_status, name)
+values ('created', 'Created')
+ , ('cancelled', 'Cancelled')
+ , ('confirmed', 'Confirmed')
+ , ('checked-in', 'Checked in')
+ , ('invoiced', 'Invoiced')
+;
+
+insert into camper.booking_status_i18n (booking_status, lang_tag, name)
+values ('created', 'ca', 'Creada')
+ , ('cancelled', 'ca', 'Cancel·lada')
+ , ('confirmed', 'ca', 'Confirmada')
+ , ('checked-in', 'ca', 'Registrada')
+ , ('invoiced', 'ca', 'Facturada')
+ , ('created', 'es', 'Creada')
+ , ('cancelled', 'es', 'Cancelada')
+ , ('confirmed', 'es', 'Confirmada')
+ , ('checked-in', 'es', 'Registrada')
+ , ('invoiced', 'es', 'Facturada')
+ , ('created', 'fr', 'Créé')
+ , ('cancelled', 'fr', 'Annulé')
+ , ('confirmed', 'fr', 'Confirmé')
+ , ('checked-in', 'fr', 'Enregistré')
+ , ('invoiced', 'fr', 'Facturé')
+;
+
+commit;
diff --git a/deploy/booking.sql b/deploy/booking.sql
new file mode 100644
index 0000000..8920c19
--- /dev/null
+++ b/deploy/booking.sql
@@ -0,0 +1,64 @@
+-- Deploy camper:booking to pg
+-- requires: roles
+-- requires: schema_camper
+-- requires: company
+-- requires: user_profile
+-- requires: campsite_type
+-- requires: booking_status
+
+begin;
+
+set search_path to camper, public;
+
+create table booking (
+ booking_id integer generated by default as identity primary key,
+ company_id integer not null references company,
+ slug uuid not null unique default gen_random_uuid(),
+ campsite_type_id integer not null references campsite_type,
+ holder_name text not null constraint holder_name_not_empty check (length(trim(holder_name)) > 0),
+ arrival_date date not null,
+ departure_date date not null constraint departure_after_arrival check (departure_date > arrival_date),
+ number_dogs integer not null constraint number_dogs_nonnegative check (number_dogs >= 0),
+ acsi_card boolean not null,
+ booking_status text not null default 'created' references booking_status,
+ created_at timestamptz not null default current_timestamp
+);
+
+grant select, insert, update on table booking to employee;
+grant select, insert, update, delete on table booking to admin;
+
+alter table booking enable row level security;
+
+create policy select_from_company
+on booking
+for select
+using (
+ company_id in (select company_id from user_profile)
+)
+;
+
+create policy insert_to_company
+on booking
+for insert
+with check (
+ company_id in (select company_id from user_profile)
+)
+;
+
+create policy update_company
+on booking
+for update
+using (
+ company_id in (select company_id from user_profile)
+)
+;
+
+create policy delete_from_company
+on booking
+for delete
+using (
+ company_id in (select company_id from user_profile)
+)
+;
+
+commit;
diff --git a/deploy/booking_status.sql b/deploy/booking_status.sql
new file mode 100644
index 0000000..cb15c0c
--- /dev/null
+++ b/deploy/booking_status.sql
@@ -0,0 +1,17 @@
+-- Deploy camper:booking_status to pg
+-- requires: roles
+-- requires: schema_camper
+
+begin;
+
+set search_path to camper, public;
+
+create table booking_status (
+ booking_status text not null primary key,
+ name text not null
+);
+
+grant select on table booking_status to employee;
+grant select on table booking_status to admin;
+
+commit;
diff --git a/deploy/booking_status_i18n.sql b/deploy/booking_status_i18n.sql
new file mode 100644
index 0000000..863aa31
--- /dev/null
+++ b/deploy/booking_status_i18n.sql
@@ -0,0 +1,21 @@
+-- Deploy camper:booking_status_i18n to pg
+-- requires: roles
+-- requires: schema_camper
+-- requires: booking_status
+-- requires: language
+
+begin;
+
+set search_path to camper, public;
+
+create table booking_status_i18n (
+ booking_status text not null references booking_status,
+ lang_tag text not null references language,
+ name text not null,
+ primary key (booking_status, lang_tag)
+);
+
+grant select on table booking_status_i18n to employee;
+grant select on table booking_status_i18n to admin;
+
+commit;
diff --git a/pkg/app/admin.go b/pkg/app/admin.go
index 4c2a64b..3e4ef49 100644
--- a/pkg/app/admin.go
+++ b/pkg/app/admin.go
@@ -26,13 +26,13 @@ import (
)
type adminHandler struct {
+ booking *booking.AdminHandler
campsite *campsite.AdminHandler
company *company.AdminHandler
home *home.AdminHandler
legal *legal.AdminHandler
location *location.AdminHandler
media *media.AdminHandler
- payment *booking.AdminHandler
season *season.AdminHandler
services *services.AdminHandler
surroundings *surroundings.AdminHandler
@@ -41,13 +41,13 @@ type adminHandler struct {
func newAdminHandler(mediaDir string) *adminHandler {
return &adminHandler{
+ booking: booking.NewAdminHandler(),
campsite: campsite.NewAdminHandler(),
company: company.NewAdminHandler(),
home: home.NewAdminHandler(),
legal: legal.NewAdminHandler(),
location: location.NewAdminHandler(),
media: media.NewAdminHandler(mediaDir),
- payment: booking.NewAdminHandler(),
season: season.NewAdminHandler(),
services: services.NewAdminHandler(),
surroundings: surroundings.NewAdminHandler(),
@@ -71,6 +71,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
+ case "bookings":
+ h.booking.Handler(user, company, conn).ServeHTTP(w, r)
case "campsites":
h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
case "company":
@@ -83,8 +85,6 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
h.location.Handler(user, company, conn).ServeHTTP(w, r)
case "media":
h.media.Handler(user, company, conn).ServeHTTP(w, r)
- case "payment":
- h.payment.Handler(user, company, conn).ServeHTTP(w, r)
case "seasons":
h.season.Handler(user, company, conn).ServeHTTP(w, r)
case "services":
diff --git a/pkg/booking/admin.go b/pkg/booking/admin.go
index 4353e55..08621d1 100644
--- a/pkg/booking/admin.go
+++ b/pkg/booking/admin.go
@@ -7,7 +7,10 @@ package booking
import (
"context"
+ "golang.org/x/text/language"
"net/http"
+ "strings"
+ "time"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
@@ -33,17 +36,30 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
case "":
switch r.Method {
case http.MethodGet:
- f := newPaymentForm(user.Locale)
- if err := f.FillFromDatabase(r.Context(), company, conn); err != nil {
- if !database.ErrorIsNotFound(err) {
- panic(err)
- }
- }
- f.MustRender(w, r, user, company)
- case http.MethodPut:
- updatePaymentSettings(w, r, user, company, conn)
+ serveBookingIndex(w, r, user, company, conn)
default:
- httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
+ httplib.MethodNotAllowed(w, r, http.MethodGet)
+ }
+ case "payment":
+ head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
+ switch head {
+ case "":
+ switch r.Method {
+ case http.MethodGet:
+ f := newPaymentForm(user.Locale)
+ if err := f.FillFromDatabase(r.Context(), company, conn); err != nil {
+ if !database.ErrorIsNotFound(err) {
+ panic(err)
+ }
+ }
+ f.MustRender(w, r, user, company)
+ case http.MethodPut:
+ updatePaymentSettings(w, r, user, company, conn)
+ default:
+ httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
+ }
+ default:
+ http.NotFound(w, r)
}
default:
http.NotFound(w, r)
@@ -51,6 +67,91 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
})
}
+func serveBookingIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
+ bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language)
+ if err != nil {
+ panic(err)
+ }
+ page := bookingIndex(bookings)
+ page.MustRender(w, r, user, company)
+}
+
+func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag) ([]*bookingEntry, error) {
+ rows, err := conn.Query(ctx, `
+ select left(slug::text, 10)
+ , '/admin/booking/' || slug
+ , arrival_date
+ , departure_date
+ , holder_name
+ , booking.booking_status
+ , coalesce(i18n.name, status.name)
+ from booking
+ join booking_status as status using (booking_status)
+ left join booking_status_i18n as i18n on status.booking_status = i18n.booking_status and i18n.lang_tag = $1
+ order by arrival_date desc
+ `, lang)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var entries []*bookingEntry
+ for rows.Next() {
+ entry := &bookingEntry{}
+ if err = rows.Scan(&entry.Reference, &entry.URL, &entry.ArrivalDate, &entry.DepartureDate, &entry.HolderName, &entry.Status, &entry.StatusLabel); err != nil {
+ return nil, err
+ }
+ entries = append(entries, entry)
+ }
+
+ return entries, nil
+}
+
+type bookingEntry struct {
+ Reference string
+ URL string
+ ArrivalDate time.Time
+ DepartureDate time.Time
+ HolderName string
+ Status string
+ StatusLabel string
+}
+
+type bookingIndex []*bookingEntry
+
+func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
+ switch r.URL.Query().Get("format") {
+ case "ods":
+ columns := []string{
+ "Reference",
+ "Arrival Date",
+ "Departure Date",
+ "Holder Name",
+ "Status",
+ }
+ ods, err := writeTableOds(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
+ if err := writeCellString(sb, entry.Reference); err != nil {
+ return err
+ }
+ writeCellDate(sb, entry.ArrivalDate)
+ writeCellDate(sb, entry.DepartureDate)
+ if err := writeCellString(sb, entry.HolderName); err != nil {
+ return err
+ }
+ if err := writeCellString(sb, entry.StatusLabel); err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ panic(err)
+ }
+ mustWriteOdsResponse(w, ods, user.Locale.Pgettext("bookings.ods", "filename"))
+ default:
+ template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page)
+ }
+}
+
type paymentForm struct {
MerchantCode *form.Input
TerminalNumber *form.Input
@@ -116,7 +217,7 @@ func (f *paymentForm) FillFromDatabase(ctx context.Context, company *auth.Compan
}
func (f *paymentForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
- template.MustRenderAdmin(w, r, user, company, "payment.gohtml", f)
+ template.MustRenderAdmin(w, r, user, company, "booking/payment.gohtml", f)
}
func (f *paymentForm) Parse(r *http.Request) error {
@@ -169,5 +270,5 @@ func updatePaymentSettings(w http.ResponseWriter, r *http.Request, user *auth.Us
if err := conn.SetupRedsys(r.Context(), company.ID, f.MerchantCode.Val, f.TerminalNumber.Int(), f.Environment.Selected[0], f.Integration.Selected[0], f.EncryptKey.Val); err != nil {
panic(err)
}
- httplib.Redirect(w, r, "/admin/payment", http.StatusSeeOther)
+ httplib.Redirect(w, r, "/admin/booking/payment", http.StatusSeeOther)
}
diff --git a/pkg/booking/ods.go b/pkg/booking/ods.go
new file mode 100644
index 0000000..1be8055
--- /dev/null
+++ b/pkg/booking/ods.go
@@ -0,0 +1,170 @@
+package booking
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "dev.tandem.ws/tandem/camper/pkg/locale"
+)
+
+const (
+ mimetype = "application/vnd.oasis.opendocument.spreadsheet"
+ metaDashInfManifestXml = `
+
+
+
+
+
+
+`
+ metaXml = `
+
+
+
+ Camper
+
+
+`
+ stylesXml = `
+
+
+`
+)
+
+func writeTableOds[K interface{}](rows []*K, columns []string, locale *locale.Locale, writeRow func(*strings.Builder, *K) error) ([]byte, error) {
+ var sb strings.Builder
+
+ sb.WriteString(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /
+
+ /
+
+
+
+
+
+
+
+
+`)
+ sb.WriteString(fmt.Sprintf(" \n", len(columns)))
+ sb.WriteString(`
+`)
+ for _, t := range columns {
+ if err := writeCellString(&sb, locale.GetC(t, "header")); err != nil {
+ return nil, err
+ }
+ }
+ sb.WriteString(" \n")
+ for _, row := range rows {
+ sb.WriteString(" \n")
+ if err := writeRow(&sb, row); err != nil {
+ return nil, err
+ }
+ sb.WriteString(" \n")
+ }
+ sb.WriteString(`
+
+
+
+
+`)
+ return writeOds(sb.String())
+}
+
+func writeOds(content string) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ ods := zip.NewWriter(buf)
+ if err := writeOdsFile(ods, "mimetype", mimetype, zip.Store); err != nil {
+ return nil, err
+ }
+ if err := writeOdsFile(ods, "META-INF/manifest.xml", metaDashInfManifestXml, zip.Deflate); err != nil {
+ return nil, err
+ }
+ if err := writeOdsFile(ods, "meta.xml", metaXml, zip.Deflate); err != nil {
+ return nil, err
+ }
+ if err := writeOdsFile(ods, "styles.xml", stylesXml, zip.Deflate); err != nil {
+ return nil, err
+ }
+ if err := writeOdsFile(ods, "content.xml", content, zip.Deflate); err != nil {
+ return nil, err
+ }
+ if err := ods.Close(); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func writeOdsFile(ods *zip.Writer, name string, content string, method uint16) error {
+ f, err := ods.CreateHeader(&zip.FileHeader{
+ Name: name,
+ Method: method,
+ Modified: time.Now(),
+ })
+ if err != nil {
+ return err
+ }
+ _, err = f.Write([]byte(content))
+ return err
+}
+
+func writeCellString(sb *strings.Builder, s string) error {
+ sb.WriteString(` `)
+ if err := xml.EscapeText(sb, []byte(s)); err != nil {
+ return err
+ }
+ sb.WriteString(" \n")
+ return nil
+}
+
+func writeCellDate(sb *strings.Builder, t time.Time) {
+ sb.WriteString(fmt.Sprintf(" %s \n", t.Format("2006-01-02"), t.Format("02/01/06")))
+}
+
+func mustWriteOdsResponse(w http.ResponseWriter, ods []byte, filename string) {
+ w.Header().Set("Content-Type", "application/vnd.oasis.opendocument.spreadsheet")
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write(ods); err != nil {
+ panic(err)
+ }
+}
diff --git a/pkg/template/render.go b/pkg/template/render.go
index 98b8541..cd5a450 100644
--- a/pkg/template/render.go
+++ b/pkg/template/render.go
@@ -99,6 +99,9 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
"formatPrice": func(price string) string {
return formatPrice(price, user.Locale.Language, user.Locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol)
},
+ "formatDate": func(time time.Time) template.HTML {
+ return template.HTML(`` + time.Format("02/01/2006") + " ")
+ },
"queryEscape": func(s string) string {
return url.QueryEscape(s)
},
diff --git a/po/ca.po b/po/ca.po
index 74a07a2..38eedd8 100644
--- a/po/ca.po
+++ b/po/ca.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
-"POT-Creation-Date: 2024-01-18 15:25+0100\n"
+"POT-Creation-Date: 2024-01-18 20:54+0100\n"
"PO-Revision-Date: 2023-07-22 23:45+0200\n"
"Last-Translator: jordi fita mas \n"
"Language-Team: Catalan \n"
@@ -406,7 +406,7 @@ msgstr "Menú"
#: web/templates/public/layout.gohtml:58 web/templates/public/layout.gohtml:104
#: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12
-#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:89
+#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:92
msgctxt "title"
msgid "Campsites"
msgstr "Allotjaments"
@@ -553,9 +553,9 @@ msgctxt "input"
msgid "Map Embed"
msgstr "Incrustació del mapa"
-#: web/templates/admin/location.gohtml:60 web/templates/admin/payment.gohtml:62
-#: web/templates/admin/profile.gohtml:75
+#: web/templates/admin/location.gohtml:60 web/templates/admin/profile.gohtml:75
#: web/templates/admin/taxDetails.gohtml:167
+#: web/templates/admin/booking/payment.gohtml:62
msgctxt "action"
msgid "Save changes"
msgstr "Desa els canvis"
@@ -933,42 +933,6 @@ msgctxt "action"
msgid "Cancel"
msgstr "Canceŀla"
-#: web/templates/admin/payment.gohtml:6 web/templates/admin/payment.gohtml:12
-#: web/templates/admin/layout.gohtml:40
-msgctxt "title"
-msgid "Payment Settings"
-msgstr "Paràmetres de pagament"
-
-#: web/templates/admin/payment.gohtml:17
-msgctxt "input"
-msgid "Merchant Code"
-msgstr "Codi del comerç"
-
-#: web/templates/admin/payment.gohtml:26
-msgctxt "input"
-msgid "Terminal Number"
-msgstr "Número de terminal"
-
-#: web/templates/admin/payment.gohtml:36
-msgctxt "input"
-msgid "Merchant Key (only if must change it)"
-msgstr "Clau del comerç (només si s’ha de canviar)"
-
-#: web/templates/admin/payment.gohtml:38
-msgctxt "input"
-msgid "Merchant Key"
-msgstr "Clau del comerç"
-
-#: web/templates/admin/payment.gohtml:48
-msgctxt "title"
-msgid "Environment"
-msgstr "Entorn"
-
-#: web/templates/admin/payment.gohtml:55
-msgctxt "title"
-msgid "Integration"
-msgstr "Integració"
-
#: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:86
msgctxt "title"
@@ -1243,6 +1207,13 @@ msgctxt "title"
msgid "Company Settings"
msgstr "Paràmetres de l’empresa"
+#: web/templates/admin/layout.gohtml:40
+#: web/templates/admin/booking/payment.gohtml:6
+#: web/templates/admin/booking/payment.gohtml:12
+msgctxt "title"
+msgid "Payment Settings"
+msgstr "Paràmetres de pagament"
+
#: web/templates/admin/layout.gohtml:52
#: web/templates/admin/media/index.gohtml:6
#: web/templates/admin/media/index.gohtml:11
@@ -1260,6 +1231,13 @@ msgctxt "action"
msgid "Logout"
msgstr "Surt"
+#: web/templates/admin/layout.gohtml:89
+#: web/templates/admin/booking/index.gohtml:6
+#: web/templates/admin/booking/index.gohtml:13
+msgctxt "title"
+msgid "Bookings"
+msgstr "Reserves"
+
#: web/templates/admin/home/index.gohtml:11
msgctxt "title"
msgid "Cover"
@@ -1343,6 +1321,75 @@ msgctxt "title"
msgid "Upload Media"
msgstr "Pujada de mèdia"
+#: web/templates/admin/booking/payment.gohtml:17
+msgctxt "input"
+msgid "Merchant Code"
+msgstr "Codi del comerç"
+
+#: web/templates/admin/booking/payment.gohtml:26
+msgctxt "input"
+msgid "Terminal Number"
+msgstr "Número de terminal"
+
+#: web/templates/admin/booking/payment.gohtml:36
+msgctxt "input"
+msgid "Merchant Key (only if must change it)"
+msgstr "Clau del comerç (només si s’ha de canviar)"
+
+#: web/templates/admin/booking/payment.gohtml:38
+msgctxt "input"
+msgid "Merchant Key"
+msgstr "Clau del comerç"
+
+#: web/templates/admin/booking/payment.gohtml:48
+msgctxt "title"
+msgid "Environment"
+msgstr "Entorn"
+
+#: web/templates/admin/booking/payment.gohtml:55
+msgctxt "title"
+msgid "Integration"
+msgstr "Integració"
+
+#: web/templates/admin/booking/index.gohtml:11
+msgctxt "action"
+msgid "Add Booking"
+msgstr "Afegeix reserva"
+
+#: web/templates/admin/booking/index.gohtml:12
+msgctxt "action"
+msgid "Export Bookings"
+msgstr "Exporta reserves"
+
+#: web/templates/admin/booking/index.gohtml:18
+msgctxt "header"
+msgid "Reference"
+msgstr "Referència"
+
+#: web/templates/admin/booking/index.gohtml:19
+msgctxt "header"
+msgid "Arrival Date"
+msgstr "Data d’arribada"
+
+#: web/templates/admin/booking/index.gohtml:20
+msgctxt "header"
+msgid "Departure Date"
+msgstr "Data de sortida"
+
+#: web/templates/admin/booking/index.gohtml:21
+msgctxt "header"
+msgid "Holder Name"
+msgstr "Nom del titular"
+
+#: web/templates/admin/booking/index.gohtml:22
+msgctxt "header"
+msgid "Status"
+msgstr "Estat"
+
+#: web/templates/admin/booking/index.gohtml:38
+msgid "No booking found."
+msgstr "No s’ha trobat cap reserva."
+
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:344
#: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:463
#: pkg/season/admin.go:412 pkg/services/admin.go:316
@@ -1706,55 +1753,60 @@ msgstr "No podeu deixar el fitxer del mèdia en blanc."
msgid "Filename can not be empty."
msgstr "No podeu deixar el nom del fitxer en blanc."
-#: pkg/booking/admin.go:76
+#: pkg/booking/admin.go:148
+msgctxt "filename"
+msgid "bookings.ods"
+msgstr "reserves.ods"
+
+#: pkg/booking/admin.go:176
msgctxt "redsys environment"
msgid "Test"
msgstr "Proves"
-#: pkg/booking/admin.go:80
+#: pkg/booking/admin.go:180
msgctxt "redsys environment"
msgid "Live"
msgstr "Real"
-#: pkg/booking/admin.go:89
+#: pkg/booking/admin.go:189
msgctxt "redsys integration"
msgid "InSite"
msgstr "InSite"
-#: pkg/booking/admin.go:93
+#: pkg/booking/admin.go:193
msgctxt "redsys integration"
msgid "Redirect"
msgstr "Redirecció"
-#: pkg/booking/admin.go:137
+#: pkg/booking/admin.go:237
msgid "Merchant code can not be empty."
msgstr "No podeu deixar el codi del comerç en blanc."
-#: pkg/booking/admin.go:138
+#: pkg/booking/admin.go:238
msgid "Merchant code must be exactly nine digits long."
msgstr "El codi del comerç ha de ser de nou dígits."
-#: pkg/booking/admin.go:139
+#: pkg/booking/admin.go:239
msgid "Merchant code must be a number."
msgstr "El codi del comerç."
-#: pkg/booking/admin.go:143
+#: pkg/booking/admin.go:243
msgid "Terminal number can not be empty."
msgstr "No podeu deixar el número del terminal en blanc."
-#: pkg/booking/admin.go:144
+#: pkg/booking/admin.go:244
msgid "Terminal number must be a number between 1 and 999."
msgstr "El número del terminal ha de ser entre 1 i 999"
-#: pkg/booking/admin.go:152
+#: pkg/booking/admin.go:252
msgid "Selected environment is not valid."
msgstr "L’entorn escollit no és vàlid."
-#: pkg/booking/admin.go:153
+#: pkg/booking/admin.go:253
msgid "Selected integration is not valid."
msgstr "La integració escollida no és vàlida."
-#: pkg/booking/admin.go:156
+#: pkg/booking/admin.go:256
msgid "The merchant key is not valid."
msgstr "Aquesta clau del comerç no és vàlid."
diff --git a/po/es.po b/po/es.po
index 5f4b060..fac6062 100644
--- a/po/es.po
+++ b/po/es.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
-"POT-Creation-Date: 2024-01-18 15:25+0100\n"
+"POT-Creation-Date: 2024-01-18 20:54+0100\n"
"PO-Revision-Date: 2023-07-22 23:46+0200\n"
"Last-Translator: jordi fita mas \n"
"Language-Team: Spanish \n"
@@ -406,7 +406,7 @@ msgstr "Menú"
#: web/templates/public/layout.gohtml:58 web/templates/public/layout.gohtml:104
#: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12
-#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:89
+#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:92
msgctxt "title"
msgid "Campsites"
msgstr "Alojamientos"
@@ -553,9 +553,9 @@ msgctxt "input"
msgid "Map Embed"
msgstr "Incrustación del mapa"
-#: web/templates/admin/location.gohtml:60 web/templates/admin/payment.gohtml:62
-#: web/templates/admin/profile.gohtml:75
+#: web/templates/admin/location.gohtml:60 web/templates/admin/profile.gohtml:75
#: web/templates/admin/taxDetails.gohtml:167
+#: web/templates/admin/booking/payment.gohtml:62
msgctxt "action"
msgid "Save changes"
msgstr "Guardar los cambios"
@@ -933,42 +933,6 @@ msgctxt "action"
msgid "Cancel"
msgstr "Cancelar"
-#: web/templates/admin/payment.gohtml:6 web/templates/admin/payment.gohtml:12
-#: web/templates/admin/layout.gohtml:40
-msgctxt "title"
-msgid "Payment Settings"
-msgstr "Parámetros de pago"
-
-#: web/templates/admin/payment.gohtml:17
-msgctxt "input"
-msgid "Merchant Code"
-msgstr "Código del comercio"
-
-#: web/templates/admin/payment.gohtml:26
-msgctxt "input"
-msgid "Terminal Number"
-msgstr "Número de terminal"
-
-#: web/templates/admin/payment.gohtml:36
-msgctxt "input"
-msgid "Merchant Key (only if must change it)"
-msgstr "Clave del comercio (sólo si se debe cambiar)"
-
-#: web/templates/admin/payment.gohtml:38
-msgctxt "input"
-msgid "Merchant Key"
-msgstr "Clave del comercio"
-
-#: web/templates/admin/payment.gohtml:48
-msgctxt "title"
-msgid "Environment"
-msgstr "Entorno"
-
-#: web/templates/admin/payment.gohtml:55
-msgctxt "title"
-msgid "Integration"
-msgstr "Integración"
-
#: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:86
msgctxt "title"
@@ -1243,6 +1207,13 @@ msgctxt "title"
msgid "Company Settings"
msgstr "Parámetros de la empresa"
+#: web/templates/admin/layout.gohtml:40
+#: web/templates/admin/booking/payment.gohtml:6
+#: web/templates/admin/booking/payment.gohtml:12
+msgctxt "title"
+msgid "Payment Settings"
+msgstr "Parámetros de pago"
+
#: web/templates/admin/layout.gohtml:52
#: web/templates/admin/media/index.gohtml:6
#: web/templates/admin/media/index.gohtml:11
@@ -1260,6 +1231,13 @@ msgctxt "action"
msgid "Logout"
msgstr "Salir"
+#: web/templates/admin/layout.gohtml:89
+#: web/templates/admin/booking/index.gohtml:6
+#: web/templates/admin/booking/index.gohtml:13
+msgctxt "title"
+msgid "Bookings"
+msgstr "Reservas"
+
#: web/templates/admin/home/index.gohtml:11
msgctxt "title"
msgid "Cover"
@@ -1343,6 +1321,75 @@ msgctxt "title"
msgid "Upload Media"
msgstr "Subida de medio"
+#: web/templates/admin/booking/payment.gohtml:17
+msgctxt "input"
+msgid "Merchant Code"
+msgstr "Código del comercio"
+
+#: web/templates/admin/booking/payment.gohtml:26
+msgctxt "input"
+msgid "Terminal Number"
+msgstr "Número de terminal"
+
+#: web/templates/admin/booking/payment.gohtml:36
+msgctxt "input"
+msgid "Merchant Key (only if must change it)"
+msgstr "Clave del comercio (sólo si se debe cambiar)"
+
+#: web/templates/admin/booking/payment.gohtml:38
+msgctxt "input"
+msgid "Merchant Key"
+msgstr "Clave del comercio"
+
+#: web/templates/admin/booking/payment.gohtml:48
+msgctxt "title"
+msgid "Environment"
+msgstr "Entorno"
+
+#: web/templates/admin/booking/payment.gohtml:55
+msgctxt "title"
+msgid "Integration"
+msgstr "Integración"
+
+#: web/templates/admin/booking/index.gohtml:11
+msgctxt "action"
+msgid "Add Booking"
+msgstr "Añadir reserva"
+
+#: web/templates/admin/booking/index.gohtml:12
+msgctxt "action"
+msgid "Export Bookings"
+msgstr "Exportar eservas"
+
+#: web/templates/admin/booking/index.gohtml:18
+msgctxt "header"
+msgid "Reference"
+msgstr "Referencia"
+
+#: web/templates/admin/booking/index.gohtml:19
+msgctxt "header"
+msgid "Arrival Date"
+msgstr "Fecha de llegada"
+
+#: web/templates/admin/booking/index.gohtml:20
+msgctxt "header"
+msgid "Departure Date"
+msgstr "Fecha de salida"
+
+#: web/templates/admin/booking/index.gohtml:21
+msgctxt "header"
+msgid "Holder Name"
+msgstr "Nombre del titular"
+
+#: web/templates/admin/booking/index.gohtml:22
+msgctxt "header"
+msgid "Status"
+msgstr "Estado"
+
+#: web/templates/admin/booking/index.gohtml:38
+msgid "No booking found."
+msgstr "No se ha encontrado ninguna reserva."
+
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:344
#: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:463
#: pkg/season/admin.go:412 pkg/services/admin.go:316
@@ -1706,55 +1753,60 @@ msgstr "No podéis dejar el archivo del medio en blanco."
msgid "Filename can not be empty."
msgstr "No podéis dejar el nombre del archivo en blanco."
-#: pkg/booking/admin.go:76
+#: pkg/booking/admin.go:148
+msgctxt "filename"
+msgid "bookings.ods"
+msgstr "reservas.ods"
+
+#: pkg/booking/admin.go:176
msgctxt "redsys environment"
msgid "Test"
msgstr "Pruebas"
-#: pkg/booking/admin.go:80
+#: pkg/booking/admin.go:180
msgctxt "redsys environment"
msgid "Live"
msgstr "Real"
-#: pkg/booking/admin.go:89
+#: pkg/booking/admin.go:189
msgctxt "redsys integration"
msgid "InSite"
msgstr "InSite"
-#: pkg/booking/admin.go:93
+#: pkg/booking/admin.go:193
msgctxt "redsys integration"
msgid "Redirect"
msgstr "Redirección"
-#: pkg/booking/admin.go:137
+#: pkg/booking/admin.go:237
msgid "Merchant code can not be empty."
msgstr "No podéis dejar el código del comercio en blanco."
-#: pkg/booking/admin.go:138
+#: pkg/booking/admin.go:238
msgid "Merchant code must be exactly nine digits long."
msgstr "El código del comercio tiene que ser de nueve dígitos."
-#: pkg/booking/admin.go:139
+#: pkg/booking/admin.go:239
msgid "Merchant code must be a number."
msgstr "El código del comercio tiene que ser un número."
-#: pkg/booking/admin.go:143
+#: pkg/booking/admin.go:243
msgid "Terminal number can not be empty."
msgstr "No podéis dejar el número de terminal en blanco."
-#: pkg/booking/admin.go:144
+#: pkg/booking/admin.go:244
msgid "Terminal number must be a number between 1 and 999."
msgstr "El número de terminal tiene que ser entre 1 y 999."
-#: pkg/booking/admin.go:152
+#: pkg/booking/admin.go:252
msgid "Selected environment is not valid."
msgstr "El entorno escogido no es válido."
-#: pkg/booking/admin.go:153
+#: pkg/booking/admin.go:253
msgid "Selected integration is not valid."
msgstr "La integración escogida no es válida."
-#: pkg/booking/admin.go:156
+#: pkg/booking/admin.go:256
msgid "The merchant key is not valid."
msgstr "Esta clave del comercio no es válida."
diff --git a/po/fr.po b/po/fr.po
index 2b25ca9..2607a74 100644
--- a/po/fr.po
+++ b/po/fr.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
-"POT-Creation-Date: 2024-01-18 15:25+0100\n"
+"POT-Creation-Date: 2024-01-18 20:54+0100\n"
"PO-Revision-Date: 2023-12-20 10:13+0100\n"
"Last-Translator: Oriol Carbonell \n"
"Language-Team: French \n"
@@ -306,7 +306,7 @@ msgstr "Camping"
#: web/templates/public/layout.gohtml:70
msgctxt "title"
msgid "Booking"
-msgstr "Reservation"
+msgstr "Réservation"
#: web/templates/public/booking.gohtml:16
msgctxt "title"
@@ -407,7 +407,7 @@ msgstr "Menu"
#: web/templates/public/layout.gohtml:58 web/templates/public/layout.gohtml:104
#: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12
-#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:89
+#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:92
msgctxt "title"
msgid "Campsites"
msgstr "Locatifs"
@@ -554,9 +554,9 @@ msgctxt "input"
msgid "Map Embed"
msgstr "Carte intégrée"
-#: web/templates/admin/location.gohtml:60 web/templates/admin/payment.gohtml:62
-#: web/templates/admin/profile.gohtml:75
+#: web/templates/admin/location.gohtml:60 web/templates/admin/profile.gohtml:75
#: web/templates/admin/taxDetails.gohtml:167
+#: web/templates/admin/booking/payment.gohtml:62
msgctxt "action"
msgid "Save changes"
msgstr "Enregistrer les changements"
@@ -934,42 +934,6 @@ msgctxt "action"
msgid "Cancel"
msgstr "Annuler"
-#: web/templates/admin/payment.gohtml:6 web/templates/admin/payment.gohtml:12
-#: web/templates/admin/layout.gohtml:40
-msgctxt "title"
-msgid "Payment Settings"
-msgstr "Paramètres de paiement"
-
-#: web/templates/admin/payment.gohtml:17
-msgctxt "input"
-msgid "Merchant Code"
-msgstr "Code Marchant"
-
-#: web/templates/admin/payment.gohtml:26
-msgctxt "input"
-msgid "Terminal Number"
-msgstr "Numéro de terminal"
-
-#: web/templates/admin/payment.gohtml:36
-msgctxt "input"
-msgid "Merchant Key (only if must change it)"
-msgstr "Clé marchande (uniquement si vous devez la changer)"
-
-#: web/templates/admin/payment.gohtml:38
-msgctxt "input"
-msgid "Merchant Key"
-msgstr "Clé Marchant"
-
-#: web/templates/admin/payment.gohtml:48
-msgctxt "title"
-msgid "Environment"
-msgstr "Environnement"
-
-#: web/templates/admin/payment.gohtml:55
-msgctxt "title"
-msgid "Integration"
-msgstr "Intégration"
-
#: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:86
msgctxt "title"
@@ -1244,6 +1208,13 @@ msgctxt "title"
msgid "Company Settings"
msgstr "Paramètres de l'entreprise"
+#: web/templates/admin/layout.gohtml:40
+#: web/templates/admin/booking/payment.gohtml:6
+#: web/templates/admin/booking/payment.gohtml:12
+msgctxt "title"
+msgid "Payment Settings"
+msgstr "Paramètres de paiement"
+
#: web/templates/admin/layout.gohtml:52
#: web/templates/admin/media/index.gohtml:6
#: web/templates/admin/media/index.gohtml:11
@@ -1261,6 +1232,13 @@ msgctxt "action"
msgid "Logout"
msgstr "Déconnexion"
+#: web/templates/admin/layout.gohtml:89
+#: web/templates/admin/booking/index.gohtml:6
+#: web/templates/admin/booking/index.gohtml:13
+msgctxt "title"
+msgid "Bookings"
+msgstr "Réservations"
+
#: web/templates/admin/home/index.gohtml:11
msgctxt "title"
msgid "Cover"
@@ -1344,6 +1322,75 @@ msgctxt "title"
msgid "Upload Media"
msgstr "Envoyer un fichier"
+#: web/templates/admin/booking/payment.gohtml:17
+msgctxt "input"
+msgid "Merchant Code"
+msgstr "Code Marchant"
+
+#: web/templates/admin/booking/payment.gohtml:26
+msgctxt "input"
+msgid "Terminal Number"
+msgstr "Numéro de terminal"
+
+#: web/templates/admin/booking/payment.gohtml:36
+msgctxt "input"
+msgid "Merchant Key (only if must change it)"
+msgstr "Clé marchande (uniquement si vous devez la changer)"
+
+#: web/templates/admin/booking/payment.gohtml:38
+msgctxt "input"
+msgid "Merchant Key"
+msgstr "Clé Marchant"
+
+#: web/templates/admin/booking/payment.gohtml:48
+msgctxt "title"
+msgid "Environment"
+msgstr "Environnement"
+
+#: web/templates/admin/booking/payment.gohtml:55
+msgctxt "title"
+msgid "Integration"
+msgstr "Intégration"
+
+#: web/templates/admin/booking/index.gohtml:11
+msgctxt "action"
+msgid "Add Booking"
+msgstr "Ajouter une réservation"
+
+#: web/templates/admin/booking/index.gohtml:12
+msgctxt "action"
+msgid "Export Bookings"
+msgstr "Exporter les réservations"
+
+#: web/templates/admin/booking/index.gohtml:18
+msgctxt "header"
+msgid "Reference"
+msgstr "Référence"
+
+#: web/templates/admin/booking/index.gohtml:19
+msgctxt "header"
+msgid "Arrival Date"
+msgstr "Date d’arrivée"
+
+#: web/templates/admin/booking/index.gohtml:20
+msgctxt "header"
+msgid "Departure Date"
+msgstr "Date de depart"
+
+#: web/templates/admin/booking/index.gohtml:21
+msgctxt "header"
+msgid "Holder Name"
+msgstr "Nom du titulaire"
+
+#: web/templates/admin/booking/index.gohtml:22
+msgctxt "header"
+msgid "Status"
+msgstr "Statut"
+
+#: web/templates/admin/booking/index.gohtml:38
+msgid "No booking found."
+msgstr "Aucune réservation trouvée."
+
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:344
#: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:463
#: pkg/season/admin.go:412 pkg/services/admin.go:316
@@ -1707,55 +1754,60 @@ msgstr "Le fichier téléchargé ne peut pas être vide."
msgid "Filename can not be empty."
msgstr "Le nom de fichier ne peut pas être vide."
-#: pkg/booking/admin.go:76
+#: pkg/booking/admin.go:148
+msgctxt "filename"
+msgid "bookings.ods"
+msgstr "reservations.ods"
+
+#: pkg/booking/admin.go:176
msgctxt "redsys environment"
msgid "Test"
msgstr "Test"
-#: pkg/booking/admin.go:80
+#: pkg/booking/admin.go:180
msgctxt "redsys environment"
msgid "Live"
msgstr "Live"
-#: pkg/booking/admin.go:89
+#: pkg/booking/admin.go:189
msgctxt "redsys integration"
msgid "InSite"
msgstr "Insite"
-#: pkg/booking/admin.go:93
+#: pkg/booking/admin.go:193
msgctxt "redsys integration"
msgid "Redirect"
msgstr "Redirection"
-#: pkg/booking/admin.go:137
+#: pkg/booking/admin.go:237
msgid "Merchant code can not be empty."
msgstr "Le code marchand ne peut pas être vide."
-#: pkg/booking/admin.go:138
+#: pkg/booking/admin.go:238
msgid "Merchant code must be exactly nine digits long."
msgstr "Le code marchand doit comporter exactement neuf chiffres."
-#: pkg/booking/admin.go:139
+#: pkg/booking/admin.go:239
msgid "Merchant code must be a number."
msgstr "Le code du commerçant doit être un chiffre."
-#: pkg/booking/admin.go:143
+#: pkg/booking/admin.go:243
msgid "Terminal number can not be empty."
msgstr "Le numéro de terminal ne peut pas être vide."
-#: pkg/booking/admin.go:144
+#: pkg/booking/admin.go:244
msgid "Terminal number must be a number between 1 and 999."
msgstr "Le numéro de terminal doit être compris entre 1 et 999."
-#: pkg/booking/admin.go:152
+#: pkg/booking/admin.go:252
msgid "Selected environment is not valid."
msgstr "L’environnement sélectionné n’est pas valide."
-#: pkg/booking/admin.go:153
+#: pkg/booking/admin.go:253
msgid "Selected integration is not valid."
msgstr "L’intégration sélectionnée n’est pas valide."
-#: pkg/booking/admin.go:156
+#: pkg/booking/admin.go:256
msgid "The merchant key is not valid."
msgstr "La clé marchand n’est pas valide."
diff --git a/revert/available_booking_status.sql b/revert/available_booking_status.sql
new file mode 100644
index 0000000..32a0e08
--- /dev/null
+++ b/revert/available_booking_status.sql
@@ -0,0 +1,13 @@
+-- Revert camper:available_booking_status from pg
+
+begin;
+
+delete
+from camper.booking_status_i18n
+;
+
+delete
+from camper.booking_status
+;
+
+commit;
diff --git a/revert/booking.sql b/revert/booking.sql
new file mode 100644
index 0000000..33621c1
--- /dev/null
+++ b/revert/booking.sql
@@ -0,0 +1,7 @@
+-- Revert camper:booking from pg
+
+begin;
+
+drop table if exists camper.booking;
+
+commit;
diff --git a/revert/booking_status.sql b/revert/booking_status.sql
new file mode 100644
index 0000000..8d76cbf
--- /dev/null
+++ b/revert/booking_status.sql
@@ -0,0 +1,7 @@
+-- Revert camper:booking_status from pg
+
+begin;
+
+drop table if exists camper.booking_status;
+
+commit;
diff --git a/revert/booking_status_i18n.sql b/revert/booking_status_i18n.sql
new file mode 100644
index 0000000..98e7072
--- /dev/null
+++ b/revert/booking_status_i18n.sql
@@ -0,0 +1,7 @@
+-- Revert camper:booking_status_i18n from pg
+
+begin;
+
+drop table if exists camper.booking_status_i18n;
+
+commit;
diff --git a/sqitch.plan b/sqitch.plan
index 7827ce3..7bfd865 100644
--- a/sqitch.plan
+++ b/sqitch.plan
@@ -156,3 +156,7 @@ remove_cover_carousel_slide [roles schema_camper cover_carousel cover_carousel_i
order_cover_carousel [schema_camper roles cover_carousel] 2024-01-16T18:40:12Z jordi fita mas # Add function to order cover carousel
company_user_profile [roles schema_camper user company_user current_company_id] 2024-01-17T17:37:19Z jordi fita mas # Add view to list users for admins
company_login_attempt [roles schema_camper login_attempt user company_user current_company_id] 2024-01-17T19:02:26Z jordi fita mas # Add view to see login attempts for current company
+booking_status [roles schema_camper] 2024-01-18T14:34:53Z jordi fita mas # Add relation of booking statuses
+booking_status_i18n [roles schema_camper booking_status language] 2024-01-18T14:39:49Z jordi fita mas # Add relation of booking status’ translations
+available_booking_status [booking_status booking_status_i18n] 2024-01-18T14:45:37Z jordi fita mas # Add the list of available booking statuses
+booking [roles schema_camper company user_profile campsite_type booking_status] 2024-01-18T16:48:07Z jordi fita mas # Booking relation
diff --git a/test/booking.sql b/test/booking.sql
new file mode 100644
index 0000000..9ebcb31
--- /dev/null
+++ b/test/booking.sql
@@ -0,0 +1,276 @@
+-- Test booking
+set client_min_messages to warning;
+create extension if not exists pgtap;
+reset client_min_messages;
+
+begin;
+
+select plan(79);
+
+set search_path to camper, public;
+
+select has_table('booking');
+select has_pk('booking');
+select table_privs_are('booking', 'guest', array[]::text[]);
+select table_privs_are('booking', 'employee', array['SELECT', 'INSERT', 'UPDATE']);
+select table_privs_are('booking', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
+select table_privs_are('booking', 'authenticator', array[]::text[]);
+
+select has_column('booking', 'booking_id');
+select col_is_pk('booking', 'booking_id');
+select col_type_is('booking', 'booking_id', 'integer');
+select col_not_null('booking', 'booking_id');
+select col_hasnt_default('booking', 'booking_id');
+
+select has_column('booking', 'company_id');
+select col_is_fk('booking', 'company_id');
+select fk_ok('booking', 'company_id', 'company', 'company_id');
+select col_type_is('booking', 'company_id', 'integer');
+select col_not_null('booking', 'company_id');
+select col_hasnt_default('booking', 'company_id');
+
+select has_column('booking', 'slug');
+select col_is_unique('booking', 'slug');
+select col_type_is('booking', 'slug', 'uuid');
+select col_not_null('booking', 'slug');
+select col_has_default('booking', 'slug');
+select col_default_is('booking', 'slug', 'gen_random_uuid()');
+
+select has_column('booking', 'campsite_type_id');
+select col_is_fk('booking', 'campsite_type_id');
+select fk_ok('booking', 'campsite_type_id', 'campsite_type', 'campsite_type_id');
+select col_type_is('booking', 'campsite_type_id', 'integer');
+select col_not_null('booking', 'campsite_type_id');
+select col_hasnt_default('booking', 'campsite_type_id');
+
+select has_column('booking', 'holder_name');
+select col_type_is('booking', 'holder_name', 'text');
+select col_not_null('booking', 'holder_name');
+select col_hasnt_default('booking', 'holder_name');
+
+select has_column('booking', 'arrival_date');
+select col_type_is('booking', 'arrival_date', 'date');
+select col_not_null('booking', 'arrival_date');
+select col_hasnt_default('booking', 'arrival_date');
+
+select has_column('booking', 'departure_date');
+select col_type_is('booking', 'departure_date', 'date');
+select col_not_null('booking', 'departure_date');
+select col_hasnt_default('booking', 'departure_date');
+
+select has_column('booking', 'number_dogs');
+select col_type_is('booking', 'number_dogs', 'integer');
+select col_not_null('booking', 'number_dogs');
+select col_hasnt_default('booking', 'number_dogs');
+
+select has_column('booking', 'acsi_card');
+select col_type_is('booking', 'acsi_card', 'boolean');
+select col_not_null('booking', 'acsi_card');
+select col_hasnt_default('booking', 'acsi_card');
+
+select has_column('booking', 'booking_status');
+select col_is_fk('booking', 'booking_status');
+select fk_ok('booking', 'booking_status', 'booking_status', 'booking_status');
+select col_type_is('booking', 'booking_status', 'text');
+select col_not_null('booking', 'booking_status');
+select col_has_default('booking', 'booking_status');
+select col_default_is('booking', 'booking_status', 'created');
+
+select has_column('booking', 'created_at');
+select col_type_is('booking', 'created_at', 'timestamp with time zone');
+select col_not_null('booking', 'created_at');
+select col_has_default('booking', 'created_at');
+select col_default_is('booking', 'created_at', 'CURRENT_TIMESTAMP');
+
+
+set client_min_messages to warning;
+truncate booking cascade;
+truncate campsite_type cascade;
+truncate media cascade;
+truncate media_content cascade;
+truncate company_host cascade;
+truncate company_user cascade;
+truncate company cascade;
+truncate auth."user" cascade;
+reset client_min_messages;
+
+insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
+values (1, 'employee2@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
+ , (3, 'admin2@tandem.blog', 'Admin', 'test', '6d8215c4888ffac017c3e4b8438e9a1a5559decd719df9c790', current_timestamp + interval '1 month')
+ , (5, 'employee4@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
+;
+
+insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag)
+values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca')
+ , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 'FR', 'USD', 'ca')
+;
+
+insert into company_user (company_id, user_id, role)
+values (2, 1, 'employee')
+ , (2, 3, 'admin')
+ , (4, 5, 'employee')
+;
+
+insert into company_host (company_id, host)
+values (2, 'co2')
+ , (4, 'co4')
+;
+
+insert into media_content (media_type, bytes)
+values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
+;
+
+insert into media (media_id, company_id, original_filename, content_hash)
+values (6, 2, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};'))
+ , (8, 4, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};'))
+;
+
+insert into campsite_type (campsite_type_id, company_id, name, media_id, dogs_allowed, max_campers)
+values (10, 2, 'Wooden lodge', 6, false, 7)
+ , (12, 4, 'Bungalow', 8, false, 6)
+;
+
+insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card)
+values (2, 10, 'Holder 2', '2024-01-18', '2024-01-19', 0, false)
+ , (4, 12, 'Holder 4', '2024-01-18', '2024-01-19', 0, false)
+;
+
+prepare booking_data as
+select company_id, holder_name
+from booking
+order by company_id, holder_name;
+
+
+select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/employee4@tandem.blog', 'co4');
+
+select bag_eq(
+ 'booking_data',
+ $$ values (4, 'Holder 4')
+ $$,
+ 'Should only list bookings from second company'
+);
+
+reset role;
+
+select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/employee2@tandem.blog', 'co2');
+
+select bag_eq(
+ 'booking_data',
+ $$ values (2, 'Holder 2')
+ $$,
+ 'Should only list bookings from first company'
+);
+
+select lives_ok(
+ $$ insert into booking(company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card) values (2, 10, 'New Holder', '2024-01-18', '2024-01-19', 0, false) $$,
+ 'Users from company 2 should be able to insert a new booking type to their company.'
+);
+
+select bag_eq(
+ 'booking_data',
+ $$ values (2, 'Holder 2')
+ , (2, 'New Holder')
+ $$,
+ 'The new row should have been added'
+);
+
+select lives_ok(
+ $$ update booking set holder_name = 'Another Holder' where company_id = 2 and holder_name = 'New Holder' $$,
+ 'Users from company 2 should be able to update bookins of their company.'
+);
+
+select bag_eq(
+ 'booking_data',
+ $$ values (2, 'Holder 2')
+ , (2, 'Another Holder')
+ $$,
+ 'The row should have been updated.'
+);
+
+select throws_ok(
+ $$ insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card) values (4, 12, 'Another holder', '2024-01-18', '2024-01-19', 0, false) $$,
+ '42501', 'new row violates row-level security policy for table "booking"',
+ 'Users from company 2 should NOT be able to insert new bookings to company 4.'
+);
+
+select lives_ok(
+ $$ update booking set holder_name = 'Nope' where company_id = 4 $$,
+ 'Users from company 2 should not be able to update new campsite types of company 4, but no error if company_id is not changed.'
+);
+
+select bag_eq(
+ 'booking_data',
+ $$ values (2, 'Holder 2')
+ , (2, 'Another Holder')
+ $$,
+ 'No row should have been changed.'
+);
+
+select throws_ok(
+ $$ update booking set company_id = 4 where company_id = 2 $$,
+ '42501', 'new row violates row-level security policy for table "booking"',
+ 'Users from company 2 should NOT be able to move bookings to company 4'
+);
+
+reset role;
+
+
+select set_cookie('6d8215c4888ffac017c3e4b8438e9a1a5559decd719df9c790/admin2@tandem.blog', 'co2');
+
+select lives_ok(
+ $$ delete from booking where company_id = 2 and holder_name = 'Another Holder' $$,
+ 'Admins from company 2 should be able to delete campsite type from their company.'
+);
+
+select bag_eq(
+ 'booking_data',
+ $$ values (2, 'Holder 2')
+ $$,
+ 'The row should have been deleted.'
+);
+
+select lives_ok(
+ $$ delete from booking where company_id = 4 $$,
+ 'Admins from company 2 should NOT be able to delete bookins from company 4, but not error is thrown'
+);
+
+reset role;
+
+select bag_eq(
+ 'booking_data',
+ $$ values (2, 'Holder 2')
+ , (4, 'Holder 4')
+ $$,
+ 'No row should have been changed'
+);
+
+select throws_ok(
+ $$ insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card) values (2, 10, ' ', '2024-01-18', '2024-01-19', 0, false) $$,
+ '23514', 'new row for relation "booking" violates check constraint "holder_name_not_empty"',
+ 'Should not be able to add bookings with a blank holder name.'
+);
+
+select throws_ok(
+ $$ insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card) values (2, 10, 'Holder', '2024-01-18', '2024-01-17', 0, false) $$,
+ '23514', 'new row for relation "booking" violates check constraint "departure_after_arrival"',
+ 'Should not be able to add bookings with a departure date before the arrival.'
+);
+
+select throws_ok(
+ $$ insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card) values (2, 10, 'Holder', '2024-01-18', '2024-01-18', 0, false) $$,
+ '23514', 'new row for relation "booking" violates check constraint "departure_after_arrival"',
+ 'Should not be able to add bookings with a departure date equal to the arrival.'
+);
+
+select throws_ok(
+ $$ insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card) values (2, 10, 'Holder', '2024-01-18', '2024-01-19', -1, false) $$,
+ '23514', 'new row for relation "booking" violates check constraint "number_dogs_nonnegative"',
+ 'Should not be able to add bookings owing dogs to holder.'
+);
+
+
+select *
+from finish();
+
+rollback;
+
diff --git a/test/booking_status.sql b/test/booking_status.sql
new file mode 100644
index 0000000..e131a4e
--- /dev/null
+++ b/test/booking_status.sql
@@ -0,0 +1,35 @@
+-- Test booking_status
+set client_min_messages to warning;
+create extension if not exists pgtap;
+reset client_min_messages;
+
+begin;
+
+select plan(15);
+
+set search_path to camper, public;
+
+select has_table('booking_status');
+select has_pk('booking_status');
+select table_privs_are('booking_status', 'guest', array[]::text[]);
+select table_privs_are('booking_status', 'employee', array['SELECT']);
+select table_privs_are('booking_status', 'admin', array['SELECT']);
+select table_privs_are('booking_status', 'authenticator', array[]::text[]);
+
+select has_column('booking_status', 'booking_status');
+select col_is_pk('booking_status', 'booking_status');
+select col_type_is('booking_status', 'booking_status', 'text');
+select col_not_null('booking_status', 'booking_status');
+select col_hasnt_default('booking_status', 'booking_status');
+
+select has_column('booking_status', 'name');
+select col_type_is('booking_status', 'name', 'text');
+select col_not_null('booking_status', 'name');
+select col_hasnt_default('booking_status', 'name');
+
+
+select *
+from finish();
+
+rollback;
+
diff --git a/test/booking_status_i18n.sql b/test/booking_status_i18n.sql
new file mode 100644
index 0000000..56112f0
--- /dev/null
+++ b/test/booking_status_i18n.sql
@@ -0,0 +1,44 @@
+-- Test booking_status_i18n
+set client_min_messages to warning;
+create extension if not exists pgtap;
+reset client_min_messages;
+
+begin;
+
+select plan(23);
+
+set search_path to camper, public;
+
+select has_table('booking_status_i18n');
+select has_pk('booking_status_i18n');
+select col_is_pk('booking_status_i18n', array['booking_status', 'lang_tag']);
+select table_privs_are('booking_status_i18n', 'guest', array[]::text[]);
+select table_privs_are('booking_status_i18n', 'employee', array['SELECT']);
+select table_privs_are('booking_status_i18n', 'admin', array['SELECT']);
+select table_privs_are('booking_status_i18n', 'authenticator', array[]::text[]);
+
+select has_column('booking_status_i18n', 'booking_status');
+select col_is_fk('booking_status_i18n', 'booking_status');
+select fk_ok('booking_status_i18n', 'booking_status', 'booking_status', 'booking_status');
+select col_type_is('booking_status_i18n', 'booking_status', 'text');
+select col_not_null('booking_status_i18n', 'booking_status');
+select col_hasnt_default('booking_status_i18n', 'booking_status');
+
+select has_column('booking_status_i18n', 'lang_tag');
+select col_is_fk('booking_status_i18n', 'lang_tag');
+select fk_ok('booking_status_i18n', 'lang_tag', 'language', 'lang_tag');
+select col_type_is('booking_status_i18n', 'lang_tag', 'text');
+select col_not_null('booking_status_i18n', 'lang_tag');
+select col_hasnt_default('booking_status_i18n', 'lang_tag');
+
+select has_column('booking_status_i18n', 'name');
+select col_type_is('booking_status_i18n', 'name', 'text');
+select col_not_null('booking_status_i18n', 'name');
+select col_hasnt_default('booking_status_i18n', 'name');
+
+
+select *
+from finish();
+
+rollback;
+
diff --git a/verify/available_booking_status.sql b/verify/available_booking_status.sql
new file mode 100644
index 0000000..3abb544
--- /dev/null
+++ b/verify/available_booking_status.sql
@@ -0,0 +1,31 @@
+-- Verify camper:available_booking_status on pg
+
+begin;
+
+set search_path to camper;
+
+select 1 / count(*) from booking_status where booking_status = 'created' and name = 'Created';
+select 1 / count(*) from booking_status where booking_status = 'cancelled' and name = 'Cancelled';
+select 1 / count(*) from booking_status where booking_status = 'confirmed' and name = 'Confirmed';
+select 1 / count(*) from booking_status where booking_status = 'checked-in' and name = 'Checked in';
+select 1 / count(*) from booking_status where booking_status = 'invoiced' and name = 'Invoiced';
+
+select 1 / count(*) from booking_status_i18n where booking_status = 'created' and lang_tag = 'ca' and name = 'Creada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'cancelled' and lang_tag = 'ca' and name = 'Cancel·lada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'confirmed' and lang_tag = 'ca' and name = 'Confirmada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'checked-in' and lang_tag = 'ca' and name = 'Registrada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'invoiced' and lang_tag = 'ca' and name = 'Facturada';
+
+select 1 / count(*) from booking_status_i18n where booking_status = 'created' and lang_tag = 'es' and name = 'Creada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'cancelled' and lang_tag = 'es' and name = 'Cancelada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'confirmed' and lang_tag = 'es' and name = 'Confirmada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'checked-in' and lang_tag = 'es' and name = 'Registrada';
+select 1 / count(*) from booking_status_i18n where booking_status = 'invoiced' and lang_tag = 'es' and name = 'Facturada';
+
+select 1 / count(*) from booking_status_i18n where booking_status = 'created' and lang_tag = 'fr' and name = 'Créé';
+select 1 / count(*) from booking_status_i18n where booking_status = 'cancelled' and lang_tag = 'fr' and name = 'Annulé';
+select 1 / count(*) from booking_status_i18n where booking_status = 'confirmed' and lang_tag = 'fr' and name = 'Confirmé';
+select 1 / count(*) from booking_status_i18n where booking_status = 'checked-in' and lang_tag = 'fr' and name = 'Enregistré';
+select 1 / count(*) from booking_status_i18n where booking_status = 'invoiced' and lang_tag = 'fr' and name = 'Facturé';
+
+rollback;
diff --git a/verify/booking.sql b/verify/booking.sql
new file mode 100644
index 0000000..975fe99
--- /dev/null
+++ b/verify/booking.sql
@@ -0,0 +1,25 @@
+-- Verify camper:booking on pg
+
+begin;
+
+select booking_id
+ , company_id
+ , slug
+ , booking_id
+ , holder_name
+ , arrival_date
+ , departure_date
+ , number_dogs
+ , acsi_card
+ , booking_status
+ , created_at
+from camper.booking
+where false;
+
+select 1 / count(*) from pg_class where oid = 'camper.booking'::regclass and relrowsecurity;
+select 1 / count(*) from pg_policy where polname = 'select_from_company' and polrelid = 'camper.booking'::regclass;
+select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.booking'::regclass;
+select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.booking'::regclass;
+select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.booking'::regclass;
+
+rollback;
diff --git a/verify/booking_status.sql b/verify/booking_status.sql
new file mode 100644
index 0000000..fd813ca
--- /dev/null
+++ b/verify/booking_status.sql
@@ -0,0 +1,10 @@
+-- Verify camper:booking_status on pg
+
+begin;
+
+select booking_status
+ , name
+from camper.booking_status
+where false;
+
+rollback;
diff --git a/verify/booking_status_i18n.sql b/verify/booking_status_i18n.sql
new file mode 100644
index 0000000..e42b147
--- /dev/null
+++ b/verify/booking_status_i18n.sql
@@ -0,0 +1,11 @@
+-- Verify camper:booking_status_i18n on pg
+
+begin;
+
+select booking_status
+ , lang_tag
+ , name
+from camper.booking_status_i18n
+where false;
+
+rollback;
diff --git a/web/static/camper.css b/web/static/camper.css
index bfc9588..20ab1f0 100644
--- a/web/static/camper.css
+++ b/web/static/camper.css
@@ -279,7 +279,7 @@ table:not(.month) th, table:not(.month) td {
}
table:not(.month) th {
- text-align: start;
+ text-align: start;
}
/* user menu */
@@ -511,7 +511,7 @@ textarea {
/* accommodation type */
fieldset img {
- max-width: 400px;
+ max-width: 400px;
}
/* calendar */
@@ -624,8 +624,8 @@ fieldset img {
}
.sortable img {
- max-width: 20rem;
- border-radius: 5px;
+ max-width: 20rem;
+ border-radius: 5px;
}
#slide-index img {
@@ -678,7 +678,8 @@ fieldset img {
opacity: .3;
}
-/* i18n input */
+/**/
+
[x-cloak] {
display: none !important;
}
@@ -702,3 +703,28 @@ fieldset img {
label[x-show] > span, label[x-show] > br {
display: none;
}
+
+/* */
+/**/
+
+.booking-created .booking-status {
+ background-color: var(--camper--color--light-blue);
+}
+
+.booking-cancelled .booking-status {
+ background-color: var(--camper--color--rosy);
+}
+
+.booking-confirmed .booking-status {
+ background-color: var(--camper--color--hay);
+}
+
+.booking-checked-in .booking-status {
+ background-color: var(--camper--color--light-green);
+}
+
+.booking-invoiced .booking-status {
+ background-color: var(--camper--color--light-gray);
+}
+
+/* */
diff --git a/web/templates/admin/booking/index.gohtml b/web/templates/admin/booking/index.gohtml
new file mode 100644
index 0000000..cfdec5e
--- /dev/null
+++ b/web/templates/admin/booking/index.gohtml
@@ -0,0 +1,40 @@
+
+{{ define "title" -}}
+ {{( pgettext "Bookings" "title" )}}
+{{- end }}
+
+{{ define "content" -}}
+ {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.bookingIndex*/ -}}
+ {{( pgettext "Add Booking" "action" )}}
+ {{( pgettext "Export Bookings" "action" )}}
+ {{( pgettext "Bookings" "title" )}}
+ {{ if . -}}
+
+
+
+ {{( pgettext "Reference" "header" )}}
+ {{( pgettext "Arrival Date" "header" )}}
+ {{( pgettext "Departure Date" "header" )}}
+ {{( pgettext "Holder Name" "header" )}}
+ {{( pgettext "Status" "header" )}}
+
+
+
+ {{ range . -}}
+
+ {{ .Reference }}
+ {{ .ArrivalDate | formatDate }}
+ {{ .DepartureDate | formatDate }}
+ {{ .HolderName }}
+ {{ .StatusLabel }}
+
+ {{- end }}
+
+
+ {{ else -}}
+ {{( gettext "No booking found." )}}
+ {{- end }}
+{{- end }}
diff --git a/web/templates/admin/payment.gohtml b/web/templates/admin/booking/payment.gohtml
similarity index 98%
rename from web/templates/admin/payment.gohtml
rename to web/templates/admin/booking/payment.gohtml
index 2efbd88..87de695 100644
--- a/web/templates/admin/payment.gohtml
+++ b/web/templates/admin/booking/payment.gohtml
@@ -8,7 +8,7 @@
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.paymentForm*/ -}}
-