forgejo-tickets/internal/handlers/admin/tickets.go

246 lines
7.7 KiB
Go

package admin
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/mattnite/forgejo-tickets/internal/forgejo"
"github.com/mattnite/forgejo-tickets/internal/models"
"github.com/rs/zerolog/log"
)
type TicketHandler struct {
deps Dependencies
}
type ticketListRow struct {
ID uuid.UUID
Title string
Status string
RepoName string
RepoSlug string
UserEmail string
UserName string
CreatedAt interface{}
}
func (h *TicketHandler) List(c *gin.Context) {
statusFilter := c.Query("status")
// Load all ticket mappings with User and Repo joins
type ticketMapping struct {
models.Ticket
RepoName string
RepoSlug string
ForgejoOwner string
ForgejoRepo string
UserEmail string
UserName string
}
var mappings []ticketMapping
if err := h.deps.DB.Table("tickets").
Select("tickets.*, repos.name as repo_name, repos.slug as repo_slug, repos.forgejo_owner, repos.forgejo_repo, users.email as user_email, users.name as user_name").
Joins("JOIN repos ON repos.id = tickets.repo_id").
Joins("JOIN users ON users.id = tickets.user_id").
Order("tickets.created_at DESC").
Limit(100).
Scan(&mappings).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
}
// Group by repo for Forgejo API calls
type repoGroup struct {
owner string
repo string
mappings []ticketMapping
}
repoGroups := map[string]*repoGroup{}
for _, m := range mappings {
key := m.ForgejoOwner + "/" + m.ForgejoRepo
if _, ok := repoGroups[key]; !ok {
repoGroups[key] = &repoGroup{owner: m.ForgejoOwner, repo: m.ForgejoRepo}
}
repoGroups[key].mappings = append(repoGroups[key].mappings, m)
}
// Determine Forgejo API state filter
apiState := "all"
if statusFilter == "closed" {
apiState = "closed"
} else if statusFilter == "open" || statusFilter == "in_progress" {
apiState = "open"
}
// Fetch issues from Forgejo per repo and match
var tickets []ticketListRow
for _, group := range repoGroups {
issues, err := h.deps.ForgejoClient.ListIssues(group.owner, group.repo, apiState, "")
if err != nil {
log.Error().Err(err).Str("repo", group.owner+"/"+group.repo).Msg("forgejo list issues error")
for _, m := range group.mappings {
tickets = append(tickets, ticketListRow{
ID: m.ID,
Title: "Unable to load",
Status: "open",
RepoName: m.RepoName,
RepoSlug: m.RepoSlug,
UserEmail: m.UserEmail,
UserName: m.UserName,
CreatedAt: m.CreatedAt,
})
}
continue
}
issueByNumber := map[int64]*forgejo.Issue{}
for i := range issues {
issueByNumber[issues[i].Number] = &issues[i]
}
for _, m := range group.mappings {
issue, ok := issueByNumber[m.ForgejoIssueNumber]
if !ok {
continue
}
status := forgejo.DeriveStatus(issue)
// Apply client-side status filter
if statusFilter != "" && status != statusFilter {
continue
}
tickets = append(tickets, ticketListRow{
ID: m.ID,
Title: issue.Title,
Status: status,
RepoName: m.RepoName,
RepoSlug: m.RepoSlug,
UserEmail: m.UserEmail,
UserName: m.UserName,
CreatedAt: m.CreatedAt,
})
}
}
h.deps.Renderer.Render(c.Writer, c.Request, "admin/tickets/list", map[string]interface{}{
"Tickets": tickets,
"StatusFilter": statusFilter,
})
}
func (h *TicketHandler) Detail(c *gin.Context) {
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
}
var user models.User
h.deps.DB.First(&user, "id = ?", ticket.UserID)
var repo models.Repo
h.deps.DB.First(&repo, "id = ?", ticket.RepoID)
// Fetch issue and comments from Forgejo
issue, err := h.deps.ForgejoClient.GetIssue(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber)
if err != nil {
log.Error().Err(err).Msg("forgejo get issue error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusServiceUnavailable, "Unable to load ticket details")
return
}
rawComments, err := h.deps.ForgejoClient.ListIssueComments(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber)
if err != nil {
log.Error().Err(err).Msg("forgejo list comments error")
rawComments = nil
}
cleanBody, _ := forgejo.StripCommentFooter(issue.Body)
comments := forgejo.BuildCommentViews(rawComments, h.deps.ForgejoClient.BotLogin)
status := forgejo.DeriveStatus(issue)
h.deps.Renderer.Render(c.Writer, c.Request, "admin/tickets/detail", map[string]interface{}{
"Ticket": map[string]interface{}{
"ID": ticket.ID,
"Title": issue.Title,
"Description": cleanBody,
"Status": status,
"ForgejoIssueNumber": ticket.ForgejoIssueNumber,
"CreatedAt": ticket.CreatedAt,
},
"User": user,
"Repo": repo,
"Comments": comments,
})
}
func (h *TicketHandler) UpdateStatus(c *gin.Context) {
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
}
var repo models.Repo
if err := h.deps.DB.First(&repo, "id = ?", ticket.RepoID).Error; err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to find product")
return
}
newStatus := c.PostForm("status")
// Get or create the in_progress label
inProgressLabel, err := h.deps.ForgejoClient.GetLabel(repo.ForgejoOwner, repo.ForgejoRepo, "in_progress")
if err != nil {
log.Error().Err(err).Msg("forgejo get/create in_progress label error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusServiceUnavailable, "Service temporarily unavailable")
return
}
switch newStatus {
case "open":
if err := h.deps.ForgejoClient.EditIssue(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber, forgejo.EditIssueRequest{State: "open"}); err != nil {
log.Error().Err(err).Msg("forgejo edit issue error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusServiceUnavailable, "Failed to update status")
return
}
h.deps.ForgejoClient.RemoveLabel(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber, inProgressLabel.ID)
case "in_progress":
if err := h.deps.ForgejoClient.EditIssue(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber, forgejo.EditIssueRequest{State: "open"}); err != nil {
log.Error().Err(err).Msg("forgejo edit issue error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusServiceUnavailable, "Failed to update status")
return
}
if err := h.deps.ForgejoClient.AddLabels(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber, []int64{inProgressLabel.ID}); err != nil {
log.Error().Err(err).Msg("forgejo add label error")
}
case "closed":
if err := h.deps.ForgejoClient.EditIssue(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber, forgejo.EditIssueRequest{State: "closed"}); err != nil {
log.Error().Err(err).Msg("forgejo edit issue error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusServiceUnavailable, "Failed to update status")
return
}
h.deps.ForgejoClient.RemoveLabel(repo.ForgejoOwner, repo.ForgejoRepo, ticket.ForgejoIssueNumber, inProgressLabel.ID)
}
c.Redirect(http.StatusSeeOther, "/tickets/"+ticketID.String())
}