From 912bbdbacdcadb0244600be2f5ab0e74d814b23b Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:40:30 +0100 Subject: [PATCH] add issues endpoints --- pkg/web/handler/issues.go | 170 ++++++++++++++++++++++++++------------ 1 file changed, 116 insertions(+), 54 deletions(-) diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index bbe2fe0..a9bb23a 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -1,72 +1,134 @@ package handler import ( - "encoding/json" - "fmt" + "context" "net/http" "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/go-chi/chi/v5" ) -func IssuesRoutes(svc *service.Services) []Route { - return []Route{ - {Pattern: "/issues", Handler: GetAllIssues(svc)}, - {Pattern: "POST /create-issue", Handler: CreateIssue(svc)}, +type IssueForm struct { + Title string `form:"title" validate:"required,max=255"` + Description string `form:"description" validate:"required,max=2000"` + Status models.Status `form:"status" validate:"required,oneof=open in_progress closed"` + IssueType models.IssueType `form:"issue_type" validate:"required,oneof=task bug feature chore"` + Priority int `form:"priority" validate:"gte=0,lte=4"` +} + +func (f *IssueForm) ToIssue() models.Issue { + return models.Issue{ + Title: f.Title, + Description: f.Description, + Status: f.Status, + IssueType: f.IssueType, + Priority: f.Priority, } } -func CreateIssue(svc *service.Services) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } +func CreateIssue(w http.ResponseWriter, r *http.Request) { + svc := Services(r) + hx := HTMX(r) - // 1. Parse Form instead of JSON - if err := r.ParseForm(); err != nil { - http.Error(w, "Failed to parse form", http.StatusBadRequest) - return - } - - // 2. Map form values to your struct manually - // (Or use a library like 'gorilla/schema') - issue := models.Issue{ - Title: r.FormValue("title"), - Description: r.FormValue("description"), - Status: models.Status(r.FormValue("status")), - IssueType: models.IssueType(r.FormValue("issue_type")), - } - - err := svc.Beads.CreateIssue(r.Context(), &issue, "") - if err != nil { - http.Error(w, "Failed to create issue: "+err.Error(), http.StatusInternalServerError) - return - } - - // 3. HTMX usually expects HTML back, not JSON - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, "

Created issue: %s

", issue.Title) + form, err := ParseForm[IssueForm](r) + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return } + + if err := ValidateForm(form); err != nil { + w.WriteHeader(http.StatusUnprocessableEntity) + if hx.IsHxRequest() { + hx.WriteString("
Please fix the form errors
") + } else { + hx.WriteJSON(map[string]interface{}{"error": err.Error()}) + } + return + } + + issue := form.ToIssue() + if err := svc.Beads.CreateIssue(r.Context(), &issue, ""); err != nil { + http.Error(w, "Failed to create issue: "+err.Error(), http.StatusInternalServerError) + return + } + + if hx.IsHxRequest() { + hx.WriteString("
Issue created successfully
") + return + } + + hx.WriteJSON(map[string]any{ + "title": issue.Title, + "status": issue.Status, + }) } -func GetAllIssues(svc *service.Services) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - issues, err := svc.Beads.AllIssues(r.Context()) +func ListIssues(w http.ResponseWriter, r *http.Request) { + svc := Services(r) + hx := HTMX(r) - if err != nil { - http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError) - return - } - - jsonData, err := json.Marshal(issues) - - if err != nil { - http.Error(w, "Failed to marshal issues", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(jsonData) + issues, err := svc.Beads.AllIssues(r.Context()) + if err != nil { + http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError) + return } + + hx.WriteJSON(issues) +} + +func IssueCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + svc := Services(r) + + id := chi.URLParam(r, "id") + issue, err := svc.Beads.GetIssue(r.Context(), id) + if err != nil { + http.Error(w, "Issue not found", http.StatusNotFound) + return + } + + ctx := context.WithValue(r.Context(), "issue", issue) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + +} + +func GetIssue(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value("issue").(*models.Issue) + hx := HTMX(r) + + hx.WriteJSON(issue) +} + +func UpdateIssue(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value("issue").(*models.Issue) + svc := Services(r) + hx := HTMX(r) + + changes := make(map[string]any) + + if err := svc.Beads.UpdateIssue(r.Context(), issue.ID, changes, ""); err != nil { + http.Error(w, "Failed to update issue", http.StatusInternalServerError) + return + } + + issue, err := svc.Beads.GetIssue(r.Context(), issue.ID) + if err != nil { + http.Error(w, "Failed to retrieve updated issue", http.StatusInternalServerError) + return + } + + hx.WriteJSON(issue) +} + +func DeleteIssue(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value("issue").(*models.Issue) + + svc := Services(r) + if err := svc.Beads.DeleteIssue(r.Context(), issue.ID); err != nil { + http.Error(w, "Failed to delete issue", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) }