package customer import ( "context" "net/http" "dev.tandem.ws/tandem/camper/pkg/auth" "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" ) type AdminHandler struct { } func NewAdminHandler() *AdminHandler { return &AdminHandler{} } 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: serveCustomerIndex(w, r, user, company, conn) case http.MethodPost: addCustomer(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } case "new": switch r.Method { case http.MethodGet: f := NewContactForm(r.Context(), conn, user.Locale) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } default: f := NewContactForm(r.Context(), conn, user.Locale) if err := f.FillFromDatabase(r.Context(), conn, head); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } h.customerHandler(user, company, conn, f).ServeHTTP(w, r) } }) } func (h *AdminHandler) customerHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *ContactForm) 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: f.MustRender(w, r, user, company) case http.MethodPut: editCustomer(w, r, user, company, conn, f) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } default: http.NotFound(w, r) } }) } func serveCustomerIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { customers, err := collectCustomerEntries(r.Context(), conn, company) if err != nil { panic(err) } page := &customerIndex{ Customers: customers, } page.MustRender(w, r, user, company) } func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *auth.Company) ([]*customerEntry, error) { rows, err := conn.Query(ctx, ` select '/admin/customers/' || slug , name , coalesce(email::text, '') , coalesce(phone::text, '') from contact left join contact_email using (contact_id) left join contact_phone using (contact_id) where company_id = $1 order by name `, company.ID) if err != nil { return nil, err } defer rows.Close() var customers []*customerEntry for rows.Next() { customer := &customerEntry{} if err = rows.Scan(&customer.URL, &customer.Name, &customer.Email, &customer.Phone); err != nil { return nil, err } customers = append(customers, customer) } return customers, nil } type customerEntry struct { URL string Name string Email string Phone string } type customerIndex struct { Customers []*customerEntry } func (page *customerIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "customer/index.gohtml", page) } func addCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := NewContactForm(r.Context(), conn, user.Locale) processCustomerForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { var err error f.Slug, err = tx.AddContact(ctx, company.ID, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String()) return err }) } func editCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *ContactForm) { processCustomerForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { _, err := tx.EditContact(ctx, f.Slug, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String()) return err }) } func processCustomerForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *ContactForm, act func(ctx context.Context, tx *database.Tx) error) { if err := f.Parse(r); 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 } if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { panic(err) } else if !ok { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } tx := conn.MustBegin(r.Context()) if err := act(r.Context(), tx); err == nil { if err := tx.Commit(r.Context()); err != nil { panic(err) } } else { if err := tx.Rollback(r.Context()); err != nil { panic(err) } panic(err) } httplib.Redirect(w, r, "/admin/customers", http.StatusSeeOther) } type ContactForm struct { URL string Slug string FullName *form.Input IDDocumentType *form.Select IDDocumentNumber *form.Input Address *form.Input City *form.Input Province *form.Input PostalCode *form.Input Country *form.Select Email *form.Input Phone *form.Input } func NewContactForm(ctx context.Context, conn *database.Conn, l *locale.Locale) *ContactForm { return &ContactForm{ FullName: &form.Input{ Name: "full_name", }, IDDocumentType: &form.Select{ Name: "id_document_type", Options: form.MustGetDocumentTypeOptions(ctx, conn, l), }, IDDocumentNumber: &form.Input{ Name: "id_document_number", }, Address: &form.Input{ Name: "address", }, City: &form.Input{ Name: "city", }, Province: &form.Input{ Name: "province", }, PostalCode: &form.Input{ Name: "postal_code", }, Country: &form.Select{ Name: "country", Options: form.MustGetCountryOptions(ctx, conn, l), }, Email: &form.Input{ Name: "email", }, Phone: &form.Input{ Name: "phone", }, } } func (f *ContactForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error { row := conn.QueryRow(ctx, ` select '/admin/customers/' || slug , slug , name , array[id_document_type_id::text] , id_document_number , address , city , province , postal_code , array[country_code::text] , coalesce(email::text, '') , coalesce(phone::text, '') from contact as text left join contact_email using (contact_id) left join contact_phone using (contact_id) where slug = $1 `, slug) return row.Scan( &f.URL, &f.Slug, &f.FullName.Val, &f.IDDocumentType.Selected, &f.IDDocumentNumber.Val, &f.Address.Val, &f.City.Val, &f.Province.Val, &f.PostalCode.Val, &f.Country.Selected, &f.Email.Val, &f.Phone.Val, ) } func (f *ContactForm) FillFromBooking(ctx context.Context, conn *database.Conn, bookingID int) error { row := conn.QueryRow(ctx, ` select '' , '' , holder_name , array[]::text[] , '' , coalesce(address, '') , coalesce(city, '') , '' , coalesce(postal_code, '') , array[coalesce(country_code::text, '')] , coalesce(email::text, '') , coalesce(phone::text, '') from booking where booking_id = $1 `, bookingID) return row.Scan( &f.URL, &f.Slug, &f.FullName.Val, &f.IDDocumentType.Selected, &f.IDDocumentNumber.Val, &f.Address.Val, &f.City.Val, &f.Province.Val, &f.PostalCode.Val, &f.Country.Selected, &f.Email.Val, &f.Phone.Val, ) } func (f *ContactForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.FullName.FillValue(r) f.IDDocumentType.FillValue(r) f.IDDocumentNumber.FillValue(r) f.Address.FillValue(r) f.City.FillValue(r) f.Province.FillValue(r) f.PostalCode.FillValue(r) f.Country.FillValue(r) f.Email.FillValue(r) f.Phone.FillValue(r) return nil } func (f *ContactForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { v := form.NewValidator(l) var country string if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) { country = f.Country.Selected[0] } v.CheckSelectedOptions(f.IDDocumentType, l.GettextNoop("Selected ID document type is not valid.")) v.CheckRequired(f.IDDocumentNumber, l.GettextNoop("ID document number can not be empty.")) if v.CheckRequired(f.FullName, l.GettextNoop("Full name can not be empty.")) { v.CheckMinLength(f.FullName, 1, l.GettextNoop("Full name must have at least one letter.")) } v.CheckRequired(f.Address, l.GettextNoop("Address can not be empty.")) v.CheckRequired(f.City, l.GettextNoop("Town or village can not be empty.")) if v.CheckRequired(f.PostalCode, l.GettextNoop("Postcode can not be empty.")) && country != "" { if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postcode is not valid.")); err != nil { return false, err } } if f.Email.Val != "" { v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com.")) } if f.Phone.Val != "" && country != "" { if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil { return false, err } } return v.AllOK, nil } func (f *ContactForm) UpdateOrCreate(ctx context.Context, company *auth.Company, tx *database.Tx) (int, error) { var contactID int row := tx.QueryRow(ctx, ` select contact_id, slug from contact where id_document_type_id = $1 and id_document_number = $2 and country_code = $3 `, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Country.String(), ) if err := row.Scan(&contactID, &f.Slug); err != nil { if database.ErrorIsNotFound(err) { f.Slug, err = tx.AddContact(ctx, company.ID, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String()) if err != nil { return 0, err } contactID, err = tx.GetInt(ctx, "select contact_id from contact where slug = $1", f.Slug) if err != nil { return 0, err } } else { return 0, err } } _, err := tx.EditContact(ctx, f.Slug, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String()) if err != nil { return 0, err } return contactID, nil } func (f *ContactForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdminFiles(w, r, user, company, f, "customer/form.gohtml", "customer/contact.gohtml") }