package handlers import ( "fmt" "net/http" "strconv" "strings" "time" "go-crm/internal/db" "github.com/go-chi/chi/v5" ) // LeadReviewQueue renders the review queue: leads where needs_review = 1. // GET /leads/review func (a *App) LeadReviewQueue(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { return } clientID := a.clientIDForAccount(accountID) if clientID == 0 { http.Error(w, "No client found for account", http.StatusBadRequest) return } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) if limit == 0 { limit = 50 } leads, err := db.ListLeadsForReview(a.DB, clientID, limit, offset) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } statuses, _ := db.ListLeadStatuses(a.DB, clientID) services := a.serviceNames(clientID) pendingCount := len(leads) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Review Queue (%d pending)

Review Queue %d

Leads with service interest not yet identified. Confirm or correct each entry.


`, pendingCount, pendingCount) if len(leads) == 0 { fmt.Fprintf(w, `
No leads pending review.
`) } else { fmt.Fprintf(w, ``) for _, l := range leads { arrived := time.Unix(l.CreatedAt, 0).Format("02/01 15:04") fmt.Fprintf(w, ` `, l.LeadID, htmlEscape(l.PhoneNormalized), htmlEscape(l.Name), l.LeadID, l.LeadID, buildServiceOptions(services, l.ServiceInterest), buildStatusOptions(statuses, l.Status), htmlEscape(l.Name), htmlEscape(l.Status), arrived, l.LeadID, l.LeadID, ) } fmt.Fprintf(w, `
Phone Name Service Interest Status Arrived Actions
%s %s
%s %s
`) } fmt.Fprintf(w, ``) } // ConfirmLeadReview handles the form submission from the review queue. // PUT /leads/{id}/review func (a *App) ConfirmLeadReview(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { return } clientID := a.clientIDForAccount(accountID) if clientID == 0 { http.Error(w, "No client found", http.StatusBadRequest) return } leadID, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) r.ParseForm() lead, err := db.GetLeadByID(a.DB, clientID, leadID) if err != nil { http.Error(w, "Lead not found", http.StatusNotFound) return } lead.Name = r.FormValue("name") lead.ServiceInterest = r.FormValue("service_interest") lead.Status = r.FormValue("status") lead.NeedsReview = false if err := db.UpdateLead(a.DB, lead); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ``, leadID) } // LeadAllList renders all leads (not just review queue). // GET /leads/all func (a *App) LeadAllList(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { return } clientID := a.clientIDForAccount(accountID) if clientID == 0 { http.Error(w, "No client found for account", http.StatusBadRequest) return } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) if limit == 0 { limit = 50 } var leads []db.Lead var err error if a.LeadService != nil { domainLeads, svcErr := a.LeadService.ListAllLeads(r.Context(), clientID, limit, offset) if svcErr != nil { http.Error(w, svcErr.Error(), http.StatusInternalServerError) return } // convert domain.Leaf to db.Lead leads = make([]db.Lead, len(domainLeads)) for i, dl := range domainLeads { leads[i] = db.Lead{ LeadID: dl.LeadID, ClientID: dl.ClientID, Name: dl.Name, PhoneRaw: dl.PhoneRaw, PhoneNormalized: dl.PhoneNormalized, ServiceInterest: dl.ServiceInterest, Status: dl.Status, NeedsReview: dl.NeedsReview, AppointmentDate: dl.AppointmentDate, AppointmentTime: dl.AppointmentTime, PaymentStatus: dl.PaymentStatus, PaymentAmount: dl.PaymentAmount, PaymentDate: dl.PaymentDate, CreatedAt: dl.CreatedAt, LastContactAt: dl.LastContactAt, } } } else { leads, err = db.ListAllLeads(a.DB, clientID, limit, offset) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } pendingCount, _ := db.CountLeadsNeedingReview(a.DB, clientID) statuses, _ := db.ListLeadStatuses(a.DB, clientID) services := a.serviceNames(clientID) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` All Leads

