/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package services import ( "context" "net/http" "strconv" "github.com/jackc/pgx/v4" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/carousel" "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/template" ) const carouselName = "services" type AdminHandler struct { carousel *carousel.AdminHandler } func NewAdminHandler() *AdminHandler { return &AdminHandler{ carousel: carousel.NewAdminHandler(carouselName, serveServicesIndex), } } func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: serveServicesIndex(w, r, user, company, conn) case http.MethodPost: addService(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } case "new": switch r.Method { case http.MethodGet: f := newServiceForm(r.Context(), conn, company) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "order": switch r.Method { case http.MethodPost: orderServices(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodPost) } case "slides": h.carousel.Handler(user, company, conn).ServeHTTP(w, r) default: id, err := strconv.Atoi(head) if err != nil { http.NotFound(w, r) return } f := newServiceForm(r.Context(), conn, company) if err := f.FillFromDatabase(r.Context(), conn, id); 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: editService(w, r, user, company, conn, f) case http.MethodDelete: deleteService(w, r, user, conn, id) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) } default: http.NotFound(w, r) } } }) } func orderServices(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } input := r.PostForm["service_id"] if len(input) > 0 { var ids []int for _, s := range input { if id, err := strconv.Atoi(s); err == nil { ids = append(ids, id) } else { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } } if err := conn.OrderServices(r.Context(), ids); err != nil { panic(err) } } serveServicesIndex(w, r, user, company, conn) } func serveServicesIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { slides, err := carousel.CollectSlideEntries(r.Context(), company, conn, carouselName) if err != nil { panic(err) } services, err := collectServiceEntries(r.Context(), company, conn) if err != nil { panic(err) } page := &servicesIndex{ Services: services, Slides: slides, } page.MustRender(w, r, user, company) } type servicesIndex struct { Services []*serviceEntry Slides []*carousel.SlideEntry } func (page *servicesIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "services/index.gohtml", page) } type serviceEntry struct { ID int URL string Icon string Name string } func collectServiceEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*serviceEntry, error) { rows, err := conn.Query(ctx, ` select service_id , '/admin/services/' || service_id , icon_name , name from service where company_id = $1 order by position , name `, company.ID) if err != nil { return nil, err } defer rows.Close() var services []*serviceEntry for rows.Next() { entry := &serviceEntry{} if err = rows.Scan(&entry.ID, &entry.URL, &entry.Icon, &entry.Name); err != nil { return nil, err } services = append(services, entry) } return services, nil } func addService(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := newServiceForm(r.Context(), conn, company) processServiceForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { var err error if f.ID, err = tx.AddService(ctx, company.ID, f.Icon.String(), f.Name[f.DefaultLang].Val, f.Description[f.DefaultLang].Val); err != nil { return err } return translateService(ctx, tx, company, f) }) } func processServiceForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *serviceForm, act func(context.Context, *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()) if err := act(r.Context(), tx); err == nil { tx.MustCommit(r.Context()) } else { if rErr := tx.Rollback(r.Context()); rErr != nil { err = rErr } panic(err) } httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther) } func translateService(ctx context.Context, tx *database.Tx, company *auth.Company, f *serviceForm) error { for lang := range company.Locales { l := lang.String() if l == f.DefaultLang { continue } if err := tx.TranslateService(ctx, f.ID, lang, f.Name[l].Val, f.Description[l].Val); err != nil { return err } } return nil } func editService(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *serviceForm) { processServiceForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { if err := tx.EditService(ctx, f.ID, f.Icon.String(), f.Name[f.DefaultLang].Val, f.Description[f.DefaultLang].Val); err != nil { return err } return translateService(ctx, tx, company, f) }) httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther) } func deleteService(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, id int) { if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } conn.MustExec(r.Context(), "select remove_service($1)", id) httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther) } type serviceForm struct { DefaultLang string ID int Icon *form.Select Name form.I18nInput Description form.I18nInput } func newServiceForm(ctx context.Context, conn *database.Conn, company *auth.Company) *serviceForm { return &serviceForm{ DefaultLang: company.DefaultLanguage.String(), Icon: &form.Select{ Name: "icon", Options: form.MustGetOptions(ctx, conn, "select icon_name, icon_name from icon order by 1"), }, Name: form.NewI18nInput(company.Locales, "name"), Description: form.NewI18nInput(company.Locales, "description"), } } func (f *serviceForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error { f.ID = id var name database.RecordArray var description database.RecordArray row := conn.QueryRow(ctx, ` select array[icon_name] , service.name , service.description::text , array_agg((lang_tag, i18n.name)) , array_agg((lang_tag, i18n.description::text)) from service left join service_i18n as i18n using (service_id) where service_id = $1 group by icon_name , service.name , service.description::text `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, id) if err := row.Scan(&f.Icon.Selected, &f.Name[f.DefaultLang].Val, &f.Description[f.DefaultLang].Val, &name, &description); err != nil { return err } if err := f.Name.FillArray(name); err != nil { return err } if err := f.Description.FillArray(description); err != nil { return err } return nil } func (f *serviceForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Icon.FillValue(r) f.Name.FillValue(r) f.Description.FillValue(r) return nil } func (f *serviceForm) Valid(l *locale.Locale) bool { v := form.NewValidator(l) v.CheckSelectedOptions(f.Icon, l.GettextNoop("Selected icon is not valid.")) v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty.")) return v.AllOK } func (f *serviceForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "services/form.gohtml", f) }