Add “part” of the bookings’ management

“Part”, because it is not possible to add or actually manage any
booking yet, but it has the export feature that we need to validate the
project.
This commit is contained in:
jordi fita mas 2024-01-18 21:05:30 +01:00
parent 1b7e7ed2c6
commit d945f55096
28 changed files with 1298 additions and 180 deletions

View File

@ -654,4 +654,18 @@ select translate_surroundings_highlight(120, 'fr', 'Pyrénées', '<p>Pyrénées
select translate_surroundings_highlight(121, 'fr', 'Gérone', '<p>Visite incontournable, principalement: le quartier juifs, les Rambles, la cathédrale et les jardins qui lentourent … Sans oublier ses nombreuses boutiques !</p><p><a href="https://www.girona.cat/turisme/fra/">Turisme Girona</a></p>');
select translate_surroundings_highlight(122, 'fr', 'Barcelone', '<p>Barcelone cest plus que des boutiques et le Barça … Découvrez la richesse de ces quartiers: Gràcia, Barceloneta, …</p><p><a href="https://www.barcelonaturisme.com/wv3/fr/">Turisme Barcelona</a></p>');
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;

View File

@ -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;

64
deploy/booking.sql Normal file
View File

@ -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;

17
deploy/booking_status.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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":

View File

@ -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)
}

