feat(boc): v1.0 - Complete Business Operations Center
- Go backend API with full CRUD for all modules - Rust analytics service with parallel processing - C runtime with POSIX shared memory IPC - PostgreSQL schema with 30+ tables - Redis cache, Kafka event streaming - WebSocket hub, automation engine - PDF generation, Resend email integration - JWT auth, multi-tenant - Docker Compose deployment - Nginx reverse proxy Refs: BOC-001
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type CRMHandler struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func NewCRMHandler(db *sql.DB) *CRMHandler {
|
||||
return &CRMHandler{DB: db}
|
||||
}
|
||||
|
||||
type Customer struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Company string `json:"company"`
|
||||
OrgNumber string `json:"org_number"`
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
Tags []string `json:"tags"`
|
||||
AssignedTo *string `json:"assigned_to"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CustomerInteraction struct {
|
||||
ID string `json:"id"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
Type string `json:"type"`
|
||||
Direction string `json:"direction"`
|
||||
Subject string `json:"subject"`
|
||||
Content string `json:"content"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedBy *string `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type PipelineStage struct {
|
||||
Stage string `json:"stage"`
|
||||
Count int `json:"count"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
func (h *CRMHandler) ListCustomers(w http.ResponseWriter, r *http.Request) {
|
||||
status := r.URL.Query().Get("status")
|
||||
if status == "" {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
rows, err := h.DB.Query(`
|
||||
SELECT id, name, email, phone, company, org_number, status, source, tags, assigned_to, created_at, updated_at
|
||||
FROM boc_customers
|
||||
WHERE status = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100
|
||||
`, status)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
customers := []Customer{}
|
||||
for rows.Next() {
|
||||
var c Customer
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &c.Tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
customers = append(customers, c)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"customers": customers,
|
||||
"total": len(customers),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CRMHandler) CreateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
var req Customer
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
err := h.DB.QueryRow(`
|
||||
INSERT INTO boc_customers (name, email, phone, company, org_number, status, source, tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`, req.Name, req.Email, req.Phone, req.Company, req.OrgNumber, req.Status, req.Source, req.Tags).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create customer")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": id,
|
||||
"message": "Customer created",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CRMHandler) GetCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var c Customer
|
||||
err := h.DB.QueryRow(`
|
||||
SELECT id, name, email, phone, company, org_number, status, source, tags, assigned_to, created_at, updated_at
|
||||
FROM boc_customers WHERE id = $1
|
||||
`, id).Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &c.Tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusNotFound, "customer not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, c)
|
||||
}
|
||||
|
||||
func (h *CRMHandler) UpdateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var req Customer
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.DB.Exec(`
|
||||
UPDATE boc_customers
|
||||
SET name = $1, email = $2, phone = $3, company = $4, org_number = $5,
|
||||
status = $6, source = $7, tags = $8, assigned_to = $9
|
||||
WHERE id = $10
|
||||
`, req.Name, req.Email, req.Phone, req.Company, req.OrgNumber,
|
||||
req.Status, req.Source, req.Tags, req.AssignedTo, id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to update customer")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Customer updated",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CRMHandler) DeleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
_, err := h.DB.Exec(`DELETE FROM boc_customers WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete customer")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Customer deleted",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CRMHandler) ListLeads(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.DB.Query(`
|
||||
SELECT id, name, email, phone, company, org_number, status, source, tags, assigned_to, created_at, updated_at
|
||||
FROM boc_customers
|
||||
WHERE status = 'lead'
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
leads := []Customer{}
|
||||
for rows.Next() {
|
||||
var c Customer
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &c.Tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
leads = append(leads, c)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"leads": leads,
|
||||
"total": len(leads),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CRMHandler) GetPipeline(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.DB.Query(`
|
||||
SELECT stage, COUNT(*), COALESCE(SUM(value), 0)
|
||||
FROM boc_deals
|
||||
WHERE status = 'open'
|
||||
GROUP BY stage
|
||||
ORDER BY
|
||||
CASE stage
|
||||
WHEN 'prospect' THEN 1
|
||||
WHEN 'qualified' THEN 2
|
||||
WHEN 'proposal' THEN 3
|
||||
WHEN 'negotiation' THEN 4
|
||||
WHEN 'closed_won' THEN 5
|
||||
ELSE 6
|
||||
END
|
||||
`)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
stages := []PipelineStage{}
|
||||
for rows.Next() {
|
||||
var s PipelineStage
|
||||
if err := rows.Scan(&s.Stage, &s.Count, &s.Value); err != nil {
|
||||
continue
|
||||
}
|
||||
stages = append(stages, s)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"pipeline": stages,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CRMHandler) CreateInteraction(w http.ResponseWriter, r *http.Request) {
|
||||
var req CustomerInteraction
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
metadata, _ := json.Marshal(req.Metadata)
|
||||
|
||||
var id string
|
||||
err := h.DB.QueryRow(`
|
||||
INSERT INTO boc_customer_interactions (customer_id, type, direction, subject, content, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id
|
||||
`, req.CustomerID, req.Type, req.Direction, req.Subject, req.Content, metadata).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create interaction")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": id,
|
||||
"message": "Interaction created",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CRMHandler) GetCustomerInteractions(w http.ResponseWriter, r *http.Request) {
|
||||
customerID := chi.URLParam(r, "id")
|
||||
|
||||
rows, err := h.DB.Query(`
|
||||
SELECT id, customer_id, type, direction, subject, content, metadata, created_by, created_at
|
||||
FROM boc_customer_interactions
|
||||
WHERE customer_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`, customerID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
interactions := []CustomerInteraction{}
|
||||
for rows.Next() {
|
||||
var i CustomerInteraction
|
||||
var metadata []byte
|
||||
if err := rows.Scan(&i.ID, &i.CustomerID, &i.Type, &i.Direction, &i.Subject,
|
||||
&i.Content, &metadata, &i.CreatedBy, &i.CreatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
json.Unmarshal(metadata, &i.Metadata)
|
||||
interactions = append(interactions, i)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"interactions": interactions,
|
||||
"total": len(interactions),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user