1878 lines
54 KiB
Go
1878 lines
54 KiB
Go
package handlers
|
||
|
||
import (
|
||
"aggios-app/backend/internal/domain"
|
||
"aggios-app/backend/internal/repository"
|
||
"aggios-app/backend/internal/api/middleware"
|
||
"crypto/rand"
|
||
"database/sql"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"log"
|
||
"net/http"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/gorilla/mux"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
type CRMHandler struct {
|
||
repo *repository.CRMRepository
|
||
}
|
||
|
||
func NewCRMHandler(repo *repository.CRMRepository) *CRMHandler {
|
||
return &CRMHandler{repo: repo}
|
||
}
|
||
|
||
// ==================== CUSTOMERS ====================
|
||
|
||
type publicRegisterRequest struct {
|
||
domain.CRMCustomer
|
||
Password string `json:"password"`
|
||
}
|
||
|
||
// PublicRegisterCustomer allows public registration without authentication
|
||
// SECURITY: Rate limited, validates tenant exists, checks email format, prevents duplicates
|
||
func (h *CRMHandler) PublicRegisterCustomer(w http.ResponseWriter, r *http.Request) {
|
||
var req publicRegisterRequest
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
customer := req.CRMCustomer
|
||
password := req.Password
|
||
|
||
// SECURITY 0: Validar força da senha
|
||
if password == "" || len(password) < 8 {
|
||
log.Printf("⚠️ Public registration blocked: invalid password length")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "invalid_password",
|
||
"message": "A senha deve ter no mínimo 8 caracteres.",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Validar complexidade da senha
|
||
hasUpper := false
|
||
hasLower := false
|
||
hasNumber := false
|
||
|
||
for _, char := range password {
|
||
switch {
|
||
case char >= 'A' && char <= 'Z':
|
||
hasUpper = true
|
||
case char >= 'a' && char <= 'z':
|
||
hasLower = true
|
||
case char >= '0' && char <= '9':
|
||
hasNumber = true
|
||
}
|
||
}
|
||
|
||
if !hasUpper || !hasLower || !hasNumber {
|
||
log.Printf("⚠️ Public registration blocked: weak password (missing character types)")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "weak_password",
|
||
"message": "A senha deve conter pelo menos uma letra maiúscula, uma minúscula e um número.",
|
||
})
|
||
return
|
||
}
|
||
|
||
// SECURITY 1: Validar tenant_id obrigatório
|
||
if customer.TenantID == "" {
|
||
log.Printf("⚠️ Public registration blocked: missing tenant_id")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "tenant_id is required",
|
||
})
|
||
return
|
||
}
|
||
|
||
// SECURITY 2: Validar que o tenant existe no banco
|
||
tenantExists, err := h.repo.TenantExists(customer.TenantID)
|
||
if err != nil {
|
||
log.Printf("❌ Error checking tenant existence: %v", err)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Internal server error",
|
||
})
|
||
return
|
||
}
|
||
if !tenantExists {
|
||
log.Printf("🚫 Public registration blocked: invalid tenant_id=%s", customer.TenantID)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid agency",
|
||
})
|
||
return
|
||
}
|
||
|
||
// SECURITY 3: Validar campos obrigatórios
|
||
if customer.Name == "" || customer.Email == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "name and email are required",
|
||
})
|
||
return
|
||
}
|
||
|
||
// SECURITY 4: Validar formato de email
|
||
email := strings.TrimSpace(strings.ToLower(customer.Email))
|
||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||
if !emailRegex.MatchString(email) {
|
||
log.Printf("⚠️ Public registration blocked: invalid email format=%s", email)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid email format",
|
||
})
|
||
return
|
||
}
|
||
customer.Email = email
|
||
|
||
// SECURITY 5: Verificar se email já existe para este tenant (constraint unique_email_per_tenant)
|
||
existingCustomer, err := h.repo.GetCustomerByEmailAndTenant(email, customer.TenantID)
|
||
if err == nil && existingCustomer != nil {
|
||
log.Printf("⚠️ Public registration blocked: email already exists for tenant (tenant=%s, email=%s)", customer.TenantID, email)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusConflict)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "duplicate_email",
|
||
"message": "Já existe uma conta cadastrada com este e-mail.",
|
||
})
|
||
return
|
||
}
|
||
|
||
// SECURITY 6: Verificar duplicidade de CPF/CNPJ nos notes (formato JSON)
|
||
if customer.Notes != "" {
|
||
log.Printf("🔍 Public registration: checking notes for logo/cpf/cnpj: %s", customer.Notes)
|
||
var notesData map[string]interface{}
|
||
if err := json.Unmarshal([]byte(customer.Notes), ¬esData); err == nil {
|
||
// Extrair CPF ou CNPJ
|
||
cpf, hasCPF := notesData["cpf"].(string)
|
||
cnpj, hasCNPJ := notesData["cnpj"].(string)
|
||
|
||
// Verificar CPF duplicado
|
||
if hasCPF && cpf != "" {
|
||
existing, err := h.repo.GetCustomerByCPF(cpf, customer.TenantID)
|
||
if err == nil && existing != nil {
|
||
log.Printf("⚠️ Public registration blocked: CPF already exists (tenant=%s, cpf=%s)", customer.TenantID, cpf)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusConflict)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "duplicate_email",
|
||
"message": "Já existe uma conta cadastrada com este CPF.",
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
// Verificar CNPJ duplicado
|
||
if hasCNPJ && cnpj != "" {
|
||
existing, err := h.repo.GetCustomerByCNPJ(cnpj, customer.TenantID)
|
||
if err == nil && existing != nil {
|
||
log.Printf("⚠️ Public registration blocked: CNPJ already exists (tenant=%s, cnpj=%s)", customer.TenantID, cnpj)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusConflict)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "duplicate_email",
|
||
"message": "Já existe uma conta cadastrada com este CNPJ.",
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
// Extrair logo se existir
|
||
if logo, hasLogo := notesData["logo_path"].(string); hasLogo && logo != "" {
|
||
log.Printf("🖼️ Found logo in public registration notes: %s", logo)
|
||
customer.LogoURL = logo
|
||
}
|
||
} else {
|
||
log.Printf("⚠️ Failed to unmarshal public registration notes: %v", err)
|
||
}
|
||
}
|
||
|
||
// SECURITY 7: Sanitizar nome
|
||
customer.Name = strings.TrimSpace(customer.Name)
|
||
if len(customer.Name) > 255 {
|
||
customer.Name = customer.Name[:255]
|
||
}
|
||
|
||
// SECURITY 8: Hash da senha fornecida pelo cliente
|
||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
log.Printf("❌ Error hashing password: %v", err)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Internal server error",
|
||
})
|
||
return
|
||
}
|
||
|
||
customer.ID = uuid.New().String()
|
||
customer.IsActive = true
|
||
// CreatedBy fica vazio pois é cadastro público
|
||
|
||
// Garantir que as tags de cadastro público sejam aplicadas
|
||
if customer.Tags == nil || len(customer.Tags) == 0 {
|
||
customer.Tags = []string{"cadastro_publico", "pendente_aprovacao"}
|
||
} else {
|
||
// Garantir que tenha pelo menos a tag cadastro_publico
|
||
hasPublicTag := false
|
||
hasPendingTag := false
|
||
for _, tag := range customer.Tags {
|
||
if tag == "cadastro_publico" {
|
||
hasPublicTag = true
|
||
}
|
||
if tag == "pendente_aprovacao" {
|
||
hasPendingTag = true
|
||
}
|
||
}
|
||
if !hasPublicTag {
|
||
customer.Tags = append(customer.Tags, "cadastro_publico")
|
||
}
|
||
if !hasPendingTag {
|
||
customer.Tags = append(customer.Tags, "pendente_aprovacao")
|
||
}
|
||
}
|
||
|
||
log.Printf("📝 Public customer registration: tenant_id=%s, email=%s, name=%s, tags=%v", customer.TenantID, email, customer.Name, customer.Tags)
|
||
|
||
if err := h.repo.CreateCustomer(&customer); err != nil {
|
||
log.Printf("❌ Error creating public customer: %v", err)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to create customer",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// Salvar senha hasheada
|
||
if err := h.repo.UpdateCustomerPassword(customer.ID, string(passwordHash)); err != nil {
|
||
log.Printf("⚠️ Error saving password for customer %s: %v", customer.ID, err)
|
||
// Não retornar erro pois o cliente foi criado, senha pode ser resetada depois
|
||
}
|
||
|
||
log.Printf("✅ Public customer created successfully with password: id=%s", customer.ID)
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"customer": customer,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) CreateCustomer(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
var customer domain.CRMCustomer
|
||
if err := json.NewDecoder(r.Body).Decode(&customer); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
customer.ID = uuid.New().String()
|
||
customer.TenantID = tenantID
|
||
customer.CreatedBy = userID
|
||
customer.IsActive = true
|
||
|
||
log.Printf("➕ CreateCustomer called: name=%s, company=%s, logo_url=%s", customer.Name, customer.Company, customer.LogoURL)
|
||
|
||
if err := h.repo.CreateCustomer(&customer); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to create customer",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// Auto-create a default campaign for this customer
|
||
defaultCampaign := domain.CRMList{
|
||
ID: uuid.New().String(),
|
||
TenantID: tenantID,
|
||
CustomerID: &customer.ID,
|
||
Name: "Geral - " + customer.Name,
|
||
Description: "Campanha padrão para " + customer.Name,
|
||
Color: "#3b82f6",
|
||
CreatedBy: userID,
|
||
}
|
||
_ = h.repo.CreateList(&defaultCampaign)
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"customer": customer,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) GetCustomers(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
log.Printf("GetCustomers: tenantID=%s", tenantID)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
customers, err := h.repo.GetCustomersByTenant(tenantID)
|
||
log.Printf("GetCustomers: found %d customers, error: %v", len(customers), err)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to fetch customers",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
if customers == nil {
|
||
customers = []domain.CRMCustomer{}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"customers": customers,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) GetCustomer(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
customerID := vars["id"]
|
||
|
||
customer, err := h.repo.GetCustomerByID(customerID, tenantID)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Customer not found",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// Buscar listas do cliente
|
||
lists, _ := h.repo.GetCustomerLists(customerID)
|
||
if lists == nil {
|
||
lists = []domain.CRMList{}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"customer": customer,
|
||
"lists": lists,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) UpdateCustomer(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
customerID := vars["id"]
|
||
|
||
var customer domain.CRMCustomer
|
||
if err := json.NewDecoder(r.Body).Decode(&customer); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
customer.ID = customerID
|
||
customer.TenantID = tenantID
|
||
|
||
log.Printf("🔄 UpdateCustomer called for customer %s, name: %s, logo_url: %s, tags: %v", customerID, customer.Name, customer.LogoURL, customer.Tags)
|
||
|
||
if err := h.repo.UpdateCustomer(&customer); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to update customer",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// Se as tags foram alteradas e a tag pendente_aprovacao foi removida, liberar acesso ao portal
|
||
hasPendingTag := false
|
||
for _, tag := range customer.Tags {
|
||
if tag == "pendente_aprovacao" {
|
||
hasPendingTag = true
|
||
break
|
||
}
|
||
}
|
||
|
||
log.Printf("🔍 Checking portal access: hasPendingTag=%v", hasPendingTag)
|
||
|
||
// Se não tem mais a tag pendente e tinha senha definida, liberar acesso
|
||
if !hasPendingTag {
|
||
existingCustomer, err := h.repo.GetCustomerByID(customerID, tenantID)
|
||
log.Printf("🔍 Existing customer check: hasPassword=%v, err=%v", existingCustomer != nil && existingCustomer.PasswordHash != "", err)
|
||
|
||
if err == nil && existingCustomer.PasswordHash != "" {
|
||
// Liberar acesso ao portal
|
||
if err := h.repo.EnableCustomerPortalAccess(customerID); err != nil {
|
||
log.Printf("⚠️ Warning: Failed to enable portal access for customer %s: %v", customerID, err)
|
||
} else {
|
||
log.Printf("✅ Portal access enabled for customer %s", customerID)
|
||
}
|
||
} else if err == nil && existingCustomer.PasswordHash == "" {
|
||
log.Printf("⚠️ Customer %s approved but has no password yet - portal access will be enabled when password is set", customerID)
|
||
}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "Customer updated successfully",
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) DeleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
customerID := vars["id"]
|
||
|
||
if err := h.repo.DeleteCustomer(customerID, tenantID); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to delete customer",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "Customer deleted successfully",
|
||
})
|
||
}
|
||
|
||
// ==================== LISTS ====================
|
||
|
||
func (h *CRMHandler) CreateList(w http.ResponseWriter, r *http.Request) {
|
||
tenantIDVal := r.Context().Value(middleware.TenantIDKey)
|
||
userIDVal := r.Context().Value(middleware.UserIDKey)
|
||
|
||
log.Printf("🔍 CreateList DEBUG: tenantID type=%T value=%v | userID type=%T value=%v",
|
||
tenantIDVal, tenantIDVal, userIDVal, userIDVal)
|
||
|
||
tenantID, ok := tenantIDVal.(string)
|
||
if !ok || tenantID == "" {
|
||
log.Printf("❌ CreateList: Missing or invalid tenant_id")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
userID, _ := userIDVal.(string)
|
||
|
||
var list domain.CRMList
|
||
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
list.ID = uuid.New().String()
|
||
list.TenantID = tenantID
|
||
list.CreatedBy = userID
|
||
|
||
// Handle empty customer_id
|
||
if list.CustomerID != nil && *list.CustomerID == "" {
|
||
list.CustomerID = nil
|
||
}
|
||
|
||
if list.Color == "" {
|
||
list.Color = "#3b82f6"
|
||
}
|
||
|
||
if err := h.repo.CreateList(&list); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to create list",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"list": list,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) GetLists(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
customerID := r.URL.Query().Get("customer_id")
|
||
log.Printf("GetLists: tenantID=%s, customerID=%s", tenantID, customerID)
|
||
|
||
lists, err := h.repo.GetListsByTenant(tenantID)
|
||
if err != nil {
|
||
log.Printf("GetLists: Error fetching lists: %v", err)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to fetch lists",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
if lists == nil {
|
||
lists = []domain.CRMListWithCustomers{}
|
||
}
|
||
|
||
// Filter by customer if provided
|
||
if customerID != "" {
|
||
filteredLists := []domain.CRMListWithCustomers{}
|
||
for _, list := range lists {
|
||
if list.CustomerID != nil && *list.CustomerID == customerID {
|
||
filteredLists = append(filteredLists, list)
|
||
}
|
||
}
|
||
log.Printf("GetLists: Filtered lists from %d to %d", len(lists), len(filteredLists))
|
||
lists = filteredLists
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"lists": lists,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) GetList(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
listID := vars["id"]
|
||
|
||
list, err := h.repo.GetListByID(listID, tenantID)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "List not found",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// Buscar clientes da lista
|
||
customers, _ := h.repo.GetListCustomers(listID, tenantID)
|
||
if customers == nil {
|
||
customers = []domain.CRMCustomer{}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"list": list,
|
||
"customers": customers,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) UpdateList(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
listID := vars["id"]
|
||
|
||
var list domain.CRMList
|
||
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
list.ID = listID
|
||
list.TenantID = tenantID
|
||
|
||
// Handle empty customer_id
|
||
if list.CustomerID != nil && *list.CustomerID == "" {
|
||
list.CustomerID = nil
|
||
}
|
||
|
||
if err := h.repo.UpdateList(&list); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to update list",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "List updated successfully",
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) DeleteList(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Missing tenant_id",
|
||
})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
listID := vars["id"]
|
||
|
||
if err := h.repo.DeleteList(listID, tenantID); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to delete list",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "List deleted successfully",
|
||
})
|
||
}
|
||
|
||
// ==================== LEADS ====================
|
||
|
||
func (h *CRMHandler) CreateLead(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
var lead domain.CRMLead
|
||
if err := json.NewDecoder(r.Body).Decode(&lead); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
lead.ID = uuid.New().String()
|
||
lead.TenantID = tenantID
|
||
lead.CreatedBy = userID
|
||
lead.IsActive = true
|
||
if lead.Status == "" {
|
||
lead.Status = "novo"
|
||
}
|
||
if lead.SourceMeta == nil {
|
||
lead.SourceMeta = json.RawMessage(`{}`)
|
||
}
|
||
|
||
// Ensure default funnel and stage if not provided
|
||
if lead.FunnelID == nil || *lead.FunnelID == "" || lead.StageID == nil || *lead.StageID == "" {
|
||
funnelID, err := h.repo.EnsureDefaultFunnel(tenantID)
|
||
if err == nil {
|
||
lead.FunnelID = &funnelID
|
||
stages, err := h.repo.GetStagesByFunnelID(funnelID)
|
||
if err == nil && len(stages) > 0 {
|
||
lead.StageID = &stages[0].ID
|
||
}
|
||
}
|
||
}
|
||
|
||
if err := h.repo.CreateLead(&lead); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to create lead",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"lead": lead,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) GetLeads(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
customerID := r.URL.Query().Get("customer_id")
|
||
log.Printf("GetLeads: tenantID=%s, customerID=%s", tenantID, customerID)
|
||
|
||
leads, err := h.repo.GetLeadsWithListsByTenant(tenantID)
|
||
if err != nil {
|
||
log.Printf("GetLeads: Error fetching leads: %v", err)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to fetch leads",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
if leads == nil {
|
||
leads = []domain.CRMLeadWithLists{}
|
||
}
|
||
|
||
// Filter by customer if provided
|
||
if customerID != "" {
|
||
filteredLeads := []domain.CRMLeadWithLists{}
|
||
for _, lead := range leads {
|
||
if lead.CustomerID != nil && *lead.CustomerID == customerID {
|
||
filteredLeads = append(filteredLeads, lead)
|
||
}
|
||
}
|
||
log.Printf("GetLeads: Filtered leads for customer %s: from %d to %d", customerID, len(leads), len(filteredLeads))
|
||
leads = filteredLeads
|
||
} else {
|
||
log.Printf("GetLeads: No customer_id filter applied, returning all %d leads", len(leads))
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"leads": leads,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) GetLeadsByList(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
listID := vars["id"]
|
||
|
||
leads, err := h.repo.GetLeadsByListID(listID)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to fetch leads",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
if leads == nil {
|
||
leads = []domain.CRMLead{}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"leads": leads,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) GetLead(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
leadID := vars["id"]
|
||
|
||
lead, err := h.repo.GetLeadByID(leadID, tenantID)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Lead not found",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
lists, _ := h.repo.GetLeadLists(leadID)
|
||
if lists == nil {
|
||
lists = []domain.CRMList{}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"lead": lead,
|
||
"lists": lists,
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) UpdateLead(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
leadID := vars["id"]
|
||
|
||
var lead domain.CRMLead
|
||
if err := json.NewDecoder(r.Body).Decode(&lead); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
lead.ID = leadID
|
||
lead.TenantID = tenantID
|
||
if lead.SourceMeta == nil {
|
||
lead.SourceMeta = json.RawMessage(`{}`)
|
||
}
|
||
|
||
if err := h.repo.UpdateLead(&lead); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to update lead",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{"message": "Lead updated successfully"})
|
||
}
|
||
|
||
func (h *CRMHandler) DeleteLead(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
leadID := vars["id"]
|
||
|
||
if err := h.repo.DeleteLead(leadID, tenantID); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to delete lead",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{"message": "Lead deleted successfully"})
|
||
}
|
||
|
||
func (h *CRMHandler) AddLeadToList(w http.ResponseWriter, r *http.Request) {
|
||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||
vars := mux.Vars(r)
|
||
leadID := vars["lead_id"]
|
||
listID := vars["list_id"]
|
||
|
||
if err := h.repo.AddLeadToList(leadID, listID, userID); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to add lead to list",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{"message": "Lead added to list successfully"})
|
||
}
|
||
|
||
func (h *CRMHandler) RemoveLeadFromList(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
leadID := vars["lead_id"]
|
||
listID := vars["list_id"]
|
||
|
||
if err := h.repo.RemoveLeadFromList(leadID, listID); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to remove lead from list",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{"message": "Lead removed from list successfully"})
|
||
}
|
||
|
||
type LeadIngestRequest struct {
|
||
Name string `json:"name"`
|
||
Email string `json:"email"`
|
||
Phone string `json:"phone"`
|
||
Notes string `json:"notes"`
|
||
Tags []string `json:"tags"`
|
||
Source string `json:"source"`
|
||
SourceMeta map[string]interface{} `json:"source_meta"`
|
||
ListID string `json:"list_id"`
|
||
ListName string `json:"list_name"`
|
||
Status string `json:"status"`
|
||
CustomerID string `json:"customer_id"`
|
||
}
|
||
|
||
func (h *CRMHandler) IngestLead(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
var req LeadIngestRequest
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid request body",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
if req.Email == "" && req.Phone == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Informe ao menos email ou telefone",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Resolver list_id (opcional)
|
||
listID := req.ListID
|
||
if listID == "" && req.ListName != "" {
|
||
if existing, err := h.repo.GetListByName(tenantID, req.ListName); err == nil {
|
||
listID = existing.ID
|
||
} else if err == sql.ErrNoRows {
|
||
newList := domain.CRMList{
|
||
ID: uuid.New().String(),
|
||
TenantID: tenantID,
|
||
Name: req.ListName,
|
||
Description: "Criada automaticamente via ingestão de leads",
|
||
Color: "#3b82f6",
|
||
CreatedBy: userID,
|
||
}
|
||
if err := h.repo.CreateList(&newList); err == nil {
|
||
listID = newList.ID
|
||
}
|
||
}
|
||
}
|
||
|
||
// Dedup por email/phone
|
||
var existingLead *domain.CRMLead
|
||
if found, err := h.repo.GetLeadByEmailOrPhone(tenantID, req.Email, req.Phone); err == nil {
|
||
existingLead = found
|
||
} else if err != nil && err != sql.ErrNoRows {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to ingest lead",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// Normalizar source_meta
|
||
sourceMetaBytes, _ := json.Marshal(req.SourceMeta)
|
||
if len(sourceMetaBytes) == 0 {
|
||
sourceMetaBytes = []byte(`{}`)
|
||
}
|
||
|
||
if req.Status == "" {
|
||
req.Status = "novo"
|
||
}
|
||
if req.Source == "" {
|
||
req.Source = "import"
|
||
}
|
||
|
||
// Processar customer_id
|
||
var customerIDPtr *string
|
||
if req.CustomerID != "" {
|
||
customerIDPtr = &req.CustomerID
|
||
}
|
||
|
||
if existingLead == nil {
|
||
lead := domain.CRMLead{
|
||
ID: uuid.New().String(),
|
||
TenantID: tenantID,
|
||
CustomerID: customerIDPtr,
|
||
Name: req.Name,
|
||
Email: req.Email,
|
||
Phone: req.Phone,
|
||
Source: req.Source,
|
||
SourceMeta: json.RawMessage(sourceMetaBytes),
|
||
Status: req.Status,
|
||
Notes: req.Notes,
|
||
Tags: req.Tags,
|
||
IsActive: true,
|
||
CreatedBy: userID,
|
||
}
|
||
|
||
if err := h.repo.CreateLead(&lead); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to ingest lead",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
if listID != "" {
|
||
_ = h.repo.AddLeadToList(lead.ID, listID, userID)
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"lead": lead,
|
||
"created": true,
|
||
"linked_list": listID != "",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Se já existe: atualiza campos básicos se vierem preenchidos
|
||
updated := *existingLead
|
||
if customerIDPtr != nil {
|
||
updated.CustomerID = customerIDPtr
|
||
}
|
||
if updated.Name == "" && req.Name != "" {
|
||
updated.Name = req.Name
|
||
}
|
||
if updated.Email == "" && req.Email != "" {
|
||
updated.Email = req.Email
|
||
}
|
||
if updated.Phone == "" && req.Phone != "" {
|
||
updated.Phone = req.Phone
|
||
}
|
||
updated.Source = req.Source
|
||
updated.SourceMeta = json.RawMessage(sourceMetaBytes)
|
||
if updated.Status == "" {
|
||
updated.Status = req.Status
|
||
}
|
||
if req.Notes != "" {
|
||
updated.Notes = req.Notes
|
||
}
|
||
if len(req.Tags) > 0 {
|
||
updated.Tags = req.Tags
|
||
}
|
||
if err := h.repo.UpdateLead(&updated); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to update existing lead",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
if listID != "" {
|
||
_ = h.repo.AddLeadToList(updated.ID, listID, userID)
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"lead": updated,
|
||
"created": false,
|
||
"linked_list": listID != "",
|
||
})
|
||
}
|
||
|
||
// ==================== CUSTOMER <-> LIST ====================
|
||
|
||
func (h *CRMHandler) AddCustomerToList(w http.ResponseWriter, r *http.Request) {
|
||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||
|
||
vars := mux.Vars(r)
|
||
customerID := vars["customer_id"]
|
||
listID := vars["list_id"]
|
||
|
||
if err := h.repo.AddCustomerToList(customerID, listID, userID); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to add customer to list",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "Customer added to list successfully",
|
||
})
|
||
}
|
||
|
||
func (h *CRMHandler) RemoveCustomerFromList(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
customerID := vars["customer_id"]
|
||
listID := vars["list_id"]
|
||
|
||
if err := h.repo.RemoveCustomerFromList(customerID, listID); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to remove customer from list",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "Customer removed from list successfully",
|
||
})
|
||
}
|
||
|
||
// GenerateShareToken gera um token de compartilhamento para visualização de leads de um cliente
|
||
func (h *CRMHandler) GenerateShareToken(w http.ResponseWriter, r *http.Request) {
|
||
var req struct {
|
||
CustomerID string `json:"customer_id"`
|
||
}
|
||
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid request body"})
|
||
return
|
||
}
|
||
|
||
tenantID := r.Header.Get("X-Tenant-Subdomain")
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Tenant ID is required"})
|
||
return
|
||
}
|
||
|
||
userID := r.Context().Value("user_id").(string)
|
||
|
||
// Gera token seguro
|
||
tokenBytes := make([]byte, 32)
|
||
if _, err := rand.Read(tokenBytes); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to generate token"})
|
||
return
|
||
}
|
||
token := hex.EncodeToString(tokenBytes)
|
||
|
||
shareToken := domain.CRMShareToken{
|
||
ID: uuid.New().String(),
|
||
TenantID: tenantID,
|
||
CustomerID: req.CustomerID,
|
||
Token: token,
|
||
ExpiresAt: nil, // Token sem expiração
|
||
CreatedBy: userID,
|
||
CreatedAt: time.Now(),
|
||
}
|
||
|
||
if err := h.repo.CreateShareToken(&shareToken); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to create share token",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"token": token,
|
||
})
|
||
}
|
||
|
||
// GetSharedData retorna os dados compartilhados de um cliente via token (endpoint público)
|
||
func (h *CRMHandler) GetSharedData(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
token := vars["token"]
|
||
|
||
shareToken, err := h.repo.GetShareTokenByToken(token)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid or expired token"})
|
||
return
|
||
}
|
||
|
||
// Verifica se o token expirou
|
||
if shareToken.ExpiresAt != nil && shareToken.ExpiresAt.Before(time.Now()) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusForbidden)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Token expired"})
|
||
return
|
||
}
|
||
|
||
// Busca dados do cliente
|
||
customer, err := h.repo.GetCustomerByID(shareToken.CustomerID, shareToken.TenantID)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Customer not found"})
|
||
return
|
||
}
|
||
|
||
// Busca leads do cliente
|
||
leads, err := h.repo.GetLeadsByCustomerID(shareToken.CustomerID)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to fetch leads"})
|
||
return
|
||
}
|
||
|
||
// Calcula estatísticas
|
||
stats := calculateLeadStats(leads)
|
||
|
||
response := map[string]interface{}{
|
||
"customer": map[string]string{
|
||
"name": customer.Name,
|
||
"company": customer.Company,
|
||
},
|
||
"leads": leads,
|
||
"stats": stats,
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(response)
|
||
}
|
||
|
||
func calculateLeadStats(leads []domain.CRMLead) map[string]interface{} {
|
||
stats := map[string]interface{}{
|
||
"total": len(leads),
|
||
"novo": 0,
|
||
"qualificado": 0,
|
||
"negociacao": 0,
|
||
"convertido": 0,
|
||
"perdido": 0,
|
||
"bySource": make(map[string]int),
|
||
"conversionRate": 0.0,
|
||
"thisMonth": 0,
|
||
"lastMonth": 0,
|
||
}
|
||
|
||
if len(leads) == 0 {
|
||
return stats
|
||
}
|
||
|
||
now := time.Now()
|
||
currentMonth := now.Month()
|
||
currentYear := now.Year()
|
||
lastMonth := now.AddDate(0, -1, 0)
|
||
|
||
statusCount := make(map[string]int)
|
||
bySource := make(map[string]int)
|
||
thisMonthCount := 0
|
||
lastMonthCount := 0
|
||
|
||
for _, lead := range leads {
|
||
// Conta por status
|
||
statusCount[lead.Status]++
|
||
|
||
// Conta por origem
|
||
source := lead.Source
|
||
if source == "" {
|
||
source = "manual"
|
||
}
|
||
bySource[source]++
|
||
|
||
// Conta por mês
|
||
if lead.CreatedAt.Month() == currentMonth && lead.CreatedAt.Year() == currentYear {
|
||
thisMonthCount++
|
||
}
|
||
if lead.CreatedAt.Month() == lastMonth.Month() && lead.CreatedAt.Year() == lastMonth.Year() {
|
||
lastMonthCount++
|
||
}
|
||
}
|
||
|
||
stats["novo"] = statusCount["novo"]
|
||
stats["qualificado"] = statusCount["qualificado"]
|
||
stats["negociacao"] = statusCount["negociacao"]
|
||
stats["convertido"] = statusCount["convertido"]
|
||
stats["perdido"] = statusCount["perdido"]
|
||
stats["bySource"] = bySource
|
||
stats["thisMonth"] = thisMonthCount
|
||
stats["lastMonth"] = lastMonthCount
|
||
|
||
// Taxa de conversão
|
||
if len(leads) > 0 {
|
||
conversionRate := (float64(statusCount["convertido"]) / float64(len(leads))) * 100
|
||
stats["conversionRate"] = conversionRate
|
||
}
|
||
|
||
return stats
|
||
}
|
||
|
||
// GenerateCustomerPortalAccess gera credenciais de acesso ao portal para um cliente
|
||
func (h *CRMHandler) GenerateCustomerPortalAccess(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
customerID := vars["id"]
|
||
|
||
var req struct {
|
||
Password string `json:"password"`
|
||
}
|
||
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Password == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Password is required"})
|
||
return
|
||
}
|
||
|
||
// Verificar se cliente existe
|
||
customer, err := h.repo.GetCustomerByID(customerID, tenantID)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Customer not found"})
|
||
return
|
||
}
|
||
|
||
// Gerar hash da senha
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to generate password"})
|
||
return
|
||
}
|
||
|
||
// Atualizar acesso ao portal
|
||
if err := h.repo.SetCustomerPortalAccess(customerID, string(hashedPassword), true); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to set portal access"})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"message": "Portal access granted",
|
||
"email": customer.Email,
|
||
})
|
||
}
|
||
|
||
// GetDashboard returns stats for the CRM dashboard
|
||
func (h *CRMHandler) GetDashboard(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
customerID := r.URL.Query().Get("customer_id")
|
||
log.Printf("GetDashboard: tenantID=%s, customerID=%s", tenantID, customerID)
|
||
|
||
// Get all leads for stats
|
||
leads, err := h.repo.GetLeadsByTenant(tenantID)
|
||
if err != nil {
|
||
log.Printf("GetDashboard: Error fetching leads: %v", err)
|
||
leads = []domain.CRMLead{}
|
||
}
|
||
|
||
// Get all customers for stats
|
||
customers, err := h.repo.GetCustomersByTenant(tenantID)
|
||
if err != nil {
|
||
log.Printf("GetDashboard: Error fetching customers: %v", err)
|
||
customers = []domain.CRMCustomer{}
|
||
}
|
||
|
||
// Get all lists (campaigns)
|
||
lists, err := h.repo.GetListsByTenant(tenantID)
|
||
if err != nil {
|
||
log.Printf("GetDashboard: Error fetching lists: %v", err)
|
||
}
|
||
|
||
// Filter by customer if provided
|
||
if customerID != "" {
|
||
filteredLeads := []domain.CRMLead{}
|
||
for _, lead := range leads {
|
||
if lead.CustomerID != nil && *lead.CustomerID == customerID {
|
||
filteredLeads = append(filteredLeads, lead)
|
||
}
|
||
}
|
||
log.Printf("GetDashboard: Filtered leads from %d to %d", len(leads), len(filteredLeads))
|
||
leads = filteredLeads
|
||
|
||
filteredLists := []domain.CRMListWithCustomers{}
|
||
for _, list := range lists {
|
||
if list.CustomerID != nil && *list.CustomerID == customerID {
|
||
filteredLists = append(filteredLists, list)
|
||
}
|
||
}
|
||
log.Printf("GetDashboard: Filtered lists from %d to %d", len(lists), len(filteredLists))
|
||
lists = filteredLists
|
||
}
|
||
|
||
stats := calculateLeadStats(leads)
|
||
stats["total_customers"] = len(customers)
|
||
if customerID != "" {
|
||
stats["total_customers"] = 1 // If filtered by customer, we only care about that one
|
||
}
|
||
stats["total_campaigns"] = len(lists)
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"stats": stats,
|
||
})
|
||
}
|
||
|
||
// ImportLeads handles bulk lead import from JSON
|
||
func (h *CRMHandler) ImportLeads(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||
|
||
if tenantID == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing tenant_id"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
CampaignID string `json:"campaign_id"`
|
||
CustomerID string `json:"customer_id"`
|
||
Leads []domain.CRMLead `json:"leads"`
|
||
}
|
||
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
log.Printf("ImportLeads: Error decoding body: %v", err)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid request body"})
|
||
return
|
||
}
|
||
|
||
log.Printf("ImportLeads: Received %d leads for campaign %s and customer %s", len(req.Leads), req.CampaignID, req.CustomerID)
|
||
|
||
if len(req.Leads) == 0 {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "No leads provided"})
|
||
return
|
||
}
|
||
|
||
// Get default funnel and stage
|
||
var defaultFunnelID string
|
||
var defaultStageID string
|
||
funnelID, err := h.repo.EnsureDefaultFunnel(tenantID)
|
||
if err == nil {
|
||
defaultFunnelID = funnelID
|
||
stages, err := h.repo.GetStagesByFunnelID(funnelID)
|
||
if err == nil && len(stages) > 0 {
|
||
defaultStageID = stages[0].ID
|
||
}
|
||
}
|
||
|
||
// Prepare leads for bulk insert
|
||
now := time.Now()
|
||
for i := range req.Leads {
|
||
if req.Leads[i].ID == "" {
|
||
req.Leads[i].ID = uuid.New().String()
|
||
}
|
||
req.Leads[i].TenantID = tenantID
|
||
req.Leads[i].CreatedBy = userID
|
||
req.Leads[i].CreatedAt = now
|
||
req.Leads[i].UpdatedAt = now
|
||
req.Leads[i].IsActive = true
|
||
if req.Leads[i].Status == "" {
|
||
req.Leads[i].Status = "novo"
|
||
}
|
||
if req.Leads[i].Source == "" {
|
||
req.Leads[i].Source = "import"
|
||
}
|
||
if len(req.Leads[i].SourceMeta) == 0 {
|
||
req.Leads[i].SourceMeta = json.RawMessage("{}")
|
||
}
|
||
|
||
// Assign default funnel and stage if not provided
|
||
if (req.Leads[i].FunnelID == nil || *req.Leads[i].FunnelID == "") && defaultFunnelID != "" {
|
||
req.Leads[i].FunnelID = &defaultFunnelID
|
||
}
|
||
if (req.Leads[i].StageID == nil || *req.Leads[i].StageID == "") && defaultStageID != "" {
|
||
req.Leads[i].StageID = &defaultStageID
|
||
}
|
||
|
||
log.Printf("Lead %d: SourceMeta='%s'", i, string(req.Leads[i].SourceMeta))
|
||
// If a customer_id was provided in the request, use it for all leads
|
||
if req.CustomerID != "" {
|
||
customerID := req.CustomerID
|
||
req.Leads[i].CustomerID = &customerID
|
||
}
|
||
}
|
||
|
||
// Bulk insert leads
|
||
if err := h.repo.BulkCreateLeads(req.Leads); err != nil {
|
||
log.Printf("ImportLeads: Error in BulkCreateLeads: %v", err)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to import leads", "details": err.Error()})
|
||
return
|
||
}
|
||
|
||
log.Printf("ImportLeads: Successfully created %d leads", len(req.Leads))
|
||
|
||
// If a campaign_id was provided, link all leads to it
|
||
if req.CampaignID != "" {
|
||
leadIDs := make([]string, len(req.Leads))
|
||
for i, lead := range req.Leads {
|
||
leadIDs[i] = lead.ID
|
||
}
|
||
if err := h.repo.BulkAddLeadsToList(leadIDs, req.CampaignID, userID); err != nil {
|
||
log.Printf("ImportLeads: Error in BulkAddLeadsToList: %v", err)
|
||
}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"message": "Leads imported successfully",
|
||
"count": len(req.Leads),
|
||
})
|
||
}
|
||
|
||
// ==================== FUNNELS & STAGES ====================
|
||
|
||
func (h *CRMHandler) ListFunnels(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
http.Error(w, "Missing tenant_id", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
funnels, err := h.repo.GetFunnelsByTenant(tenantID)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// If no funnels, ensure default exists
|
||
if len(funnels) == 0 {
|
||
_, err := h.repo.EnsureDefaultFunnel(tenantID)
|
||
if err == nil {
|
||
funnels, _ = h.repo.GetFunnelsByTenant(tenantID)
|
||
}
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{"funnels": funnels})
|
||
}
|
||
|
||
func (h *CRMHandler) GetFunnel(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
if tenantID == "" {
|
||
http.Error(w, "Missing tenant_id", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
vars := mux.Vars(r)
|
||
id := vars["id"]
|
||
|
||
funnel, err := h.repo.GetFunnelByID(id, tenantID)
|
||
if err != nil {
|
||
http.Error(w, "Funnel not found", http.StatusNotFound)
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{"funnel": funnel})
|
||
}
|
||
|
||
func (h *CRMHandler) CreateFunnel(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
var funnel domain.CRMFunnel
|
||
if err := json.NewDecoder(r.Body).Decode(&funnel); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
funnel.ID = uuid.New().String()
|
||
funnel.TenantID = tenantID
|
||
|
||
if err := h.repo.CreateFunnel(&funnel); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(funnel)
|
||
}
|
||
|
||
func (h *CRMHandler) UpdateFunnel(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
vars := mux.Vars(r)
|
||
id := vars["id"]
|
||
|
||
var funnel domain.CRMFunnel
|
||
if err := json.NewDecoder(r.Body).Decode(&funnel); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
funnel.ID = id
|
||
funnel.TenantID = tenantID
|
||
|
||
if err := h.repo.UpdateFunnel(&funnel); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
func (h *CRMHandler) DeleteFunnel(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
vars := mux.Vars(r)
|
||
id := vars["id"]
|
||
|
||
if err := h.repo.DeleteFunnel(id, tenantID); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
func (h *CRMHandler) ListStages(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
funnelID := vars["funnelId"]
|
||
|
||
stages, err := h.repo.GetStagesByFunnelID(funnelID)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{"stages": stages})
|
||
}
|
||
|
||
func (h *CRMHandler) CreateStage(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
funnelID := vars["funnelId"]
|
||
|
||
var stage domain.CRMFunnelStage
|
||
if err := json.NewDecoder(r.Body).Decode(&stage); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
stage.ID = uuid.New().String()
|
||
stage.FunnelID = funnelID
|
||
|
||
if err := h.repo.CreateFunnelStage(&stage); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(stage)
|
||
}
|
||
|
||
func (h *CRMHandler) UpdateStage(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
id := vars["id"]
|
||
|
||
var stage domain.CRMFunnelStage
|
||
if err := json.NewDecoder(r.Body).Decode(&stage); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
stage.ID = id
|
||
|
||
if err := h.repo.UpdateFunnelStage(&stage); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
func (h *CRMHandler) DeleteStage(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
id := vars["id"]
|
||
|
||
if err := h.repo.DeleteFunnelStage(id); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
func (h *CRMHandler) UpdateLeadStage(w http.ResponseWriter, r *http.Request) {
|
||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||
vars := mux.Vars(r)
|
||
leadID := vars["leadId"]
|
||
|
||
var req struct {
|
||
FunnelID string `json:"funnel_id"`
|
||
StageID string `json:"stage_id"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
if err := h.repo.UpdateLeadStage(leadID, tenantID, req.FunnelID, req.StageID); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
|