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()}) } }