diff --git a/backend/internal/api/handlers/core_handlers.go b/backend/internal/api/handlers/core_handlers.go index c298c7c..ee70c72 100644 --- a/backend/internal/api/handlers/core_handlers.go +++ b/backend/internal/api/handlers/core_handlers.go @@ -801,7 +801,7 @@ func (h *CoreHandlers) CreateTicket(w http.ResponseWriter, r *http.Request) { return } - ticket, err := h.ticketService.CreateTicket(r.Context(), userID, req.Subject, req.Priority) + ticket, err := h.ticketService.CreateTicket(r.Context(), userID, req.Subject, req.Category, req.Priority) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/backend/internal/core/dto/ticket_dto.go b/backend/internal/core/dto/ticket_dto.go index 0f30bef..35a7375 100644 --- a/backend/internal/core/dto/ticket_dto.go +++ b/backend/internal/core/dto/ticket_dto.go @@ -4,6 +4,7 @@ import "github.com/rede5/gohorsejobs/backend/internal/models" type CreateTicketRequest struct { Subject string `json:"subject"` + Category string `json:"category"` Priority string `json:"priority"` Message string `json:"message"` // Initial message } diff --git a/backend/internal/handlers/ticket_handler.go b/backend/internal/handlers/ticket_handler.go index 3130244..0aefbac 100644 --- a/backend/internal/handlers/ticket_handler.go +++ b/backend/internal/handlers/ticket_handler.go @@ -35,7 +35,7 @@ func (h *TicketHandler) CreateTicket(w http.ResponseWriter, r *http.Request) { return } - ticket, err := h.service.CreateTicket(r.Context(), userID, req.Subject, req.Priority) + ticket, err := h.service.CreateTicket(r.Context(), userID, req.Subject, req.Category, req.Priority) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/backend/internal/models/ticket.go b/backend/internal/models/ticket.go index 73bc88b..b77c18b 100644 --- a/backend/internal/models/ticket.go +++ b/backend/internal/models/ticket.go @@ -6,6 +6,7 @@ type Ticket struct { ID string `json:"id"` UserID string `json:"userId"` Subject string `json:"subject"` + Category string `json:"category"` // bug, feature, support, billing, other Status string `json:"status"` // open, in_progress, closed Priority string `json:"priority"` // low, medium, high CreatedAt time.Time `json:"createdAt"` @@ -22,6 +23,7 @@ type TicketMessage struct { type CreateTicketRequest struct { Subject string `json:"subject"` + Category string `json:"category,omitempty"` Priority string `json:"priority,omitempty"` } diff --git a/backend/internal/services/ticket_service.go b/backend/internal/services/ticket_service.go index 89f2f83..4be9b67 100644 --- a/backend/internal/services/ticket_service.go +++ b/backend/internal/services/ticket_service.go @@ -16,18 +16,21 @@ func NewTicketService(db *sql.DB) *TicketService { return &TicketService{DB: db} } -func (s *TicketService) CreateTicket(ctx context.Context, userID string, subject, priority string) (*models.Ticket, error) { +func (s *TicketService) CreateTicket(ctx context.Context, userID string, subject, category, priority string) (*models.Ticket, error) { if priority == "" { priority = "medium" } + if category == "" { + category = "other" + } query := ` - INSERT INTO tickets (user_id, subject, status, priority, created_at, updated_at) - VALUES ($1, $2, 'open', $3, NOW(), NOW()) - RETURNING id, user_id, subject, status, priority, created_at, updated_at + INSERT INTO tickets (user_id, subject, category, status, priority, created_at, updated_at) + VALUES ($1, $2, $3, 'open', $4, NOW(), NOW()) + RETURNING id, user_id, subject, category, status, priority, created_at, updated_at ` var t models.Ticket - err := s.DB.QueryRowContext(ctx, query, userID, subject, priority).Scan( - &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, + err := s.DB.QueryRowContext(ctx, query, userID, subject, category, priority).Scan( + &t.ID, &t.UserID, &t.Subject, &t.Category, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ) if err != nil { return nil, err @@ -37,7 +40,7 @@ func (s *TicketService) CreateTicket(ctx context.Context, userID string, subject func (s *TicketService) ListTickets(ctx context.Context, userID string) ([]models.Ticket, error) { query := ` - SELECT id, user_id, subject, status, priority, created_at, updated_at + SELECT id, user_id, subject, category, status, priority, created_at, updated_at FROM tickets WHERE user_id = $1 ORDER BY updated_at DESC @@ -52,7 +55,7 @@ func (s *TicketService) ListTickets(ctx context.Context, userID string) ([]model for rows.Next() { var t models.Ticket if err := rows.Scan( - &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, + &t.ID, &t.UserID, &t.Subject, &t.Category, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ); err != nil { return nil, err } @@ -64,7 +67,7 @@ func (s *TicketService) ListTickets(ctx context.Context, userID string) ([]model func (s *TicketService) GetTicket(ctx context.Context, ticketID string, userID string, isAdmin bool) (*models.Ticket, []models.TicketMessage, error) { // 1. Get Ticket queryTicket := ` - SELECT id, user_id, subject, status, priority, created_at, updated_at + SELECT id, user_id, subject, category, status, priority, created_at, updated_at FROM tickets WHERE id = $1 ` @@ -76,7 +79,7 @@ func (s *TicketService) GetTicket(ctx context.Context, ticketID string, userID s var t models.Ticket err := s.DB.QueryRowContext(ctx, queryTicket, args...).Scan( - &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, + &t.ID, &t.UserID, &t.Subject, &t.Category, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -181,11 +184,11 @@ func (s *TicketService) UpdateTicket(ctx context.Context, ticketID string, userI } args = append(args, ticketID) - query := "UPDATE tickets SET " + joinStrings(setClauses, ", ") + " WHERE id = $" + string(rune('0'+argIdx)) + " RETURNING id, user_id, subject, status, priority, created_at, updated_at" + query := "UPDATE tickets SET " + joinStrings(setClauses, ", ") + " WHERE id = $" + string(rune('0'+argIdx)) + " RETURNING id, user_id, subject, category, status, priority, created_at, updated_at" var t models.Ticket err = s.DB.QueryRowContext(ctx, query, args...).Scan( - &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, + &t.ID, &t.UserID, &t.Subject, &t.Category, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ) if err != nil { return nil, err @@ -224,7 +227,7 @@ func (s *TicketService) DeleteTicket(ctx context.Context, ticketID string) error // ListAllTickets returns all tickets (for admin) func (s *TicketService) ListAllTickets(ctx context.Context, status string) ([]models.Ticket, error) { query := ` - SELECT id, user_id, subject, status, priority, created_at, updated_at + SELECT id, user_id, subject, category, status, priority, created_at, updated_at FROM tickets ` args := []interface{}{} @@ -246,7 +249,7 @@ func (s *TicketService) ListAllTickets(ctx context.Context, status string) ([]mo for rows.Next() { var t models.Ticket if err := rows.Scan( - &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, + &t.ID, &t.UserID, &t.Subject, &t.Category, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ); err != nil { return nil, err } diff --git a/backend/internal/services/ticket_service_test.go b/backend/internal/services/ticket_service_test.go index 4afedb2..f8197ca 100644 --- a/backend/internal/services/ticket_service_test.go +++ b/backend/internal/services/ticket_service_test.go @@ -21,19 +21,19 @@ func TestTicketService_CRUD(t *testing.T) { // 1. Create Ticket mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO tickets`)). - WithArgs("user-id", "Help", "high"). - WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}). - AddRow("ticket-id", "user-id", "Help", "open", "high", time.Now(), time.Now())) + WithArgs("user-id", "Help", "support", "high"). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "category", "status", "priority", "created_at", "updated_at"}). + AddRow("ticket-id", "user-id", "Help", "support", "open", "high", time.Now(), time.Now())) - ticket, err := service.CreateTicket(ctx, "user-id", "Help", "high") + ticket, err := service.CreateTicket(ctx, "user-id", "Help", "support", "high") assert.NoError(t, err) assert.Equal(t, "ticket-id", ticket.ID) // 2. List Tickets - mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets`)). + mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, category, status, priority, created_at, updated_at FROM tickets`)). WithArgs("user-id"). - WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}). - AddRow("ticket-id", "user-id", "Help", "open", "high", time.Now(), time.Now())) + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "category", "status", "priority", "created_at", "updated_at"}). + AddRow("ticket-id", "user-id", "Help", "support", "open", "high", time.Now(), time.Now())) tickets, err := service.ListTickets(ctx, "user-id") assert.NoError(t, err) @@ -63,8 +63,8 @@ func TestTicketService_CRUD(t *testing.T) { WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow("user-id")) mock.ExpectQuery(regexp.QuoteMeta(`UPDATE tickets SET updated_at = NOW(), status = $1 WHERE id = $2`)). - WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}). - AddRow("ticket-id", "user-id", "Help", "closed", "high", time.Now(), time.Now())) + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "category", "status", "priority", "created_at", "updated_at"}). + AddRow("ticket-id", "user-id", "Help", "support", "closed", "high", time.Now(), time.Now())) _, err = service.CloseTicket(ctx, "ticket-id", "user-id", false) assert.NoError(t, err) @@ -79,10 +79,10 @@ func TestTicketService_Extended(t *testing.T) { ctx := context.Background() // 1. GetTicket (With messages) - mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets WHERE id = $1 AND user_id = $2`)). + mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, category, status, priority, created_at, updated_at FROM tickets WHERE id = $1 AND user_id = $2`)). WithArgs("ticket-id", "user-id"). - WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}). - AddRow("ticket-id", "user-id", "Subject", "open", "high", time.Now(), time.Now())) + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "category", "status", "priority", "created_at", "updated_at"}). + AddRow("ticket-id", "user-id", "Subject", "support", "open", "high", time.Now(), time.Now())) mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, ticket_id, user_id, message, created_at FROM ticket_messages WHERE ticket_id = $1 ORDER BY created_at ASC`)). WithArgs("ticket-id"). @@ -107,10 +107,10 @@ func TestTicketService_Extended(t *testing.T) { assert.NoError(t, err) // 3. ListAllTickets - mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets ORDER BY updated_at DESC`)). - WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}). - AddRow("ticket-1", "u1", "s1", "open", "low", time.Now(), time.Now()). - AddRow("ticket-2", "u2", "s2", "closed", "high", time.Now(), time.Now())) + mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, category, status, priority, created_at, updated_at FROM tickets ORDER BY updated_at DESC`)). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "category", "status", "priority", "created_at", "updated_at"}). + AddRow("ticket-1", "u1", "s1", "support", "open", "low", time.Now(), time.Now()). + AddRow("ticket-2", "u2", "s2", "bug", "closed", "high", time.Now(), time.Now())) allTickets, err := service.ListAllTickets(ctx, "") assert.NoError(t, err) diff --git a/backend/migrations/044_add_category_to_tickets.sql b/backend/migrations/044_add_category_to_tickets.sql new file mode 100644 index 0000000..4b6699e --- /dev/null +++ b/backend/migrations/044_add_category_to_tickets.sql @@ -0,0 +1,5 @@ +-- Add category column to tickets table +ALTER TABLE tickets ADD COLUMN IF NOT EXISTS category VARCHAR(50) DEFAULT 'other'; + +-- Update existing tickets to have default category +UPDATE tickets SET category = 'other' WHERE category IS NULL; diff --git a/frontend/src/app/dashboard/support/tickets/page.tsx b/frontend/src/app/dashboard/support/tickets/page.tsx index 949fa15..fcc0512 100644 --- a/frontend/src/app/dashboard/support/tickets/page.tsx +++ b/frontend/src/app/dashboard/support/tickets/page.tsx @@ -27,7 +27,7 @@ export default function TicketsPage() { const [tickets, setTickets] = useState([]) const [loading, setLoading] = useState(true) const [isCreateOpen, setIsCreateOpen] = useState(false) - const [newTicket, setNewTicket] = useState({ subject: "", priority: "medium", message: "" }) + const [newTicket, setNewTicket] = useState({ subject: "", category: "support", priority: "medium", message: "" }) const fetchTickets = async () => { try { @@ -52,7 +52,7 @@ export default function TicketsPage() { await ticketsApi.create(newTicket) toast.success("Ticket created successfully") setIsCreateOpen(false) - setNewTicket({ subject: "", priority: "medium", message: "" }) + setNewTicket({ subject: "", category: "support", priority: "medium", message: "" }) fetchTickets() // Refresh } catch (error) { console.error("Failed to create ticket", error) @@ -107,6 +107,24 @@ export default function TicketsPage() { className="col-span-3" /> +
+ + +