package handlers import ( "context" "encoding/json" "log" "net/http" "strconv" "strings" "time" "go-crm/internal/db" "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 (name LIKE ? OR 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) } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(` Leads

Leads

`)) for _, c := range customers { waStatus := "Not connected" waStyle := "color: #999;" if c.WhatsAppConnected == 1 && c.WhatsAppNumber != "" { waStatus = c.WhatsAppNumber waStyle = "color: #28a745; font-weight: 600;" } w.Write([]byte(``)) } w.Write([]byte(`
NamePhoneBirth DateInstagramWhatsAppActions
` + c.Name + ` ` + c.Phone + ` ` + c.BirthDate + ` ` + c.Instagram + ` ` + waStatus + `

Back to Home | Back to Clients

`)) } 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("OK")) } 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 } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(` Connect WhatsApp

Connect WhatsApp

Scan the QR code below with your WhatsApp app to connect

Loading...

Back to Home | Back to Clients

`)) } 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 := strings.TrimPrefix(client.WhatsAppNumber, "+") cleanClient := strings.TrimPrefix(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) + `"}`)) } // --- 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) }