/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package campsite import ( "context" "net/http" "time" "github.com/jackc/pgx/v4" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/campsite/types" "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/form" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/season" "dev.tandem.ws/tandem/camper/pkg/template" ) type AdminHandler struct { types *types.AdminHandler } func NewAdminHandler() *AdminHandler { return &AdminHandler{ types: types.NewAdminHandler(), } } func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "new": switch r.Method { case http.MethodGet: f := newCampsiteForm(r.Context(), conn, company) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "types": h.types.Handler(user, company, conn).ServeHTTP(w, r) case "": switch r.Method { case http.MethodGet: serveCampsiteIndex(w, r, user, company, conn) case http.MethodPost: addCampsite(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } default: f := newCampsiteForm(r.Context(), conn, company) if err := f.FillFromDatabase(r.Context(), conn, company, head); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: f.MustRender(w, r, user, company) case http.MethodPut: editCampsite(w, r, user, company, conn, f) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } case "slides": h.carouselHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r) case "features": h.featuresHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r) default: http.NotFound(w, r) } } } } func serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { page := newCampsiteIndex() if err := page.Parse(r); err != nil { panic(err) } var err error page.Campsites, err = collectCampsiteEntries(r.Context(), company, conn, page.From.Date(), page.To.Date()) if err != nil { panic(err) } page.Months = collectMonths(page.From.Date(), page.To.Date()) page.MustRender(w, r, user, company) } func collectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time) ([]*campsiteEntry, error) { rows, err := conn.Query(ctx, ` select campsite.label , campsite_type.name , campsite.active from campsite join campsite_type using (campsite_type_id) where campsite.company_id = $1 order by label`, company.ID) if err != nil { return nil, err } defer rows.Close() byLabel := make(map[string]*campsiteEntry) var campsites []*campsiteEntry for rows.Next() { entry := &campsiteEntry{} if err = rows.Scan(&entry.Label, &entry.Type, &entry.Active); err != nil { return nil, err } campsites = append(campsites, entry) byLabel[entry.Label] = entry } if err := collectBookingEntries(ctx, company, conn, from, to, byLabel); err != nil { return nil, err } return campsites, nil } func collectBookingEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time, campsites map[string]*campsiteEntry) error { lastDay := to.AddDate(0, 1, 0) rows, err := conn.Query(ctx, ` select campsite.label , lower(booking_campsite.stay * daterange($2::date, $3::date)) , holder_name , booking_status , upper(booking_campsite.stay * daterange($2::date, $3::date)) - lower(booking_campsite.stay * daterange($2::date, $3::date)) , booking_campsite.stay &> daterange($2::date, $3::date) , booking_campsite.stay &< daterange($2::date, $3::date) from booking_campsite join booking using (booking_id) join campsite using (campsite_id) where booking.company_id = $1 and booking_campsite.stay && daterange($2::date, $3::date) order by label`, company.ID, from, lastDay) if err != nil { return err } defer rows.Close() for rows.Next() { entry := &bookingEntry{} var label string var date time.Time if err = rows.Scan(&label, &date, &entry.Holder, &entry.Status, &entry.Nights, &entry.Begin, &entry.End); err != nil { return err } campsite := campsites[label] if campsite != nil { if campsite.Bookings == nil { campsite.Bookings = make(map[time.Time]*bookingEntry) } campsite.Bookings[date] = entry } } return nil } type campsiteEntry struct { Label string Type string Active bool Bookings map[time.Time]*bookingEntry } type bookingEntry struct { Holder string Status string Nights int Begin bool End bool } type campsiteIndex struct { From *form.Month To *form.Month Campsites []*campsiteEntry Months []*Month } func newCampsiteIndex() *campsiteIndex { now := time.Now() from := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) to := from.AddDate(0, 3, 0) return &campsiteIndex{ From: &form.Month{ Name: "from", Year: from.Year(), Month: from.Month(), }, To: &form.Month{ Name: "to", Year: to.Year(), Month: to.Month(), }, } } func (page *campsiteIndex) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } page.From.FillValue(r) page.To.FillValue(r) return nil } type Month struct { Year int Month time.Month Name string Days []time.Time Spans []*Span } type Span struct { Weekend bool Count int } func isWeekend(t time.Time) bool { switch t.Weekday() { case time.Saturday, time.Sunday: return true default: return false } } func collectMonths(from time.Time, to time.Time) []*Month { current := time.Date(from.Year(), from.Month(), 1, 0, 0, 0, 0, time.UTC) numMonths := (to.Year()-from.Year())*12 + int(to.Month()) - int(from.Month()) + 1 var months []*Month for i := 0; i < numMonths; i++ { span := &Span{ Weekend: isWeekend(current), } month := &Month{ Year: current.Year(), Month: current.Month(), Name: season.LongMonthNames[current.Month()-1], Days: make([]time.Time, 0, 31), Spans: make([]*Span, 0, 10), } month.Spans = append(month.Spans, span) for current.Month() == month.Month { month.Days = append(month.Days, current) if span.Weekend != isWeekend(current) { span = &Span{ Weekend: !span.Weekend, } month.Spans = append(month.Spans, span) } span.Count = span.Count + 1 current = current.AddDate(0, 0, 1) } months = append(months, month) } return months } func (page *campsiteIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdminFiles(w, r, user, company, page, "campsite/index.gohtml", "web/templates/campground_map.svg") } func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := newCampsiteForm(r.Context(), conn, company) processCampsiteForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { var err error f.ID, err = tx.AddCampsite(ctx, f.CampsiteType.Int(), f.Label.Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val) if err != nil { return err } return translateCampsite(ctx, tx, company, f) }) httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther) } func translateCampsite(ctx context.Context, tx *database.Tx, company *auth.Company, f *campsiteForm) error { for lang := range company.Locales { l := lang.String() if l == f.DefaultLang { continue } if err := tx.TranslateCampsite(ctx, f.ID, lang, f.Info1[l].Val, f.Info2[l].Val); err != nil { return err } } return nil } func editCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm) { processCampsiteForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { if err := tx.EditCampsite(ctx, f.ID, f.CampsiteType.Int(), f.Label.Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val, f.Active.Checked); err != nil { return err } return translateCampsite(ctx, tx, company, f) }) httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther) } func processCampsiteForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm, act func(ctx context.Context, tx *database.Tx) error) { if ok, err := form.Handle(f, w, r, user); err != nil { return } else if !ok { f.MustRender(w, r, user, company) return } tx := conn.MustBegin(r.Context()) defer tx.Rollback(r.Context()) if err := act(r.Context(), tx); err != nil { panic(err) } tx.MustCommit(r.Context()) httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther) } type campsiteForm struct { DefaultLang string ID int CurrentLabel string Active *form.Checkbox CampsiteType *form.Select Label *form.Input Info1 form.I18nInput Info2 form.I18nInput } func newCampsiteForm(ctx context.Context, conn *database.Conn, company *auth.Company) *campsiteForm { campsiteTypes := form.MustGetOptions(ctx, conn, "select campsite_type_id::text, name from campsite_type where active") return &campsiteForm{ DefaultLang: company.DefaultLanguage.String(), Active: &form.Checkbox{ Name: "active", Checked: true, }, CampsiteType: &form.Select{ Name: "description", Options: campsiteTypes, }, Label: &form.Input{ Name: "label", }, Info1: form.NewI18nInput(company.Locales, "info1"), Info2: form.NewI18nInput(company.Locales, "info2"), } } func (f *campsiteForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, label string) error { f.CurrentLabel = label var info1 database.RecordArray var info2 database.RecordArray row := conn.QueryRow(ctx, ` select campsite_id , array[campsite_type_id::text] , label , campsite.info1::text , campsite.info2::text , active , array_agg((lang_tag, i18n.info1::text)) , array_agg((lang_tag, i18n.info2::text)) from campsite left join campsite_i18n as i18n using (campsite_id) where company_id = $1 and label = $2 group by campsite_id , campsite_type_id , label , campsite.info1::text , campsite.info2::text , active `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID, label) if err := row.Scan(&f.ID, &f.CampsiteType.Selected, &f.Label.Val, &f.Info1[f.DefaultLang].Val, &f.Info2[f.DefaultLang].Val, &f.Active.Checked, &info1, &info2); err != nil { return err } if err := f.Info1.FillArray(info1); err != nil { return err } if err := f.Info2.FillArray(info2); err != nil { return err } return nil } func (f *campsiteForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Active.FillValue(r) f.CampsiteType.FillValue(r) f.Label.FillValue(r) f.Info1.FillValue(r) f.Info2.FillValue(r) return nil } func (f *campsiteForm) Valid(l *locale.Locale) bool { v := form.NewValidator(l) v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid.")) v.CheckRequired(f.Label, l.GettextNoop("Label can not be empty.")) return v.AllOK } func (f *campsiteForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "campsite/form.gohtml", f) }