package handlers import ( "Yimaru-Backend/internal/domain" "context" "encoding/json" "fmt" "strconv" "github.com/gofiber/fiber/v2" ) type createIssueReq struct { Subject string `json:"subject" validate:"required"` Description string `json:"description" validate:"required"` IssueType string `json:"issue_type" validate:"required"` Metadata map[string]interface{} `json:"metadata,omitempty"` } type issueRes struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` UserRole string `json:"user_role"` Subject string `json:"subject"` Description string `json:"description"` IssueType string `json:"issue_type"` Status string `json:"status"` Metadata map[string]interface{} `json:"metadata,omitempty"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type updateIssueStatusReq struct { Status string `json:"status" validate:"required,oneof=pending in_progress resolved rejected"` } type issueListRes struct { Issues []issueRes `json:"issues"` TotalCount int64 `json:"total_count"` } func mapIssueToRes(issue domain.ReportedIssue) issueRes { return issueRes{ ID: issue.ID, UserID: issue.UserID, UserRole: string(issue.UserRole), Subject: issue.Subject, Description: issue.Description, IssueType: string(issue.IssueType), Status: string(issue.Status), Metadata: issue.Metadata, CreatedAt: issue.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), UpdatedAt: issue.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), } } // CreateIssue godoc // @Summary Report an issue // @Description Allows any authenticated user to report an issue they encountered // @Tags issues // @Accept json // @Produce json // @Security Bearer // @Param body body createIssueReq true "Issue report payload" // @Success 201 {object} domain.Response{data=issueRes} // @Failure 400 {object} domain.ErrorResponse // @Failure 401 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/issues [post] func (h *Handler) CreateIssue(c *fiber.Ctx) error { var req createIssueReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if valErrs, ok := h.validator.Validate(c, req); !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Validation failed", Error: errMsg, }) } userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) issueReq := domain.ReportedIssueReq{ Subject: req.Subject, Description: req.Description, IssueType: domain.ReportedIssueType(req.IssueType), Metadata: req.Metadata, } issue, err := h.issueReportingSvc.CreateReportedIssue(c.Context(), issueReq, userID, role) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create issue report", Error: err.Error(), }) } actorRole := string(role) ip := c.IP() ua := c.Get("User-Agent") meta, _ := json.Marshal(map[string]interface{}{"subject": issue.Subject, "issue_type": string(issue.IssueType)}) go h.activityLogSvc.RecordAction(context.Background(), &userID, &actorRole, domain.ActionIssueCreated, domain.ResourceIssue, &issue.ID, "Reported issue: "+issue.Subject, meta, &ip, &ua) go func() { admins, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: string(domain.RoleAdmin)}) if err != nil { return } for _, admin := range admins { h.sendInAppNotification(admin.ID, domain.NOTIFICATION_TYPE_ISSUE_CREATED, "New Issue Reported", "A new issue \""+issue.Subject+"\" has been reported.") } }() return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Issue reported successfully", Data: mapIssueToRes(issue), Success: true, }) } // GetIssueByID godoc // @Summary Get issue by ID // @Description Returns a single issue report by its ID (admin only) // @Tags issues // @Produce json // @Security Bearer // @Param id path int true "Issue ID" // @Success 200 {object} domain.Response{data=issueRes} // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Router /api/v1/issues/{id} [get] func (h *Handler) GetIssueByID(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid issue ID", Error: err.Error(), }) } issue, err := h.issueReportingSvc.GetIssueByID(c.Context(), id) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Issue not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Issue retrieved successfully", Data: mapIssueToRes(issue), Success: true, }) } // GetUserIssues godoc // @Summary Get issues for a specific user // @Description Returns paginated issues reported by a specific user (admin only) // @Tags issues // @Produce json // @Security Bearer // @Param user_id path int true "User ID" // @Param limit query int false "Limit" default(20) // @Param offset query int false "Offset" default(0) // @Success 200 {object} domain.Response{data=issueListRes} // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/issues/user/{user_id} [get] func (h *Handler) GetUserIssues(c *fiber.Ctx) error { userIDStr := c.Params("user_id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid user ID", Error: err.Error(), }) } limit, _ := strconv.Atoi(c.Query("limit", "20")) offset, _ := strconv.Atoi(c.Query("offset", "0")) if limit > 100 { limit = 100 } issues, err := h.issueReportingSvc.GetIssuesForUser(c.Context(), userID, limit, offset) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve user issues", Error: err.Error(), }) } totalCount, _ := h.issueReportingSvc.CountIssuesByUser(c.Context(), userID) var issueResponses []issueRes for _, issue := range issues { issueResponses = append(issueResponses, mapIssueToRes(issue)) } return c.JSON(domain.Response{ Message: "User issues retrieved successfully", Data: issueListRes{ Issues: issueResponses, TotalCount: totalCount, }, Success: true, }) } // GetMyIssues godoc // @Summary Get my reported issues // @Description Returns paginated issues reported by the authenticated user // @Tags issues // @Produce json // @Security Bearer // @Param limit query int false "Limit" default(20) // @Param offset query int false "Offset" default(0) // @Success 200 {object} domain.Response{data=issueListRes} // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/issues/me [get] func (h *Handler) GetMyIssues(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) limit, _ := strconv.Atoi(c.Query("limit", "20")) offset, _ := strconv.Atoi(c.Query("offset", "0")) if limit > 100 { limit = 100 } issues, err := h.issueReportingSvc.GetIssuesForUser(c.Context(), userID, limit, offset) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve issues", Error: err.Error(), }) } totalCount, _ := h.issueReportingSvc.CountIssuesByUser(c.Context(), userID) var issueResponses []issueRes for _, issue := range issues { issueResponses = append(issueResponses, mapIssueToRes(issue)) } return c.JSON(domain.Response{ Message: "Issues retrieved successfully", Data: issueListRes{ Issues: issueResponses, TotalCount: totalCount, }, Success: true, }) } // GetAllIssues godoc // @Summary Get all issues // @Description Returns all reported issues with pagination (admin only) // @Tags issues // @Produce json // @Security Bearer // @Param limit query int false "Limit" default(20) // @Param offset query int false "Offset" default(0) // @Success 200 {object} domain.Response{data=issueListRes} // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/issues [get] func (h *Handler) GetAllIssues(c *fiber.Ctx) error { limit, _ := strconv.Atoi(c.Query("limit", "20")) offset, _ := strconv.Atoi(c.Query("offset", "0")) if limit > 100 { limit = 100 } issues, err := h.issueReportingSvc.GetAllIssues(c.Context(), limit, offset) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve issues", Error: err.Error(), }) } totalCount, _ := h.issueReportingSvc.CountAllIssues(c.Context()) var issueResponses []issueRes for _, issue := range issues { issueResponses = append(issueResponses, mapIssueToRes(issue)) } return c.JSON(domain.Response{ Message: "Issues retrieved successfully", Data: issueListRes{ Issues: issueResponses, TotalCount: totalCount, }, Success: true, }) } // UpdateIssueStatus godoc // @Summary Update issue status // @Description Updates the status of an issue (admin only) // @Tags issues // @Accept json // @Produce json // @Security Bearer // @Param id path int true "Issue ID" // @Param body body updateIssueStatusReq true "Status update payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/issues/{id}/status [patch] func (h *Handler) UpdateIssueStatus(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid issue ID", Error: err.Error(), }) } var req updateIssueStatusReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if valErrs, ok := h.validator.Validate(c, req); !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Validation failed", Error: errMsg, }) } if err := h.issueReportingSvc.UpdateIssueStatus(c.Context(), id, req.Status); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update issue status", Error: err.Error(), }) } actorID := c.Locals("user_id").(int64) actorRole := string(c.Locals("role").(domain.Role)) ip := c.IP() ua := c.Get("User-Agent") meta, _ := json.Marshal(map[string]interface{}{"issue_id": id, "new_status": req.Status}) go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionIssueStatusUpdated, domain.ResourceIssue, &id, fmt.Sprintf("Updated issue %d status to %s", id, req.Status), meta, &ip, &ua) go func() { issue, err := h.issueReportingSvc.GetIssueByID(context.Background(), id) if err != nil { return } h.sendInAppNotification(issue.UserID, domain.NOTIFICATION_TYPE_ISSUE_STATUS_UPDATED, "Issue Status Updated", fmt.Sprintf("Your issue \"%s\" has been updated to: %s", issue.Subject, req.Status)) }() return c.JSON(domain.Response{ Message: "Issue status updated successfully", Success: true, }) } // DeleteIssue godoc // @Summary Delete an issue // @Description Deletes an issue report (admin only) // @Tags issues // @Produce json // @Security Bearer // @Param id path int true "Issue ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/issues/{id} [delete] func (h *Handler) DeleteIssue(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid issue ID", Error: err.Error(), }) } if err := h.issueReportingSvc.DeleteIssue(c.Context(), id); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete issue", Error: err.Error(), }) } actorID := c.Locals("user_id").(int64) actorRole := string(c.Locals("role").(domain.Role)) ip := c.IP() ua := c.Get("User-Agent") meta, _ := json.Marshal(map[string]interface{}{"issue_id": id}) go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionIssueDeleted, domain.ResourceIssue, &id, fmt.Sprintf("Deleted issue ID: %d", id), meta, &ip, &ua) return c.JSON(domain.Response{ Message: "Issue deleted successfully", Success: true, }) }