package handlers
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"go-crm/internal/db"
"go-crm/internal/templates"
"go-crm/internal/whatsapp"
"github.com/go-chi/chi/v5"
)
func (a *App) ListLeads(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
search := r.URL.Query().Get("search")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit == 0 {
limit = 20
}
query := "SELECT cu.customer_id, cu.client_id, cu.name, cu.phone, cu.birth_date, cu.instagram, cu.created_at, COALESCE(cl.whatsapp_connected,0), COALESCE(cl.whatsapp_number,'') FROM customers cu JOIN clients cl ON cu.client_id = cl.client_id WHERE cl.account_id = ?"
args := []interface{}{accountID}
if search != "" {
query += " AND (cu.name LIKE ? OR cu.phone LIKE ?)"
searchPat := "%%" + search + "%%"
args = append(args, searchPat, searchPat)
}
query += " ORDER BY cu.created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := a.DB.Query(query, args...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var customers []db.Customer
for rows.Next() {
var c db.Customer
if err := rows.Scan(&c.CustomerID, &c.ClientID, &c.Name, &c.Phone, &c.BirthDate, &c.Instagram, &c.CreatedAt, &c.WhatsAppConnected, &c.WhatsAppNumber); err != nil {
continue
}
customers = append(customers, c)
}
buf := templates.BufRender()
fmt.Fprint(buf, templates.PageHeader("Leads", "Search and manage customer leads"))
// Search
fmt.Fprintf(buf, `
`, htmlEscape(search))
if len(customers) == 0 {
fmt.Fprint(buf, templates.EmptyState(` `, "No leads found. Try a different search or connect WhatsApp to capture leads."))
} else {
fmt.Fprintf(buf, ``)
fmt.Fprint(buf, templates.TableStart([]string{"Name", "Phone", "Birth Date", "Instagram", "WhatsApp", "Actions"}))
for _, c := range customers {
waStatus := `
— `
if c.WhatsAppConnected == 1 && c.WhatsAppNumber != "" {
waStatus = fmt.Sprintf(`
%s `, htmlEscape(c.WhatsAppNumber))
}
fmt.Fprintf(buf, `
%s
%s
%s
%s
%s
Edit
`, htmlEscape(c.Name), htmlEscape(c.Phone), htmlEscape(c.BirthDate), htmlEscape(c.Instagram), waStatus, c.CustomerID, c.CustomerID)
}
fmt.Fprint(buf, templates.TableEnd())
fmt.Fprintf(buf, `
`)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.WritePage(w, buf, "Leads", "leads")
}
func (a *App) UpdateLead(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
_, err := a.DB.Exec(
"UPDATE customers SET name = ?, phone = ?, birth_date = ?, instagram = ? WHERE customer_id = ? AND client_id IN (SELECT client_id FROM clients WHERE account_id = ?)",
r.FormValue("name"), r.FormValue("phone"), r.FormValue("birth_date"), r.FormValue("instagram"), id, accountID,
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
a.ListLeads(w, r)
}
func (a *App) DeleteLead(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
_, err := a.DB.Exec(
"DELETE FROM customers WHERE customer_id = ? AND client_id IN (SELECT client_id FROM clients WHERE account_id = ?)",
id, accountID,
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(""))
}
func (a *App) LeadsConnectPage(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
clientID, _ := strconv.ParseInt(r.URL.Query().Get("client_id"), 10, 64)
client, err := db.GetClientByID(a.DB, accountID, clientID)
if err != nil {
http.Error(w, "Client not found", http.StatusNotFound)
return
}
buf := templates.BufRender()
fmt.Fprint(buf, templates.PageHeader("Connect WhatsApp", fmt.Sprintf("Scan QR code to link %s", htmlEscape(client.Name))))
fmt.Fprintf(buf, `
`, client.ClientID, client.ClientID)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.WritePage(w, buf, "Connect WhatsApp", "leads")
}
func jsonEscape(s string) string {
b, _ := json.Marshal(s)
return string(b)
}
func (a *App) LeadsQR(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
clientID, _ := strconv.ParseInt(r.URL.Query().Get("client_id"), 10, 64)
client, err := db.GetClientByID(a.DB, accountID, clientID)
if err != nil {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"status":"error","error":"client not found"}`))
return
}
if a.WAConnector == nil {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(`{"status":"error","error":"WhatsApp not configured - contact admin"}`))
return
}
connected, err := a.WAConnector.IsConnected(r.Context(), clientID)
if err != nil {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"status":"error","error":` + jsonEscape(err.Error()) + `}`))
return
}
if connected {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"status":"ready"}`))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
log.Printf("QR: Starting Connect for client %d", clientID)
qrChan, err := a.WAConnector.Connect(context.Background(), clientID)
if err != nil {
log.Printf("QR: Connect returned error for client %d: %v", clientID, err)
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"status":"error","error":` + jsonEscape(err.Error()) + `}`))
return
}
timeout := time.After(30 * time.Second)
for {
select {
case <-timeout:
log.Printf("QR: Timeout waiting for client %d", clientID)
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"status":"error","error":` + jsonEscape("timeout waiting for QR code") + `}`))
return
case frame, ok := <-qrChan:
if !ok {
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"status":"error","error":"connection closed"}`))
return
}
if frame.QR != "" {
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"qr":` + jsonEscape(frame.QR) + `,"status":"waiting"}`))
return
}
if frame.State == whatsapp.StateConnected {
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"status":"ready"}`))
return
}
if frame.State == whatsapp.StateFailed {
w.Write([]byte(`{"client_id":` + strconv.FormatInt(client.ClientID, 10) + `,"status":"error","error":` + jsonEscape(frame.Error) + `}`))
return
}
}
}
}
func (a *App) VerifyLead(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
clientID, _ := strconv.ParseInt(chi.URLParam(r, "client_id"), 10, 64)
client, err := db.GetClientByID(a.DB, accountID, clientID)
if err != nil {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"status":"error","error":"client not found"}`))
return
}
match := "unknown"
if client.WhatsAppNumber != "" && client.Phone != "" {
cleanWA := cleanPhone(client.WhatsAppNumber)
cleanClient := cleanPhone(client.Phone)
if cleanWA == cleanClient {
match = "yes"
} else {
match = "no"
}
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"status":"ok","wa_phone":"` + jsonEscape(client.WhatsAppNumber) + `","client_phone":"` + jsonEscape(client.Phone) + `","match":"` + match + `","client_name":"` + jsonEscape(client.Name) + `"}`))
}
func cleanPhone(s string) string {
s = strings.TrimPrefix(s, "+")
s = strings.TrimPrefix(s, "55")
return s
}
// --- package-level shims kept for existing tests ---
func ListLeads(w http.ResponseWriter, r *http.Request) {
(&App{DB: DB, WAConnector: WAConnector}).ListLeads(w, r)
}
func UpdateLead(w http.ResponseWriter, r *http.Request) {
(&App{DB: DB, WAConnector: WAConnector}).UpdateLead(w, r)
}
func DeleteLead(w http.ResponseWriter, r *http.Request) {
(&App{DB: DB, WAConnector: WAConnector}).DeleteLead(w, r)
}
func LeadsConnectPage(w http.ResponseWriter, r *http.Request) {
(&App{DB: DB, WAConnector: WAConnector}).LeadsConnectPage(w, r)
}
func LeadsQR(w http.ResponseWriter, r *http.Request) {
(&App{DB: DB, WAConnector: WAConnector}).LeadsQR(w, r)
}
func VerifyLead(w http.ResponseWriter, r *http.Request) {
(&App{DB: DB, WAConnector: WAConnector}).VerifyLead(w, r)
}