170
pkg/booking/ods.go Normal file
View File

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest
xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
manifest:version="1.3">
<manifest:file-entry manifest:full-path="/" manifest:version="1.3" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
</manifest:manifest>
`
metaXml = `<?xml version="1.0" encoding="UTF-8"?>
<office:document-meta
xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
office:version="1.3">
<office:meta>
<meta:creation-date></meta:creation-date>
<meta:generator>Camper</meta:generator>
</office:meta>
</office:document-meta>
`
stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
office:version="1.3">
</office:document-styles>
`
)
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(`<?xml version="1.0" encoding="UTF-8"?>
<office:document-content
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
office:version="1.3">
<office:scripts/>
<office:font-face-decls>
<style:font-face style:name="Liberation Sans" svg:font-family="'Liberation Sans'" style:font-family-generic="swiss" style:font-pitch="variable"/>
</office:font-face-decls>
<office:automatic-styles>
<style:style style:name="co1" style:family="table-column">
<style:table-column-properties fo:break-before="auto" style:column-width="0.889in"/>
</style:style>
<style:style style:name="ro1" style:family="table-row">
<style:table-row-properties style:row-height="0.178in" fo:break-before="auto" style:use-optimal-row-height="true"/>
</style:style>
<style:style style:name="ta1" style:family="table" style:master-page-name="Default">
<style:table-properties table:display="true" style:writing-mode="lr-tb"/>
</style:style>
<number:date-style style:name="N37" number:automatic-order="true">
<number:day number:style="long"/>
<number:text>/</number:text>
<number:month number:style="long"/>
<number:text>/</number:text>
<number:year/>
</number:date-style>
<style:style style:name="ce1" style:family="table-cell" style:parent-style-name="Default" style:data-style-name="N37"/>
</office:automatic-styles>
<office:body>
<office:spreadsheet>
<table:calculation-settings table:automatic-find-labels="false" table:use-regular-expressions="false" table:use-wildcards="true"/>
<table:table table:name="Sheet1" table:style-name="ta1">
`)
sb.WriteString(fmt.Sprintf(" <table:table-column table:style-name=\"co1\" table:number-columns-repeated=\"%d\" table:default-cell-style-name=\"Default\"/>\n", len(columns)))
sb.WriteString(` <table:table-row table:style-name="ro1">
`)
for _, t := range columns {
if err := writeCellString(&sb, locale.GetC(t, "header")); err != nil {
return nil, err
}
}
sb.WriteString(" </table:table-row>\n")
for _, row := range rows {
sb.WriteString(" <table:table-row table:style-name=\"ro1\">\n")
if err := writeRow(&sb, row); err != nil {
return nil, err
}
sb.WriteString(" </table:table-row>\n")
}
sb.WriteString(` </table:table>
<table:named-expressions/>
</office:spreadsheet>
</office:body>
</office:document-content>
`)
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(` <table:table-cell office:value-type="string" calcext:value-type="string"><text:p>`)
if err := xml.EscapeText(sb, []byte(s)); err != nil {
return err
}
sb.WriteString("</text:p></table:table-cell>\n")
return nil
}
func writeCellDate(sb *strings.Builder, t time.Time) {
sb.WriteString(fmt.Sprintf(" <table:table-cell table:style-name=\"ce1\" office:value-type=\"date\" office:date-value=\"%s\" calcext:value-type=\"date\"><text:p>%s</text:p></table:table-cell>\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)
}
}

View File

@ -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 datetime="` + time.Format("2006-01-02") + `">` + time.Format("02/01/2006") + "</time>")
},
"queryEscape": func(s string) string {
return url.QueryEscape(s)
},

156
po/ca.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 sha 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 lempresa"
#: 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 sha 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 darribada"
#: 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 sha 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 "Lentorn 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."

156
po/es.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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."

158
po/fr.po
View File

@ -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 <info@oriolcarbonell.cat>\n"
"Language-Team: French <traduc@traduc.org>\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 darrivé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 "Lenvironnement sélectionné nest pas valide."
#: pkg/booking/admin.go:153
#: pkg/booking/admin.go:253
msgid "Selected integration is not valid."
msgstr "Lintégration sélectionnée nest pas valide."
#: pkg/booking/admin.go:156
#: pkg/booking/admin.go:256
msgid "The merchant key is not valid."
msgstr "La clé marchand nest pas valide."

View File

@ -0,0 +1,13 @@
-- Revert camper:available_booking_status from pg
begin;
delete
from camper.booking_status_i18n
;
delete
from camper.booking_status
;
commit;

7
revert/booking.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:booking from pg
begin;
drop table if exists camper.booking;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:booking_status from pg
begin;
drop table if exists camper.booking_status;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:booking_status_i18n from pg
begin;
drop table if exists camper.booking_status_i18n;
commit;

View File

@ -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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add view to see login attempts for current company
booking_status [roles schema_camper] 2024-01-18T14:34:53Z jordi fita mas <jordi@tandem.blog> # Add relation of booking statuses
booking_status_i18n [roles schema_camper booking_status language] 2024-01-18T14:39:49Z jordi fita mas <jordi@tandem.blog> # Add relation of booking status translations
available_booking_status [booking_status booking_status_i18n] 2024-01-18T14:45:37Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Booking relation

276
test/booking.sql Normal file
View File

@ -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;

35
test/booking_status.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

25
verify/booking.sql Normal file
View File

@ -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;

10
verify/booking_status.sql Normal file
View File

@ -0,0 +1,10 @@
-- Verify camper:booking_status on pg
begin;
select booking_status
, name
from camper.booking_status
where false;
rollback;

View File

@ -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;

View File

@ -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 */
/*<editor-fold desc="i18n input">*/
[x-cloak] {
display: none !important;
}
@ -702,3 +703,28 @@ fieldset img {
label[x-show] > span, label[x-show] > br {
display: none;
}
/*</editor-fold>*/
/*<editor-fold desc="booking">*/
.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);
}
/*</editor-fold>*/

View File

@ -0,0 +1,40 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Bookings" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.bookingIndex*/ -}}
<a href="/admin/bookings/new">{{( pgettext "Add Booking" "action" )}}</a>
<a href="/admin/bookings?format=ods">{{( pgettext "Export Bookings" "action" )}}</a>
<h2>{{( pgettext "Bookings" "title" )}}</h2>
{{ if . -}}
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Reference" "header" )}}</th>
<th scope="col">{{( pgettext "Arrival Date" "header" )}}</th>
<th scope="col">{{( pgettext "Departure Date" "header" )}}</th>
<th scope="col">{{( pgettext "Holder Name" "header" )}}</th>
<th scope="col">{{( pgettext "Status" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ range . -}}
<tr class="booking-{{ .Status }}">
<td><a href="{{ .URL }}">{{ .Reference }}</a></td>
<td>{{ .ArrivalDate | formatDate }}</td>
<td>{{ .DepartureDate | formatDate }}</td>
<td>{{ .HolderName }}</td>
<td class="booking-status">{{ .StatusLabel }}</td>
</tr>
{{- end }}
</tbody>
</table>
{{ else -}}
<p>{{( gettext "No booking found." )}}</p>
{{- end }}
{{- end }}

View File

@ -8,7 +8,7 @@
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.paymentForm*/ -}}
<form data-hx-put="/admin/payment">
<form data-hx-put="/admin/booking/payment">
<h2>{{( pgettext "Payment Settings" "title" )}}</h2>
{{ CSRFInput }}
<fieldset>

View File

@ -37,7 +37,7 @@
<a href="/admin/company">{{( pgettext "Company Settings" "title" )}}</a>
</li>
<li>
<a href="/admin/payment">{{( pgettext "Payment Settings" "title" )}}</a>
<a href="/admin/booking/payment">{{( pgettext "Payment Settings" "title" )}}</a>
</li>
<li>
<a href="/admin/campsites/types">{{( pgettext "Campsite Types" "title" )}}</a>
@ -85,6 +85,9 @@
<li>
<a href="/admin/">{{( pgettext "Dashboard" "title" )}}</a>
</li>
<li>
<a href="/admin/bookings">{{( pgettext "Bookings" "title" )}}</a>
</li>
<li>
<a href="/admin/campsites">{{( pgettext "Campsites" "title" )}}</a>
</li>