package comm import ( "database/sql" "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/rs/zerolog/log" ) // ConversationHandler hanterar konversationer type ConversationHandler struct { db *sql.DB } // NewConversationHandler skapar en ny handler func NewConversationHandler(db *sql.DB) *ConversationHandler { return &ConversationHandler{db: db} } // Conversation representerar en konversation type Conversation struct { ID uuid.UUID `json:"id"` TenantID uuid.UUID `json:"tenant_id"` ContactID uuid.UUID `json:"contact_id"` Channel string `json:"channel"` Status string `json:"status"` AssignedTo *uuid.UUID `json:"assigned_to,omitempty"` AssignedTeam string `json:"assigned_team,omitempty"` LeadID *uuid.UUID `json:"lead_id,omitempty"` Tags []string `json:"tags,omitempty"` Priority string `json:"priority"` LastActivityAt time.Time `json:"last_activity_at"` NextActionAt *time.Time `json:"next_action_at,omitempty"` NextActionType string `json:"next_action_type,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Contact *ContactBrief `json:"contact,omitempty"` MessageCount int `json:"message_count"` UnreadCount int `json:"unread_count"` } type ContactBrief struct { ID uuid.UUID `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Email string `json:"email"` Phone string `json:"phone"` } // Message representerar ett meddelande type Message struct { ID uuid.UUID `json:"id"` ConversationID uuid.UUID `json:"conversation_id"` ContactID uuid.UUID `json:"contact_id"` Direction string `json:"direction"` Content string `json:"content"` ContentType string `json:"content_type"` SenderType string `json:"sender_type"` SenderID *uuid.UUID `json:"sender_id,omitempty"` ReadAt *time.Time `json:"read_at,omitempty"` CreatedAt time.Time `json:"created_at"` } // CreateConversationRequest type CreateConversationRequest struct { ContactID uuid.UUID `json:"contact_id"` Channel string `json:"channel"` ChannelID string `json:"channel_conversation_id,omitempty"` Priority string `json:"priority,omitempty"` AssignedTo uuid.UUID `json:"assigned_to,omitempty"` AssignedTeam string `json:"assigned_team,omitempty"` } // SendMessageRequest type SendMessageRequest struct { Content string `json:"content"` ContentType string `json:"content_type,omitempty"` Direction string `json:"direction"` SenderType string `json:"sender_type,omitempty"` } // CreateConversation skapar en ny konversation func (h *ConversationHandler) CreateConversation(w http.ResponseWriter, r *http.Request) { var req CreateConversationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } tenantID := getTenantID(r) if tenantID == uuid.Nil { writeError(w, http.StatusUnauthorized, "missing tenant") return } var conv Conversation err := h.db.QueryRow(` INSERT INTO conversations (tenant_id, contact_id, channel, channel_conversation_id, status, priority, assigned_to, assigned_team) VALUES ($1, $2, $3, $4, 'new', COALESCE($5, 'normal'), $6, $7) RETURNING id, tenant_id, contact_id, channel, status, assigned_to, assigned_team, priority, last_activity_at, created_at, updated_at `, tenantID, req.ContactID, req.Channel, req.ChannelID, req.Priority, nullUUID(req.AssignedTo), nullString(req.AssignedTeam)).Scan( &conv.ID, &conv.TenantID, &conv.ContactID, &conv.Channel, &conv.Status, &conv.AssignedTo, &conv.AssignedTeam, &conv.Priority, &conv.LastActivityAt, &conv.CreatedAt, &conv.UpdatedAt, ) if err != nil { log.Error().Err(err).Msg("Failed to create conversation") writeError(w, http.StatusInternalServerError, "failed to create conversation") return } writeJSON(w, http.StatusCreated, conv) } // ListConversations listar konversationer med filtering func (h *ConversationHandler) ListConversations(w http.ResponseWriter, r *http.Request) { tenantID := getTenantID(r) if tenantID == uuid.Nil { writeError(w, http.StatusUnauthorized, "missing tenant") return } status := r.URL.Query().Get("status") assignedTo := r.URL.Query().Get("assigned_to") channel := r.URL.Query().Get("channel") query := ` SELECT c.id, c.tenant_id, c.contact_id, c.channel, c.status, c.assigned_to, c.assigned_team, c.priority, c.last_activity_at, c.next_action_at, c.next_action_type, c.created_at, c.updated_at, co.id, co.first_name, co.last_name, co.email, co.phone, (SELECT COUNT(*) FROM messages WHERE conversation_id = c.id) as message_count, (SELECT COUNT(*) FROM messages WHERE conversation_id = c.id AND direction = 'inbound' AND read_at IS NULL) as unread_count FROM conversations c JOIN contacts co ON c.contact_id = co.id WHERE c.tenant_id = $1 ` args := []interface{}{tenantID} argCount := 1 if status != "" { argCount++ query += " AND c.status = $" + string(rune('0'+argCount)) args = append(args, status) } if assignedTo != "" { argCount++ query += " AND c.assigned_to = $" + string(rune('0'+argCount)) args = append(args, assignedTo) } if channel != "" { argCount++ query += " AND c.channel = $" + string(rune('0'+argCount)) args = append(args, channel) } query += " ORDER BY c.last_activity_at DESC LIMIT 100" rows, err := h.db.Query(query, args...) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() var conversations []Conversation for rows.Next() { var conv Conversation var contact ContactBrief rows.Scan( &conv.ID, &conv.TenantID, &conv.ContactID, &conv.Channel, &conv.Status, &conv.AssignedTo, &conv.AssignedTeam, &conv.Priority, &conv.LastActivityAt, &conv.NextActionAt, &conv.NextActionType, &conv.CreatedAt, &conv.UpdatedAt, &contact.ID, &contact.FirstName, &contact.LastName, &contact.Email, &contact.Phone, &conv.MessageCount, &conv.UnreadCount, ) conv.Contact = &contact conversations = append(conversations, conv) } writeJSON(w, http.StatusOK, map[string]interface{}{"conversations": conversations}) } // GetConversation hämtar en konversation med meddelanden func (h *ConversationHandler) GetConversation(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } var conv Conversation err = h.db.QueryRow(` SELECT id, tenant_id, contact_id, channel, status, assigned_to, assigned_team, priority, last_activity_at, next_action_at, next_action_type, created_at, updated_at FROM conversations WHERE id = $1 `, id).Scan( &conv.ID, &conv.TenantID, &conv.ContactID, &conv.Channel, &conv.Status, &conv.AssignedTo, &conv.AssignedTeam, &conv.Priority, &conv.LastActivityAt, &conv.NextActionAt, &conv.NextActionType, &conv.CreatedAt, &conv.UpdatedAt, ) if err != nil { writeError(w, http.StatusNotFound, "conversation not found") return } // Hämta meddelanden rows, err := h.db.Query(` SELECT id, conversation_id, contact_id, direction, content, content_type, sender_type, sender_id, read_at, created_at FROM messages WHERE conversation_id = $1 ORDER BY created_at ASC `, id) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() var messages []Message for rows.Next() { var msg Message rows.Scan(&msg.ID, &msg.ConversationID, &msg.ContactID, &msg.Direction, &msg.Content, &msg.ContentType, &msg.SenderType, &msg.SenderID, &msg.ReadAt, &msg.CreatedAt) messages = append(messages, msg) } writeJSON(w, http.StatusOK, map[string]interface{}{ "conversation": conv, "messages": messages, }) } // SendMessage skickar ett meddelande i en konversation func (h *ConversationHandler) SendMessage(w http.ResponseWriter, r *http.Request) { convID, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid conversation id") return } var req SendMessageRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } // Hämta conversation för att få contact_id var contactID uuid.UUID err = h.db.QueryRow("SELECT contact_id FROM conversations WHERE id = $1", convID).Scan(&contactID) if err != nil { writeError(w, http.StatusNotFound, "conversation not found") return } // Sätt sender från auth context var senderID *uuid.UUID if userID, ok := r.Context().Value("user_id").(uuid.UUID); ok { senderID = &userID } var msg Message err = h.db.QueryRow(` INSERT INTO messages (conversation_id, contact_id, direction, content, content_type, sender_type, sender_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, conversation_id, contact_id, direction, content, content_type, sender_type, sender_id, created_at `, convID, contactID, req.Direction, req.Content, req.ContentType, req.SenderType, senderID).Scan( &msg.ID, &msg.ConversationID, &msg.ContactID, &msg.Direction, &msg.Content, &msg.ContentType, &msg.SenderType, &msg.SenderID, &msg.CreatedAt, ) if err != nil { log.Error().Err(err).Msg("Failed to create message") writeError(w, http.StatusInternalServerError, "failed to create message") return } // Uppdatera conversation last_activity h.db.Exec(` UPDATE conversations SET last_activity_at = NOW(), updated_at = NOW() WHERE id = $1 `, convID) writeJSON(w, http.StatusCreated, msg) } // UpdateConversationStatus uppdaterar status func (h *ConversationHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } var req struct { Status string `json:"status"` AssignedTo uuid.UUID `json:"assigned_to,omitempty"` AssignedTeam string `json:"assigned_team,omitempty"` Priority string `json:"priority,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } _, err = h.db.Exec(` UPDATE conversations SET status = COALESCE(NULLIF($1, ''), status), assigned_to = COALESCE($2, assigned_to), assigned_team = COALESCE(NULLIF($3, ''), assigned_team), priority = COALESCE(NULLIF($4, ''), priority), updated_at = NOW() WHERE id = $5 `, req.Status, nullUUID(req.AssignedTo), req.AssignedTeam, req.Priority, id) if err != nil { writeError(w, http.StatusInternalServerError, "update failed") return } writeJSON(w, http.StatusOK, map[string]interface{}{"updated": true}) } // Helper functions func nullUUID(u uuid.UUID) interface{} { if u == uuid.Nil { return nil } return u } func nullString(s string) interface{} { if s == "" { return nil } return s }