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(`") + }, "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 . -}} + + + + + + + + + + + + {{ range . -}} + + + + + + + + {{- end }} + +
{{( pgettext "Reference" "header" )}}{{( pgettext "Arrival Date" "header" )}}{{( pgettext "Departure Date" "header" )}}{{( pgettext "Holder Name" "header" )}}{{( pgettext "Status" "header" )}}
{{ .Reference }}{{ .ArrivalDate | formatDate }}{{ .DepartureDate | formatDate }}{{ .HolderName }}{{ .StatusLabel }}
+ {{ 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*/ -}} -
+

{{( pgettext "Payment Settings" "title" )}}

{{ CSRFInput }}
diff --git a/web/templates/admin/layout.gohtml b/web/templates/admin/layout.gohtml index 341118e..3d35476 100644 --- a/web/templates/admin/layout.gohtml +++ b/web/templates/admin/layout.gohtml @@ -37,7 +37,7 @@ {{( pgettext "Company Settings" "title" )}}
  • - {{( pgettext "Payment Settings" "title" )}} + {{( pgettext "Payment Settings" "title" )}}
  • {{( pgettext "Campsite Types" "title" )}} @@ -85,6 +85,9 @@
  • {{( pgettext "Dashboard" "title" )}}
  • +
  • + {{( pgettext "Bookings" "title" )}} +
  • {{( pgettext "Campsites" "title" )}}