All Leads


`, pendingCount) for _, l := range leads { arrived := time.Unix(l.CreatedAt, 0).Format("02/01/2006 15:04") lastContact := time.Unix(l.LastContactAt, 0).Format("02/01/2006 15:04") rowClass := "" if l.NeedsReview { rowClass = `class="needs-review"` } fmt.Fprintf(w, ` `, rowClass, l.LeadID, htmlEscape(l.PhoneNormalized), htmlEscape(l.Name), htmlEscape(l.ServiceInterest), htmlEscape(l.Status), htmlEscape(l.PaymentStatus), arrived, lastContact, l.LeadID, l.LeadID, l.LeadID, l.LeadID, l.LeadID, l.LeadID, htmlEscape(l.Name), buildServiceOptions(services, l.ServiceInterest), buildStatusOptions(statuses, l.Status), l.LeadID, ) } fmt.Fprintf(w, `
PhoneNameServiceStatus PaymentArrivedLast ContactActions
%s %s %s %s %s %s %s
`) } // DeleteLeadNew handles DELETE /leads/{id} func (a *App) DeleteLeadNew(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { return } clientID := a.clientIDForAccount(accountID) leadID, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) a.DB.Exec("DELETE FROM leads WHERE lead_id = ? AND client_id = ?", leadID, clientID) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte("")) } // serviceNames returns the list of service names for the review/all-leads dropdowns. // It reads from the DB-backed services table first, then falls back to the // hardcoded defaults so the dropdown is never empty on a fresh install. func (a *App) serviceNames(clientID int64) []string { rows, err := a.DB.Query( "SELECT DISTINCT name FROM services WHERE client_id = ? ORDER BY name", clientID, ) if err == nil { defer rows.Close() var names []string for rows.Next() { var n string if rows.Scan(&n) == nil { names = append(names, n) } } if len(names) > 0 { // Ensure "Não especificou" is always first. hasDefault := false for _, n := range names { if n == "Não especificou" { hasDefault = true break } } if !hasDefault { names = append([]string{"Não especificou"}, names...) } return names } } // Fallback: hardcoded defaults for a fresh install with no services yet. return []string{ "Não especificou", "Head Spa", "Massagem completa", "Drenagem linfatica", "Hydra Boost", "Design Henna", "Masculino", } } // --- rendering helpers ------------------------------------------------------- func buildServiceOptions(services []string, selected string) string { out := "" for _, s := range services { sel := "" if s == selected { sel = ` selected` } out += fmt.Sprintf(``, htmlEscape(s), sel, htmlEscape(s)) } return out } func buildStatusOptions(statuses []db.LeadStatus, selected string) string { out := "" for _, s := range statuses { sel := "" if s.StatusName == selected { sel = ` selected` } out += fmt.Sprintf(``, htmlEscape(s.StatusName), sel, htmlEscape(s.StatusName)) } return out } func htmlEscape(s string) string { s = strings.ReplaceAll(s, "&", "&") s = strings.ReplaceAll(s, "<", "<") s = strings.ReplaceAll(s, ">", ">") s = strings.ReplaceAll(s, `"`, """) return s } // --- package-level shims kept for existing tests --- func LeadReviewQueue(w http.ResponseWriter, r *http.Request) { (&App{DB: DB, WAConnector: WAConnector}).LeadReviewQueue(w, r) } func ConfirmLeadReview(w http.ResponseWriter, r *http.Request) { (&App{DB: DB, WAConnector: WAConnector}).ConfirmLeadReview(w, r) } func LeadAllList(w http.ResponseWriter, r *http.Request) { (&App{DB: DB, WAConnector: WAConnector}).LeadAllList(w, r) } func DeleteLeadNew(w http.ResponseWriter, r *http.Request) { (&App{DB: DB, WAConnector: WAConnector}).DeleteLeadNew(w, r) }