package handlers import ( "fmt" "net/http" "strconv" "strings" "time" "go-crm/internal/db" "go-crm/internal/templates" "github.com/go-chi/chi/v5" ) 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) buf := templates.BufRender() badge := "" if pendingCount > 0 { badge = fmt.Sprintf(`%d`, pendingCount) } fmt.Fprint(buf, templates.PageHeader("Review Queue", "Confirm or correct leads with unidentified service interest")) fmt.Fprintf(buf, `
%s
`, badge) if len(leads) == 0 { fmt.Fprint(buf, templates.EmptyState(``, "All caught up! No leads pending review.")) } else { fmt.Fprintf(buf, `
`) fmt.Fprint(buf, templates.TableStart([]string{"Phone", "Name", "Service Interest", "Status", "Arrived", "Actions"})) for _, l := range leads { arrived := time.Unix(l.CreatedAt, 0).Format("02/01 15:04") fmt.Fprintf(buf, ` %s %s
%s %s
`, 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.Fprint(buf, templates.TableEnd()) fmt.Fprintf(buf, `
`) } w.Header().Set("Content-Type", "text/html; charset=utf-8") templates.WritePage(w, buf, "Review Queue", "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) } 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 } 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) buf := templates.BufRender() fmt.Fprint(buf, templates.PageHeader("All Leads", "Complete lead database with filters and inline editing")) if pendingCount > 0 { fmt.Fprintf(buf, `
%d pending review
`, pendingCount) } if len(leads) == 0 { fmt.Fprint(buf, templates.EmptyState(``, "No leads captured yet. Connect WhatsApp to start.")) } else { fmt.Fprintf(buf, `
`) fmt.Fprint(buf, templates.TableStart([]string{"Phone", "Name", "Service", "Status", "Payment", "Arrived", "Last Contact", "Actions"})) for _, l := range leads { arrived := time.Unix(l.CreatedAt, 0).Format("02/01 15:04") lastContact := time.Unix(l.LastContactAt, 0).Format("02/01 15:04") rowClass := "" if l.NeedsReview { rowClass = `class="bg-amber-400/[0.03]"` } statusPill := statusPillHTML(l.Status) fmt.Fprintf(buf, ` %s %s %s %s %s %s %s %s
`, rowClass, l.LeadID, htmlEscape(l.PhoneNormalized), htmlEscape(l.Name), map[bool]string{true: `review`, false: ""}[l.NeedsReview], htmlEscape(l.ServiceInterest), statusPill, htmlEscape(l.PaymentStatus), arrived, lastContact, l.LeadID, l.LeadID, l.LeadID, ) } fmt.Fprint(buf, templates.TableEnd()) fmt.Fprintf(buf, `
`) // Edit rows for _, l := range leads { fmt.Fprintf(buf, ` `, l.LeadID, l.LeadID, l.LeadID, l.LeadID, htmlEscape(l.Name), buildServiceOptions(services, l.ServiceInterest), buildStatusOptions(statuses, l.Status), l.LeadID) } } w.Header().Set("Content-Type", "text/html; charset=utf-8") templates.WritePage(w, buf, "All Leads", "leads") } 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. 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 { 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 } } return []string{ "Não especificou", "Head Spa", "Massagem completa", "Drenagem linfatica", "Hydra Boost", "Design Henna", "Masculino", } } 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) }