Store options in field_options with public /field-options and admin CRUD; validate learner profile values on update. Co-authored-by: Cursor <cursoragent@cursor.com>
304 lines
9.3 KiB
Go
304 lines
9.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type createFieldOptionReq struct {
|
|
FieldKey string `json:"field_key" validate:"required"`
|
|
Code string `json:"code" validate:"required"`
|
|
Label string `json:"label" validate:"required"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
Status *string `json:"status"`
|
|
}
|
|
|
|
type updateFieldOptionReq struct {
|
|
Label *string `json:"label"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
Status *string `json:"status"`
|
|
}
|
|
|
|
type fieldOptionRes struct {
|
|
ID int64 `json:"id"`
|
|
FieldKey string `json:"field_key"`
|
|
Code string `json:"code"`
|
|
Label string `json:"label"`
|
|
DisplayOrder int32 `json:"display_order"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt *string `json:"updated_at,omitempty"`
|
|
}
|
|
|
|
type listFieldOptionsRes struct {
|
|
Options []fieldOptionRes `json:"options"`
|
|
TotalCount int64 `json:"total_count"`
|
|
}
|
|
|
|
func mapFieldOptionToRes(o domain.ProfileFieldOption) fieldOptionRes {
|
|
var updatedAt *string
|
|
if o.UpdatedAt != nil {
|
|
v := o.UpdatedAt.Format("2006-01-02T15:04:05Z07:00")
|
|
updatedAt = &v
|
|
}
|
|
return fieldOptionRes{
|
|
ID: o.ID,
|
|
FieldKey: o.FieldKey,
|
|
Code: o.Code,
|
|
Label: o.Label,
|
|
DisplayOrder: o.DisplayOrder,
|
|
Status: o.Status,
|
|
CreatedAt: o.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
}
|
|
|
|
// ListPublicFieldOptions godoc
|
|
// @Summary List field options for client dropdowns
|
|
// @Description Returns active options grouped by field_key (e.g. education_level, country)
|
|
// @Tags field-options
|
|
// @Produce json
|
|
// @Param field_key query string false "Filter by field key"
|
|
// @Success 200 {object} domain.Response
|
|
// @Router /api/v1/field-options [get]
|
|
func (h *Handler) ListPublicFieldOptions(c *fiber.Ctx) error {
|
|
var fieldKeyPtr *string
|
|
if raw := strings.TrimSpace(c.Query("field_key")); raw != "" {
|
|
fieldKeyPtr = &raw
|
|
}
|
|
|
|
grouped, err := h.profileFieldOptionSvc.ListActiveOptionsGrouped(c.Context(), fieldKeyPtr)
|
|
if err != nil {
|
|
return fieldOptionError(c, err, "Failed to list field options")
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Field options retrieved successfully",
|
|
Data: grouped,
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
// ListFieldKeys godoc
|
|
// @Summary List distinct field keys
|
|
// @Description Returns field_key values that have options (e.g. education_level, country)
|
|
// @Tags field-options
|
|
// @Produce json
|
|
// @Param active_only query bool false "If true, only keys with active options"
|
|
// @Success 200 {object} domain.Response
|
|
// @Router /api/v1/field-options/fields [get]
|
|
func (h *Handler) ListFieldKeys(c *fiber.Ctx) error {
|
|
activeOnly := c.Query("active_only", "true") == "true"
|
|
keys, err := h.profileFieldOptionSvc.ListFieldKeys(c.Context(), activeOnly)
|
|
if err != nil {
|
|
return fieldOptionError(c, err, "Failed to list field keys")
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Field keys retrieved successfully",
|
|
Data: fiber.Map{
|
|
"fields": keys,
|
|
},
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
// ListFieldOptionsAdmin godoc
|
|
// @Summary List field options (admin)
|
|
// @Tags field-options
|
|
// @Produce json
|
|
// @Param field_key query string false "Filter by field key"
|
|
// @Param status query string false "ACTIVE or INACTIVE"
|
|
// @Param limit query int false "Limit"
|
|
// @Param offset query int false "Offset"
|
|
// @Success 200 {object} domain.Response
|
|
// @Router /api/v1/admin/field-options [get]
|
|
func (h *Handler) ListFieldOptionsAdmin(c *fiber.Ctx) error {
|
|
limit, _ := strconv.Atoi(c.Query("limit", "50"))
|
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
|
|
|
var fieldKeyPtr, statusPtr *string
|
|
if raw := strings.TrimSpace(c.Query("field_key")); raw != "" {
|
|
fieldKeyPtr = &raw
|
|
}
|
|
if raw := strings.TrimSpace(c.Query("status")); raw != "" {
|
|
statusPtr = &raw
|
|
}
|
|
|
|
options, total, err := h.profileFieldOptionSvc.ListProfileFieldOptions(c.Context(), fieldKeyPtr, statusPtr, int32(limit), int32(offset))
|
|
if err != nil {
|
|
return fieldOptionError(c, err, "Failed to list field options")
|
|
}
|
|
|
|
out := make([]fieldOptionRes, 0, len(options))
|
|
for _, o := range options {
|
|
out = append(out, mapFieldOptionToRes(o))
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Field options retrieved successfully",
|
|
Data: listFieldOptionsRes{
|
|
Options: out,
|
|
TotalCount: total,
|
|
},
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
// GetFieldOptionByIDAdmin godoc
|
|
// @Summary Get field option by ID (admin)
|
|
// @Tags field-options
|
|
// @Produce json
|
|
// @Param id path int true "Option ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Router /api/v1/admin/field-options/{id} [get]
|
|
func (h *Handler) GetFieldOptionByIDAdmin(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid option ID",
|
|
})
|
|
}
|
|
|
|
option, err := h.profileFieldOptionSvc.GetProfileFieldOptionByID(c.Context(), id, true)
|
|
if err != nil {
|
|
return fieldOptionError(c, err, "Failed to get field option")
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Field option retrieved successfully",
|
|
Data: mapFieldOptionToRes(option),
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
// CreateFieldOption godoc
|
|
// @Summary Create field option (admin)
|
|
// @Tags field-options
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createFieldOptionReq true "Create option"
|
|
// @Success 201 {object} domain.Response
|
|
// @Router /api/v1/admin/field-options [post]
|
|
func (h *Handler) CreateFieldOption(c *fiber.Ctx) error {
|
|
var req createFieldOptionReq
|
|
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 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Validation failed",
|
|
Error: firstValidationError(valErrs),
|
|
})
|
|
}
|
|
|
|
option, err := h.profileFieldOptionSvc.CreateProfileFieldOption(c.Context(), domain.CreateProfileFieldOptionInput{
|
|
FieldKey: req.FieldKey,
|
|
Code: req.Code,
|
|
Label: req.Label,
|
|
DisplayOrder: req.DisplayOrder,
|
|
Status: req.Status,
|
|
})
|
|
if err != nil {
|
|
return fieldOptionError(c, err, "Failed to create field option")
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "Field option created successfully",
|
|
Data: mapFieldOptionToRes(option),
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
// UpdateFieldOption godoc
|
|
// @Summary Update field option (admin)
|
|
// @Tags field-options
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "Option ID"
|
|
// @Param body body updateFieldOptionReq true "Update option"
|
|
// @Success 200 {object} domain.Response
|
|
// @Router /api/v1/admin/field-options/{id} [put]
|
|
func (h *Handler) UpdateFieldOption(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid option ID",
|
|
})
|
|
}
|
|
|
|
var req updateFieldOptionReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
option, err := h.profileFieldOptionSvc.UpdateProfileFieldOption(c.Context(), id, domain.UpdateProfileFieldOptionInput{
|
|
Label: req.Label,
|
|
DisplayOrder: req.DisplayOrder,
|
|
Status: req.Status,
|
|
})
|
|
if err != nil {
|
|
return fieldOptionError(c, err, "Failed to update field option")
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Field option updated successfully",
|
|
Data: mapFieldOptionToRes(option),
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
// DeleteFieldOption godoc
|
|
// @Summary Delete field option (admin)
|
|
// @Tags field-options
|
|
// @Produce json
|
|
// @Param id path int true "Option ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Router /api/v1/admin/field-options/{id} [delete]
|
|
func (h *Handler) DeleteFieldOption(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid option ID",
|
|
})
|
|
}
|
|
|
|
if err := h.profileFieldOptionSvc.DeleteProfileFieldOption(c.Context(), id); err != nil {
|
|
return fieldOptionError(c, err, "Failed to delete field option")
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Field option deleted successfully",
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
func fieldOptionError(c *fiber.Ctx, err error, message string) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrInvalidFieldKey),
|
|
errors.Is(err, domain.ErrInvalidOptionCode):
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: message, Error: err.Error()})
|
|
case errors.Is(err, pgx.ErrNoRows):
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: message, Error: "Field option not found"})
|
|
default:
|
|
if strings.Contains(err.Error(), "invalid value for") {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: message, Error: err.Error()})
|
|
}
|
|
if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique constraint") {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: message, Error: "An option with this field_key and code already exists"})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: message, Error: err.Error()})
|
|
}
|
|
}
|