package public import ( "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/mattnite/forgejo-tickets/internal/auth" "github.com/mattnite/forgejo-tickets/internal/forgejo" "github.com/mattnite/forgejo-tickets/internal/models" "github.com/rs/zerolog/log" ) type TicketHandler struct { deps Dependencies } func (h *TicketHandler) List(c *gin.Context) { user := auth.CurrentUser(c) var tickets []models.Ticket if err := h.deps.DB.Preload("Repo").Where("user_id = ?", user.ID).Order("created_at DESC").Limit(50).Find(&tickets).Error; err != nil { log.Error().Err(err).Msg("list tickets error") h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to load tickets") return } h.deps.Renderer.Render(c.Writer, c.Request, "tickets/list", map[string]interface{}{ "Tickets": tickets, }) } func (h *TicketHandler) NewForm(c *gin.Context) { user := auth.CurrentUser(c) var repos []models.Repo if err := h.deps.DB. Joins("JOIN user_repos ON user_repos.repo_id = repos.id"). Where("user_repos.user_id = ? AND repos.active = ?", user.ID, true). Order("repos.name ASC"). Find(&repos).Error; err != nil { log.Error().Err(err).Msg("list repos error") h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to load products") return } h.deps.Renderer.Render(c.Writer, c.Request, "tickets/new", map[string]interface{}{ "Repos": repos, }) } func (h *TicketHandler) Create(c *gin.Context) { user := auth.CurrentUser(c) repoID, err := uuid.Parse(c.PostForm("repo_id")) if err != nil { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid product selection") return } // Validate the user has access to this repo var userRepo models.UserRepo if err := h.deps.DB.Where("user_id = ? AND repo_id = ?", user.ID, repoID).First(&userRepo).Error; err != nil { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusForbidden, "You do not have access to this project") return } title := c.PostForm("title") description := c.PostForm("description") if title == "" || description == "" { var repos []models.Repo h.deps.DB. Joins("JOIN user_repos ON user_repos.repo_id = repos.id"). Where("user_repos.user_id = ? AND repos.active = ?", user.ID, true). Order("repos.name ASC"). Find(&repos) h.deps.Renderer.Render(c.Writer, c.Request, "tickets/new", map[string]interface{}{ "Repos": repos, "Error": "Title and description are required", "Title": title, "Description": description, "RepoID": repoID.String(), }) return } ticket := models.Ticket{ UserID: user.ID, RepoID: repoID, Title: title, Description: description, } if err := h.deps.DB.Create(&ticket).Error; err != nil { log.Error().Err(err).Msg("create ticket error") h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to create ticket") return } // Async Forgejo issue creation var repo models.Repo if err := h.deps.DB.First(&repo, "id = ?", repoID).Error; err == nil { go func() { // Look up or create the "customer" label var labelIDs []int64 label, err := h.deps.ForgejoClient.GetOrCreateLabel(repo.ForgejoOwner, repo.ForgejoRepo, "customer", "#0075ca") if err != nil { log.Error().Err(err).Msg("forgejo get/create label error") } else { labelIDs = append(labelIDs, label.ID) } issue, err := h.deps.ForgejoClient.CreateIssue(repo.ForgejoOwner, repo.ForgejoRepo, forgejo.CreateIssueRequest{ Title: title, Body: description + "\n\n---\n*Submitted by: " + user.Email + "*", Labels: labelIDs, }) if err != nil { log.Error().Err(err).Msgf("forgejo create issue error for ticket %s", ticket.ID) return } h.deps.DB.Model(&ticket).Update("forgejo_issue_number", issue.Number) }() } c.Redirect(http.StatusSeeOther, "/tickets/"+ticket.ID.String()) } func (h *TicketHandler) Detail(c *gin.Context) { user := auth.CurrentUser(c) ticketID, err := uuid.Parse(c.Param("id")) if err != nil { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid ticket ID") return } var ticket models.Ticket if err := h.deps.DB.First(&ticket, "id = ?", ticketID).Error; err != nil { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusNotFound, "Ticket not found") return } if ticket.UserID != user.ID { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusForbidden, "Access denied") return } var repo models.Repo h.deps.DB.First(&repo, "id = ?", ticket.RepoID) var comments []struct { models.TicketComment UserName string UserEmail string } h.deps.DB.Table("ticket_comments"). Select("ticket_comments.*, users.name as user_name, users.email as user_email"). Joins("JOIN users ON users.id = ticket_comments.user_id"). Where("ticket_comments.ticket_id = ?", ticket.ID). Order("ticket_comments.created_at ASC"). Scan(&comments) h.deps.Renderer.Render(c.Writer, c.Request, "tickets/detail", map[string]interface{}{ "Ticket": ticket, "Repo": repo, "Comments": comments, }) } func (h *TicketHandler) AddComment(c *gin.Context) { user := auth.CurrentUser(c) ticketID, err := uuid.Parse(c.Param("id")) if err != nil { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid ticket ID") return } var ticket models.Ticket if err := h.deps.DB.First(&ticket, "id = ?", ticketID).Error; err != nil { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusNotFound, "Ticket not found") return } if ticket.UserID != user.ID { h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusForbidden, "Access denied") return } body := c.PostForm("body") if body == "" { c.Redirect(http.StatusSeeOther, "/tickets/"+ticket.ID.String()) return } comment := models.TicketComment{ TicketID: ticket.ID, UserID: user.ID, Body: body, } if err := h.deps.DB.Create(&comment).Error; err != nil { log.Error().Err(err).Msg("create comment error") h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to add comment") return } // Async sync to Forgejo if ticket.ForgejoIssueNumber != nil { var repo models.Repo if err := h.deps.DB.First(&repo, "id = ?", ticket.RepoID).Error; err == nil { go func() { forgejoComment, err := h.deps.ForgejoClient.CreateComment(repo.ForgejoOwner, repo.ForgejoRepo, *ticket.ForgejoIssueNumber, forgejo.CreateCommentRequest{ Body: body + "\n\n---\n*Comment by: " + user.Email + "*", }) if err != nil { log.Error().Err(err).Msg("forgejo create comment error") return } h.deps.DB.Model(&comment).Update("forgejo_comment_id", forgejoComment.ID) }() } } c.Redirect(http.StatusSeeOther, "/tickets/"+ticket.ID.String()) }