Spaces:
Sleeping
Sleeping
| package controllers | |
| import ( | |
| "strconv" | |
| "abdanhafidz.com/go-boilerplate/models/dto" | |
| entity "abdanhafidz.com/go-boilerplate/models/entity" | |
| http_error "abdanhafidz.com/go-boilerplate/models/error" | |
| "abdanhafidz.com/go-boilerplate/services" | |
| "github.com/gin-gonic/gin" | |
| "github.com/google/uuid" | |
| ) | |
| type AdminEventController interface { | |
| ListEvents(ctx *gin.Context) | |
| CreateEvent(ctx *gin.Context) | |
| UpdateEvent(ctx *gin.Context) | |
| DeleteEvent(ctx *gin.Context) | |
| ListParticipants(ctx *gin.Context) | |
| ListCandidates(ctx *gin.Context) | |
| ListExamCandidates(ctx *gin.Context) | |
| AddParticipant(ctx *gin.Context) | |
| RemoveParticipant(ctx *gin.Context) | |
| ListEventExams(ctx *gin.Context) | |
| RemoveExam(ctx *gin.Context) | |
| ListResults(ctx *gin.Context) | |
| GetAttemptReview(ctx *gin.Context) | |
| UpdateResult(ctx *gin.Context) | |
| DeleteResult(ctx *gin.Context) | |
| } | |
| type adminEventController struct { | |
| adminEventService services.AdminEventService | |
| } | |
| func NewAdminEventController(adminEventService services.AdminEventService) AdminEventController { | |
| return &adminEventController{adminEventService: adminEventService} | |
| } | |
| // ListEvents godoc | |
| // @Summary Admin: List Events | |
| // @Description Admin view of all events with participant count, exam count and optional filters/pagination | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param limit query int false "Items per page" default(10) | |
| // @Param page query int false "Page number" default(1) | |
| // @Param search query string false "Search by title / slug / event code" | |
| // @Param sortBy query string false "Sort field (title, start_event, end_event, created_at, participant_count, exam_count)" | |
| // @Param order query string false "Sort direction (asc / desc)" | |
| // @Param status query string false "Filter by status (UPCOMING, ONGOING, ENDED)" | |
| // @Success 200 {object} dto.SuccessResponse[[]dto.AdminEventResponse] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events [get] | |
| func (c *adminEventController) ListEvents(ctx *gin.Context) { | |
| limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) | |
| page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) | |
| search := ctx.DefaultQuery("search", "") | |
| sortBy := ctx.DefaultQuery("sortBy", "") | |
| order := ctx.DefaultQuery("order", "") | |
| if limit < 1 { | |
| limit = 10 | |
| } else if limit > 100 { | |
| limit = 100 | |
| } | |
| if page < 1 { | |
| page = 1 | |
| } | |
| var status *string | |
| if val := ctx.Query("status"); val != "" { | |
| if val == entity.EventStatusUpcoming || val == entity.EventStatusOngoing || val == entity.EventStatusEnded { | |
| status = &val | |
| } | |
| } | |
| offset := (page - 1) * limit | |
| p := entity.Pagination{ | |
| Limit: limit, | |
| Offset: offset, | |
| Search: search, | |
| SortBy: sortBy, | |
| Order: order, | |
| Status: status, | |
| } | |
| list, total, err := c.adminEventService.ListEvents(ctx.Request.Context(), p) | |
| if err != nil { | |
| ResponseJSON[any, any](ctx, nil, nil, err) | |
| return | |
| } | |
| var totalPages int | |
| if total == 0 { | |
| totalPages = 1 | |
| } else { | |
| totalPages = int((total + int64(limit) - 1) / int64(limit)) | |
| } | |
| if page > totalPages { | |
| page = totalPages | |
| } | |
| meta := gin.H{ | |
| "totalItems": total, | |
| "totalPages": totalPages, | |
| "currentPage": page, | |
| "limit": limit, | |
| } | |
| ResponseJSON(ctx, meta, list, nil) | |
| } | |
| // GetAttemptReview godoc | |
| // @Summary Admin: Review Exam Attempt | |
| // @Description Review a specific participant attempt for an event exam | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param attempt_id path string true "Attempt ID" | |
| // @Param user_id path string true "User ID" | |
| // @Success 200 {object} dto.SuccessResponse[dto.AdminExamReviewResponse] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/attempts/{attempt_id}/{user_id}/review [get] | |
| func (c *adminEventController) GetAttemptReview(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| attemptId, err := uuid.Parse(ctx.Param("attempt_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"attempt_id": ctx.Param("attempt_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| userId, err := uuid.Parse(ctx.Param("user_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"user_id": ctx.Param("user_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| review, reviewErr := c.adminEventService.GetAttemptReview(ctx.Request.Context(), eventId, attemptId, userId) | |
| ResponseJSON(ctx, gin.H{"event_id": eventId, "attempt_id": attemptId, "user_id": userId}, review, reviewErr) | |
| } | |
| // CreateEvent godoc | |
| // @Summary Admin: Create Event | |
| // @Description Create a new event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param request body dto.CreateEventRequest true "Create Event Request" | |
| // @Success 200 {object} dto.SuccessResponse[entity.Events] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events [post] | |
| func (c *adminEventController) CreateEvent(ctx *gin.Context) { | |
| req := RequestJSON[dto.CreateEventRequest](ctx) | |
| res, err := c.adminEventService.CreateEvent(ctx.Request.Context(), req) | |
| ResponseJSON(ctx, req, res, err) | |
| } | |
| // UpdateEvent godoc | |
| // @Summary Admin: Update Event | |
| // @Description Update an existing event by ID | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param request body dto.UpdateEventRequest true "Update Event Request" | |
| // @Success 200 {object} dto.SuccessResponse[entity.Events] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id} [put] | |
| func (c *adminEventController) UpdateEvent(ctx *gin.Context) { | |
| id, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| req := RequestJSON[dto.UpdateEventRequest](ctx) | |
| res, err := c.adminEventService.UpdateEvent(ctx.Request.Context(), id, req) | |
| ResponseJSON(ctx, req, res, err) | |
| } | |
| // DeleteEvent godoc | |
| // @Summary Admin: Delete Event | |
| // @Description Delete an event by ID | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Success 200 {object} dto.SuccessResponse[map[string]bool] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id} [delete] | |
| func (c *adminEventController) DeleteEvent(ctx *gin.Context) { | |
| id, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| delErr := c.adminEventService.DeleteEvent(ctx.Request.Context(), id) | |
| ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": delErr == nil}, delErr) | |
| } | |
| // ListParticipants godoc | |
| // @Summary Admin: List Event Participants | |
| // @Description List all participants assigned to an event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param limit query int false "Items per page" default(10) | |
| // @Param page query int false "Page number" default(1) | |
| // @Param search query string false "Search by username / full name" | |
| // @Param sortBy query string false "Sort field (assigned_at, username, email, full_name)" | |
| // @Param order query string false "Sort direction (asc / desc)" | |
| // @Success 200 {object} dto.SuccessResponse[[]entity.EventAssign] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/participants [get] | |
| func (c *adminEventController) ListParticipants(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) | |
| page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) | |
| search := ctx.DefaultQuery("search", "") | |
| sortBy := ctx.DefaultQuery("sortBy", "") | |
| order := ctx.DefaultQuery("order", "") | |
| if limit < 1 { | |
| limit = 10 | |
| } else if limit > 100 { | |
| limit = 100 | |
| } | |
| if page < 1 { | |
| page = 1 | |
| } | |
| offset := (page - 1) * limit | |
| p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order} | |
| list, total, listErr := c.adminEventService.ListParticipants(ctx.Request.Context(), eventId, p) | |
| if listErr != nil { | |
| ResponseJSON[any, any](ctx, nil, nil, listErr) | |
| return | |
| } | |
| totalPages := int((total + int64(limit) - 1) / int64(limit)) | |
| if total == 0 { | |
| totalPages = 1 | |
| } | |
| if page > totalPages { | |
| page = totalPages | |
| } | |
| meta := gin.H{ | |
| "event_id": eventId, | |
| "totalItems": total, | |
| "totalPages": totalPages, | |
| "currentPage": page, | |
| "limit": limit, | |
| } | |
| ResponseJSON(ctx, meta, list, nil) | |
| } | |
| // ListCandidates godoc | |
| // @Summary Admin: List of Event Candidates | |
| // @Description List accounts that are not yet assigned to the event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param limit query int false "Items per page" default(10) | |
| // @Param page query int false "Page number" default(1) | |
| // @Param search query string false "Search by username / full name" | |
| // @Param sortBy query string false "Sort field (username, full_name, created_at)" | |
| // @Param orderBy query string false "Sort direction (asc / desc)" | |
| // @Success 200 {object} dto.SuccessResponse[[]entity.Account] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/candidate [get] | |
| func (c *adminEventController) ListCandidates(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) | |
| page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) | |
| search := ctx.DefaultQuery("search", "") | |
| sortBy := ctx.DefaultQuery("sortBy", "") | |
| order := ctx.DefaultQuery("orderBy", "") | |
| if order == "" { | |
| order = ctx.DefaultQuery("order", "") | |
| } | |
| if limit < 1 { | |
| limit = 10 | |
| } else if limit > 100 { | |
| limit = 100 | |
| } | |
| if page < 1 { | |
| page = 1 | |
| } | |
| offset := (page - 1) * limit | |
| p := entity.Pagination{ | |
| Limit: limit, | |
| Offset: offset, | |
| Search: search, | |
| SortBy: sortBy, | |
| Order: order, | |
| } | |
| list, total, listErr := c.adminEventService.ListCandidates(ctx.Request.Context(), eventId, p) | |
| if listErr != nil { | |
| ResponseJSON[any, any](ctx, nil, nil, listErr) | |
| return | |
| } | |
| totalPages := int((total + int64(limit) - 1) / int64(limit)) | |
| if total == 0 { | |
| totalPages = 1 | |
| } | |
| if page > totalPages { | |
| page = totalPages | |
| } | |
| meta := gin.H{ | |
| "event_id": eventId, | |
| "totalItems": total, | |
| "totalPages": totalPages, | |
| "currentPage": page, | |
| "limit": limit, | |
| } | |
| ResponseJSON(ctx, meta, list, nil) | |
| } | |
| // ListExamCandidates godoc | |
| // @Summary Admin: List Exam Candidates for Event | |
| // @Description List exams that are not yet assigned to the event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param limit query int false "Items per page" default(10) | |
| // @Param page query int false "Page number" default(1) | |
| // @Param search query string false "Search by exam title / slug" | |
| // @Param sortBy query string false "Sort field (title, slug, created_at, duration)" | |
| // @Param orderBy query string false "Sort direction (asc / desc)" | |
| // @Success 200 {object} dto.SuccessResponse[[]entity.Exam] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/exams/candidate [get] | |
| func (c *adminEventController) ListExamCandidates(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) | |
| page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) | |
| search := ctx.DefaultQuery("search", "") | |
| sortBy := ctx.DefaultQuery("sortBy", "") | |
| if sortBy == "" { | |
| sortBy = ctx.DefaultQuery("sortby", "") | |
| } | |
| order := ctx.DefaultQuery("orderBy", "") | |
| if order == "" { | |
| order = ctx.DefaultQuery("orderby", "") | |
| } | |
| if order == "" { | |
| order = ctx.DefaultQuery("order", "") | |
| } | |
| if limit < 1 { | |
| limit = 10 | |
| } else if limit > 100 { | |
| limit = 100 | |
| } | |
| if page < 1 { | |
| page = 1 | |
| } | |
| offset := (page - 1) * limit | |
| p := entity.Pagination{ | |
| Limit: limit, | |
| Offset: offset, | |
| Search: search, | |
| SortBy: sortBy, | |
| Order: order, | |
| } | |
| list, total, listErr := c.adminEventService.ListExamCandidates(ctx.Request.Context(), eventId, p) | |
| if listErr != nil { | |
| ResponseJSON[any, any](ctx, nil, nil, listErr) | |
| return | |
| } | |
| totalPages := int((total + int64(limit) - 1) / int64(limit)) | |
| if total == 0 { | |
| totalPages = 1 | |
| } | |
| if page > totalPages { | |
| page = totalPages | |
| } | |
| meta := gin.H{ | |
| "event_id": eventId, | |
| "totalItems": total, | |
| "totalPages": totalPages, | |
| "currentPage": page, | |
| "limit": limit, | |
| } | |
| ResponseJSON(ctx, meta, list, nil) | |
| } | |
| // AddParticipant godoc | |
| // @Summary Admin: Add Participant to Event | |
| // @Description Manually assign a user to an event (bypasses payment) | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param request body dto.AddParticipantRequest true "Add Participant Request" | |
| // @Success 200 {object} dto.SuccessResponse[entity.EventAssign] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/participants [post] | |
| func (c *adminEventController) AddParticipant(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| req := RequestJSON[dto.AddParticipantRequest](ctx) | |
| userId, parseErr := uuid.Parse(req.UserId) | |
| if parseErr != nil { | |
| ResponseJSON[any](ctx, req, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| assign, err := c.adminEventService.AddParticipant(ctx.Request.Context(), eventId, userId) | |
| ResponseJSON(ctx, req, assign, err) | |
| } | |
| // RemoveParticipant godoc | |
| // @Summary Admin: Remove Participant from Event | |
| // @Description Unassign a user from an event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param user_id path string true "User ID" | |
| // @Success 200 {object} dto.SuccessResponse[map[string]bool] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/participants/{user_id} [delete] | |
| func (c *adminEventController) RemoveParticipant(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| userId, err := uuid.Parse(ctx.Param("user_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"user_id": ctx.Param("user_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| delErr := c.adminEventService.RemoveParticipant(ctx.Request.Context(), eventId, userId) | |
| ResponseJSON(ctx, gin.H{"event_id": eventId, "user_id": userId}, gin.H{"removed": delErr == nil}, delErr) | |
| } | |
| // ListEventExams godoc | |
| // @Summary Admin: List Event Exams | |
| // @Description List all exams assigned to an event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param limit query int false "Items per page" default(10) | |
| // @Param page query int false "Page number" default(1) | |
| // @Param search query string false "Search by exam title / slug" | |
| // @Param sortBy query string false "Sort field (title, slug, created_at, duration)" | |
| // @Param order query string false "Sort direction (asc / desc)" | |
| // @Success 200 {object} dto.SuccessResponse[[]entity.EventExamAssign] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/exams [get] | |
| func (c *adminEventController) ListEventExams(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) | |
| page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) | |
| search := ctx.DefaultQuery("search", "") | |
| sortBy := ctx.DefaultQuery("sortBy", "") | |
| order := ctx.DefaultQuery("order", "") | |
| if limit < 1 { | |
| limit = 10 | |
| } else if limit > 100 { | |
| limit = 100 | |
| } | |
| if page < 1 { | |
| page = 1 | |
| } | |
| offset := (page - 1) * limit | |
| p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order} | |
| list, total, listErr := c.adminEventService.ListEventExams(ctx.Request.Context(), eventId, p) | |
| if listErr != nil { | |
| ResponseJSON[any, any](ctx, nil, nil, listErr) | |
| return | |
| } | |
| totalPages := int((total + int64(limit) - 1) / int64(limit)) | |
| if total == 0 { | |
| totalPages = 1 | |
| } | |
| if page > totalPages { | |
| page = totalPages | |
| } | |
| meta := gin.H{ | |
| "event_id": eventId, | |
| "totalItems": total, | |
| "totalPages": totalPages, | |
| "currentPage": page, | |
| "limit": limit, | |
| } | |
| ResponseJSON(ctx, meta, list, nil) | |
| } | |
| // RemoveExam godoc | |
| // @Summary Admin: Remove Exam from Event | |
| // @Description Unassign an exam from an event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param exam_id path string true "Exam ID" | |
| // @Success 200 {object} dto.SuccessResponse[map[string]bool] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/exams/{exam_id} [delete] | |
| func (c *adminEventController) RemoveExam(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| examId, err := uuid.Parse(ctx.Param("exam_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"exam_id": ctx.Param("exam_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| delErr := c.adminEventService.RemoveExam(ctx.Request.Context(), eventId, examId) | |
| ResponseJSON(ctx, gin.H{"event_id": eventId, "exam_id": examId}, gin.H{"removed": delErr == nil}, delErr) | |
| } | |
| // ListResults godoc | |
| // @Summary Admin: List Exam Results for an Event | |
| // @Description Retrieve all participant results for a specific exam within an event | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param exam_id path string true "Exam ID" | |
| // @Param limit query int false "Items per page" default(10) | |
| // @Param page query int false "Page number" default(1) | |
| // @Param search query string false "Search by username / full name" | |
| // @Param sortBy query string false "Sort field (final_score, created_at, username, full_name)" | |
| // @Param order query string false "Sort direction (asc / desc)" | |
| // @Success 200 {object} dto.SuccessResponse[[]entity.Result] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/exams/{exam_id}/results [get] | |
| func (c *adminEventController) ListResults(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| examId, err := uuid.Parse(ctx.Param("exam_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"exam_id": ctx.Param("exam_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) | |
| page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) | |
| search := ctx.DefaultQuery("search", "") | |
| sortBy := ctx.DefaultQuery("sortBy", "") | |
| if sortBy == "" { | |
| sortBy = ctx.DefaultQuery("sortby", "") | |
| } | |
| order := ctx.DefaultQuery("order", "") | |
| if order == "" { | |
| order = ctx.DefaultQuery("orderBy", "") | |
| } | |
| if order == "" { | |
| order = ctx.DefaultQuery("orderby", "") | |
| } | |
| if limit < 1 { | |
| limit = 10 | |
| } else if limit > 100 { | |
| limit = 100 | |
| } | |
| if page < 1 { | |
| page = 1 | |
| } | |
| offset := (page - 1) * limit | |
| p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order} | |
| list, total, listErr := c.adminEventService.ListResults(ctx.Request.Context(), eventId, examId, p) | |
| if listErr != nil { | |
| ResponseJSON[any, any](ctx, nil, nil, listErr) | |
| return | |
| } | |
| totalPages := int((total + int64(limit) - 1) / int64(limit)) | |
| if total == 0 { | |
| totalPages = 1 | |
| } | |
| if page > totalPages { | |
| page = totalPages | |
| } | |
| meta := gin.H{ | |
| "event_id": eventId, | |
| "exam_id": examId, | |
| "totalItems": total, | |
| "totalPages": totalPages, | |
| "currentPage": page, | |
| "limit": limit, | |
| } | |
| ResponseJSON(ctx, meta, list, nil) | |
| } | |
| // UpdateResult godoc | |
| // @Summary Admin: Update Exam Result | |
| // @Description Edit the final score of a participant's exam result | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param exam_id path string true "Exam ID" | |
| // @Param result_id path string true "Result ID" | |
| // @Param request body dto.UpdateResultRequest true "Update Result Request" | |
| // @Success 200 {object} dto.SuccessResponse[entity.Result] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/exams/{exam_id}/results/{result_id} [put] | |
| func (c *adminEventController) UpdateResult(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| examId, err := uuid.Parse(ctx.Param("exam_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"exam_id": ctx.Param("exam_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| resultId, err := uuid.Parse(ctx.Param("result_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"result_id": ctx.Param("result_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| req := RequestJSON[dto.UpdateResultRequest](ctx) | |
| res, err := c.adminEventService.UpdateResult(ctx.Request.Context(), eventId, examId, resultId, req) | |
| ResponseJSON(ctx, req, res, err) | |
| } | |
| // DeleteResult godoc | |
| // @Summary Admin: Delete Exam Result | |
| // @Description Delete a participant's exam result | |
| // @Tags AdminEvent | |
| // @Accept json | |
| // @Produce json | |
| // @Security BearerAuth | |
| // @Param event_id path string true "Event ID" | |
| // @Param exam_id path string true "Exam ID" | |
| // @Param result_id path string true "Result ID" | |
| // @Success 200 {object} dto.SuccessResponse[map[string]bool] | |
| // @Failure 400 {object} dto.ErrorResponse | |
| // @Failure 401 {object} dto.ErrorResponse | |
| // @Failure 403 {object} dto.ErrorResponse | |
| // @Router /api/v1/admin/events/{event_id}/exams/{exam_id}/results/{result_id} [delete] | |
| func (c *adminEventController) DeleteResult(ctx *gin.Context) { | |
| eventId, err := uuid.Parse(ctx.Param("event_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"event_id": ctx.Param("event_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| examId, err := uuid.Parse(ctx.Param("exam_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"exam_id": ctx.Param("exam_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| resultId, err := uuid.Parse(ctx.Param("result_id")) | |
| if err != nil { | |
| ResponseJSON[any](ctx, gin.H{"result_id": ctx.Param("result_id")}, nil, http_error.INVALID_TOKEN) | |
| return | |
| } | |
| delErr := c.adminEventService.DeleteResult(ctx.Request.Context(), eventId, examId, resultId) | |
| ResponseJSON(ctx, gin.H{"result_id": resultId}, gin.H{"deleted": delErr == nil}, delErr) | |
| } | |