package services import ( "database/sql" "github.com/rede5/gohorsejobs/backend/internal/models" ) type TicketService struct { db *sql.DB } func NewTicketService(db *sql.DB) *TicketService { return &TicketService{db: db} } // Create creates a new ticket func (s *TicketService) Create(userID *int, companyID *int, req models.CreateTicketRequest) (*models.Ticket, error) { query := ` INSERT INTO tickets (user_id, company_id, subject, description, category, priority) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, status, created_at, updated_at ` category := req.Category if category == "" { category = "general" } priority := req.Priority if priority == "" { priority = "medium" } ticket := &models.Ticket{ UserID: userID, CompanyID: companyID, Subject: req.Subject, Description: req.Description, Category: category, Priority: priority, } err := s.db.QueryRow(query, userID, companyID, req.Subject, req.Description, category, priority). Scan(&ticket.ID, &ticket.Status, &ticket.CreatedAt, &ticket.UpdatedAt) if err != nil { return nil, err } return ticket, nil } // List lists all tickets with optional filters func (s *TicketService) List(status, priority string, limit, offset int) ([]models.Ticket, error) { query := ` SELECT t.id, t.user_id, t.company_id, t.subject, t.description, t.category, t.priority, t.status, t.assigned_to, t.created_at, t.updated_at, t.resolved_at, u.full_name as user_name, c.name as company_name FROM tickets t LEFT JOIN users u ON t.user_id = u.id LEFT JOIN companies c ON t.company_id = c.id WHERE ($1 = '' OR t.status = $1) AND ($2 = '' OR t.priority = $2) ORDER BY CASE t.priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, t.created_at DESC LIMIT $3 OFFSET $4 ` rows, err := s.db.Query(query, status, priority, limit, offset) if err != nil { return nil, err } defer rows.Close() var tickets []models.Ticket for rows.Next() { var t models.Ticket err := rows.Scan( &t.ID, &t.UserID, &t.CompanyID, &t.Subject, &t.Description, &t.Category, &t.Priority, &t.Status, &t.AssignedTo, &t.CreatedAt, &t.UpdatedAt, &t.ResolvedAt, &t.UserName, &t.CompanyName, ) if err != nil { return nil, err } tickets = append(tickets, t) } return tickets, nil } // GetByID gets a ticket by ID func (s *TicketService) GetByID(id int) (*models.Ticket, error) { query := ` SELECT t.id, t.user_id, t.company_id, t.subject, t.description, t.category, t.priority, t.status, t.assigned_to, t.created_at, t.updated_at, t.resolved_at, u.full_name as user_name, c.name as company_name FROM tickets t LEFT JOIN users u ON t.user_id = u.id LEFT JOIN companies c ON t.company_id = c.id WHERE t.id = $1 ` var t models.Ticket err := s.db.QueryRow(query, id).Scan( &t.ID, &t.UserID, &t.CompanyID, &t.Subject, &t.Description, &t.Category, &t.Priority, &t.Status, &t.AssignedTo, &t.CreatedAt, &t.UpdatedAt, &t.ResolvedAt, &t.UserName, &t.CompanyName, ) if err != nil { return nil, err } return &t, nil } // Update updates a ticket func (s *TicketService) Update(id int, req models.UpdateTicketRequest) (*models.Ticket, error) { query := ` UPDATE tickets SET status = COALESCE($1, status), priority = COALESCE($2, priority), assigned_to = COALESCE($3, assigned_to), updated_at = NOW(), resolved_at = CASE WHEN $1 IN ('resolved', 'closed') THEN NOW() ELSE resolved_at END WHERE id = $4 RETURNING id, user_id, company_id, subject, description, category, priority, status, assigned_to, created_at, updated_at, resolved_at ` var t models.Ticket err := s.db.QueryRow(query, req.Status, req.Priority, req.AssignedTo, id).Scan( &t.ID, &t.UserID, &t.CompanyID, &t.Subject, &t.Description, &t.Category, &t.Priority, &t.Status, &t.AssignedTo, &t.CreatedAt, &t.UpdatedAt, &t.ResolvedAt, ) if err != nil { return nil, err } return &t, nil } // AddMessage adds a message to a ticket func (s *TicketService) AddMessage(ticketID int, userID *int, req models.AddTicketMessageRequest) (*models.TicketMessage, error) { query := ` INSERT INTO ticket_messages (ticket_id, user_id, message, is_internal) VALUES ($1, $2, $3, $4) RETURNING id, created_at ` msg := &models.TicketMessage{ TicketID: ticketID, UserID: userID, Message: req.Message, IsInternal: req.IsInternal, } err := s.db.QueryRow(query, ticketID, userID, req.Message, req.IsInternal). Scan(&msg.ID, &msg.CreatedAt) if err != nil { return nil, err } // Update ticket updated_at _, _ = s.db.Exec("UPDATE tickets SET updated_at = NOW() WHERE id = $1", ticketID) return msg, nil } // GetMessages gets all messages for a ticket func (s *TicketService) GetMessages(ticketID int, includeInternal bool) ([]models.TicketMessage, error) { query := ` SELECT tm.id, tm.ticket_id, tm.user_id, tm.message, tm.is_internal, tm.created_at, u.full_name as user_name FROM ticket_messages tm LEFT JOIN users u ON tm.user_id = u.id WHERE tm.ticket_id = $1 AND ($2 OR tm.is_internal = false) ORDER BY tm.created_at ASC ` rows, err := s.db.Query(query, ticketID, includeInternal) if err != nil { return nil, err } defer rows.Close() var messages []models.TicketMessage for rows.Next() { var m models.TicketMessage err := rows.Scan(&m.ID, &m.TicketID, &m.UserID, &m.Message, &m.IsInternal, &m.CreatedAt, &m.UserName) if err != nil { return nil, err } messages = append(messages, m) } return messages, nil } // GetStats gets ticket statistics func (s *TicketService) GetStats() (*models.TicketStats, error) { stats := &models.TicketStats{} // Count by status query := ` SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status = 'open') as open, COUNT(*) FILTER (WHERE status = 'in_progress') as in_progress, COUNT(*) FILTER (WHERE status IN ('resolved', 'closed')) as resolved FROM tickets ` err := s.db.QueryRow(query).Scan(&stats.Total, &stats.Open, &stats.InProgress, &stats.Resolved) if err != nil { return nil, err } // Calculate average response time (from creation to first message) responseQuery := ` SELECT COALESCE( AVG(EXTRACT(EPOCH FROM ( (SELECT MIN(created_at) FROM ticket_messages WHERE ticket_id = t.id) - t.created_at )) / 3600), 0 ) as avg_response FROM tickets t WHERE EXISTS (SELECT 1 FROM ticket_messages WHERE ticket_id = t.id) ` _ = s.db.QueryRow(responseQuery).Scan(&stats.AvgResponse) return stats, nil } // DeleteTicket deletes a ticket (soft delete by setting status to 'closed') func (s *TicketService) Delete(id int) error { _, err := s.db.Exec(`UPDATE tickets SET status = 'closed', updated_at = NOW() WHERE id = $1`, id) return err }