veza/veza-backend-api/internal/api/education/handlers.go
2025-12-03 20:29:37 +01:00

868 lines
25 KiB
Go

package education
import (
"net/http"
"strconv"
"time"
"veza-backend-api/internal/common"
"veza-backend-api/internal/core/education"
"veza-backend-api/internal/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// Handler gère les requêtes HTTP pour l'éducation
type Handler struct {
courseManager *education.CourseManager
tutorialManager *education.TutorialManager
logger *zap.Logger
}
// NewHandler crée un nouveau handler d'éducation
func NewHandler(courseManager *education.CourseManager, tutorialManager *education.TutorialManager, logger *zap.Logger) *Handler {
return &Handler{
courseManager: courseManager,
tutorialManager: tutorialManager,
logger: logger,
}
}
// Request/Response structures
type CreateCourseRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description" binding:"required"`
Instructor string `json:"instructor" binding:"required"`
Category string `json:"category" binding:"required"`
Level education.CourseLevel `json:"level" binding:"required"`
Duration time.Duration `json:"duration" binding:"required"`
Price float64 `json:"price"`
Language string `json:"language" binding:"required"`
Tags []string `json:"tags"`
}
type UpdateCourseRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
Instructor *string `json:"instructor"`
Category *string `json:"category"`
Level *education.CourseLevel `json:"level"`
Duration *time.Duration `json:"duration"`
Price *float64 `json:"price"`
Language *string `json:"language"`
IsPublished *bool `json:"is_published"`
Tags []string `json:"tags"`
}
type CreateTutorialRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description" binding:"required"`
Author string `json:"author" binding:"required"`
Category string `json:"category" binding:"required"`
VideoURL string `json:"video_url" binding:"required"`
Thumbnail string `json:"thumbnail"`
Duration time.Duration `json:"duration" binding:"required"`
Quality education.VideoQuality `json:"quality" binding:"required"`
Language string `json:"language" binding:"required"`
IsFree bool `json:"is_free"`
Tags []string `json:"tags"`
}
type UpdateTutorialRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
Author *string `json:"author"`
Category *string `json:"category"`
VideoURL *string `json:"video_url"`
Thumbnail *string `json:"thumbnail"`
Duration *time.Duration `json:"duration"`
Quality *education.VideoQuality `json:"quality"`
IsPublished *bool `json:"is_published"`
Tags []string `json:"tags"`
}
type AddLessonRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description" binding:"required"`
Content string `json:"content" binding:"required"`
VideoURL string `json:"video_url"`
Duration time.Duration `json:"duration" binding:"required"`
Order int `json:"order" binding:"required"`
IsFree bool `json:"is_free"`
}
type AddExerciseRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description" binding:"required"`
Content string `json:"content" binding:"required"`
Solution string `json:"solution" binding:"required"`
Type education.ExerciseType `json:"type" binding:"required"`
Points int `json:"points" binding:"required"`
TimeLimit time.Duration `json:"time_limit"`
IsRequired bool `json:"is_required"`
}
type UpdateProgressRequest struct {
Progress float64 `json:"progress" binding:"required"`
CompletedLessons []string `json:"completed_lessons"`
CurrentLesson string `json:"current_lesson"`
Score float64 `json:"score"`
TimeSpent time.Duration `json:"time_spent"`
}
type AddTutorialStepRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description" binding:"required"`
Content string `json:"content" binding:"required"`
Order int `json:"order" binding:"required"`
Timestamp time.Duration `json:"timestamp"`
IsFree bool `json:"is_free"`
}
type AddTutorialCommentRequest struct {
Content string `json:"content" binding:"required"`
Rating int `json:"rating" binding:"min=1,max=5"`
}
// COURSES HANDLERS
// CreateCourse crée un nouveau cours
func (h *Handler) CreateCourse(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
var req CreateCourseRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
course, err := h.courseManager.CreateCourse(
c.Request.Context(),
req.Title,
req.Description,
req.Instructor,
req.Category,
req.Level,
req.Duration,
req.Price,
req.Language,
)
if err != nil {
h.logger.Error("Échec de création du cours", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de création du cours")
return
}
response.Success(c, course, "Cours créé avec succès")
}
// GetCourse récupère un cours par son ID
func (h *Handler) GetCourse(c *gin.Context) {
courseID := c.Param("course_id")
if courseID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours requis")
return
}
course, err := h.courseManager.GetCourse(c.Request.Context(), courseID)
if err != nil {
h.logger.Error("Échec de récupération du cours", zap.Error(err))
response.Error(c, http.StatusNotFound, "Cours non trouvé")
return
}
response.Success(c, course, "Cours récupéré avec succès")
}
// ListCourses liste tous les cours disponibles
func (h *Handler) ListCourses(c *gin.Context) {
filters := make(map[string]interface{})
if category := c.Query("category"); category != "" {
filters["category"] = category
}
if level := c.Query("level"); level != "" {
filters["level"] = education.CourseLevel(level)
}
if isPublished := c.Query("is_published"); isPublished != "" {
if published, err := strconv.ParseBool(isPublished); err == nil {
filters["is_published"] = published
}
}
if isFree := c.Query("is_free"); isFree != "" {
if free, err := strconv.ParseBool(isFree); err == nil {
filters["is_free"] = free
}
}
courses, err := h.courseManager.ListCourses(c.Request.Context(), filters)
if err != nil {
h.logger.Error("Échec de récupération des cours", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de récupération des cours")
return
}
response.Success(c, courses, "Cours récupérés avec succès")
}
// UpdateCourse met à jour un cours
func (h *Handler) UpdateCourse(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
courseID := c.Param("course_id")
if courseID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours requis")
return
}
var req UpdateCourseRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Instructor != nil {
updates["instructor"] = *req.Instructor
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.Level != nil {
updates["level"] = *req.Level
}
if req.Duration != nil {
updates["duration"] = *req.Duration
}
if req.Price != nil {
updates["price"] = *req.Price
}
if req.Language != nil {
updates["language"] = *req.Language
}
if req.IsPublished != nil {
updates["is_published"] = *req.IsPublished
}
if req.Tags != nil {
updates["tags"] = req.Tags
}
course, err := h.courseManager.UpdateCourse(c.Request.Context(), courseID, updates)
if err != nil {
h.logger.Error("Échec de mise à jour du cours", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de mise à jour du cours")
return
}
response.Success(c, course, "Cours mis à jour avec succès")
}
// DeleteCourse supprime un cours
func (h *Handler) DeleteCourse(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
courseID := c.Param("course_id")
if courseID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours requis")
return
}
err := h.courseManager.DeleteCourse(c.Request.Context(), courseID)
if err != nil {
h.logger.Error("Échec de suppression du cours", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de suppression du cours")
return
}
response.Success(c, nil, "Cours supprimé avec succès")
}
// AddLesson ajoute une leçon à un cours
func (h *Handler) AddLesson(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
courseID := c.Param("course_id")
if courseID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours requis")
return
}
var req AddLessonRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
lesson, err := h.courseManager.AddLesson(
c.Request.Context(),
courseID,
req.Title,
req.Description,
req.Content,
req.VideoURL,
req.Duration,
req.Order,
req.IsFree,
)
if err != nil {
h.logger.Error("Échec d'ajout de leçon", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec d'ajout de leçon")
return
}
response.Success(c, lesson, "Leçon ajoutée avec succès")
}
// AddExercise ajoute un exercice à un cours
func (h *Handler) AddExercise(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
courseID := c.Param("course_id")
lessonID := c.Param("lesson_id")
if courseID == "" || lessonID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours et de leçon requis")
return
}
var req AddExerciseRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
exercise, err := h.courseManager.AddExercise(
c.Request.Context(),
courseID,
lessonID,
req.Title,
req.Description,
req.Content,
req.Solution,
req.Type,
req.Points,
req.TimeLimit,
req.IsRequired,
)
if err != nil {
h.logger.Error("Échec d'ajout d'exercice", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec d'ajout d'exercice")
return
}
response.Success(c, exercise, "Exercice ajouté avec succès")
}
// GetUserProgress récupère la progression d'un utilisateur
func (h *Handler) GetUserProgress(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
courseID := c.Param("course_id")
if courseID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours requis")
return
}
progress, err := h.courseManager.GetUserProgress(c.Request.Context(), userID, courseID)
if err != nil {
h.logger.Error("Échec de récupération de la progression", zap.Error(err))
response.Error(c, http.StatusNotFound, "Progression non trouvée")
return
}
response.Success(c, progress, "Progression récupérée avec succès")
}
// UpdateUserProgress met à jour la progression d'un utilisateur
func (h *Handler) UpdateUserProgress(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
courseID := c.Param("course_id")
if courseID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours requis")
return
}
var req UpdateProgressRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
progress, err := h.courseManager.UpdateUserProgress(
c.Request.Context(),
userID,
courseID,
req.Progress,
req.CompletedLessons,
req.CurrentLesson,
req.Score,
req.TimeSpent,
)
if err != nil {
h.logger.Error("Échec de mise à jour de la progression", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de mise à jour de la progression")
return
}
response.Success(c, progress, "Progression mise à jour avec succès")
}
// IssueCertificate émet un certificat
func (h *Handler) IssueCertificate(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
courseID := c.Param("course_id")
if courseID == "" {
response.Error(c, http.StatusBadRequest, "ID de cours requis")
return
}
// Récupérer les paramètres de la requête
title := c.Query("title")
description := c.Query("description")
scoreStr := c.Query("score")
maxScoreStr := c.Query("max_score")
if title == "" || description == "" || scoreStr == "" || maxScoreStr == "" {
response.Error(c, http.StatusBadRequest, "Tous les paramètres sont requis")
return
}
score, err := strconv.ParseFloat(scoreStr, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Score invalide")
return
}
maxScore, err := strconv.ParseFloat(maxScoreStr, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Score maximum invalide")
return
}
certificate, err := h.courseManager.IssueCertificate(
c.Request.Context(),
courseID,
userID,
title,
description,
score,
maxScore,
)
if err != nil {
h.logger.Error("Échec d'émission du certificat", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec d'émission du certificat")
return
}
response.Success(c, certificate, "Certificat émis avec succès")
}
// TUTORIALS HANDLERS
// CreateTutorial crée un nouveau tutoriel
func (h *Handler) CreateTutorial(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
var req CreateTutorialRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
tutorial, err := h.tutorialManager.CreateTutorial(
c.Request.Context(),
req.Title,
req.Description,
req.Author,
req.Category,
req.VideoURL,
req.Thumbnail,
req.Language,
req.Duration,
req.Quality,
req.IsFree,
req.Tags,
)
if err != nil {
h.logger.Error("Échec de création du tutoriel", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de création du tutoriel")
return
}
response.Success(c, tutorial, "Tutoriel créé avec succès")
}
// GetTutorial récupère un tutoriel par son ID
func (h *Handler) GetTutorial(c *gin.Context) {
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
tutorial, err := h.tutorialManager.GetTutorial(c.Request.Context(), tutorialID)
if err != nil {
h.logger.Error("Échec de récupération du tutoriel", zap.Error(err))
response.Error(c, http.StatusNotFound, "Tutoriel non trouvé")
return
}
// Incrémenter les vues
go func() {
if err := h.tutorialManager.IncrementViews(c.Request.Context(), tutorialID); err != nil {
h.logger.Error("Échec d'incrémentation des vues", zap.Error(err))
}
}()
response.Success(c, tutorial, "Tutoriel récupéré avec succès")
}
// ListTutorials liste tous les tutoriels disponibles
func (h *Handler) ListTutorials(c *gin.Context) {
filters := make(map[string]interface{})
if category := c.Query("category"); category != "" {
filters["category"] = category
}
if isPublished := c.Query("is_published"); isPublished != "" {
if published, err := strconv.ParseBool(isPublished); err == nil {
filters["is_published"] = published
}
}
if isFree := c.Query("is_free"); isFree != "" {
if free, err := strconv.ParseBool(isFree); err == nil {
filters["is_free"] = free
}
}
if language := c.Query("language"); language != "" {
filters["language"] = language
}
if author := c.Query("author"); author != "" {
filters["author"] = author
}
tutorials, err := h.tutorialManager.ListTutorials(c.Request.Context(), filters)
if err != nil {
h.logger.Error("Échec de récupération des tutoriels", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de récupération des tutoriels")
return
}
response.Success(c, tutorials, "Tutoriels récupérés avec succès")
}
// SearchTutorials recherche des tutoriels
func (h *Handler) SearchTutorials(c *gin.Context) {
query := c.Query("q")
if query == "" {
response.Error(c, http.StatusBadRequest, "Terme de recherche requis")
return
}
filters := make(map[string]interface{})
if category := c.Query("category"); category != "" {
filters["category"] = category
}
if isPublished := c.Query("is_published"); isPublished != "" {
if published, err := strconv.ParseBool(isPublished); err == nil {
filters["is_published"] = published
}
}
if isFree := c.Query("is_free"); isFree != "" {
if free, err := strconv.ParseBool(isFree); err == nil {
filters["is_free"] = free
}
}
tutorials, err := h.tutorialManager.SearchTutorials(c.Request.Context(), query, filters)
if err != nil {
h.logger.Error("Échec de recherche des tutoriels", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de recherche des tutoriels")
return
}
response.Success(c, tutorials, "Recherche de tutoriels terminée")
}
// UpdateTutorial met à jour un tutoriel
func (h *Handler) UpdateTutorial(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
var req UpdateTutorialRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Author != nil {
updates["author"] = *req.Author
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.VideoURL != nil {
updates["video_url"] = *req.VideoURL
}
if req.Thumbnail != nil {
updates["thumbnail"] = *req.Thumbnail
}
if req.Duration != nil {
updates["duration"] = *req.Duration
}
if req.Quality != nil {
updates["quality"] = *req.Quality
}
if req.IsPublished != nil {
updates["is_published"] = *req.IsPublished
}
if req.Tags != nil {
updates["tags"] = req.Tags
}
tutorial, err := h.tutorialManager.UpdateTutorial(c.Request.Context(), tutorialID, updates)
if err != nil {
h.logger.Error("Échec de mise à jour du tutoriel", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de mise à jour du tutoriel")
return
}
response.Success(c, tutorial, "Tutoriel mis à jour avec succès")
}
// DeleteTutorial supprime un tutoriel
func (h *Handler) DeleteTutorial(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
err := h.tutorialManager.DeleteTutorial(c.Request.Context(), tutorialID)
if err != nil {
h.logger.Error("Échec de suppression du tutoriel", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de suppression du tutoriel")
return
}
response.Success(c, nil, "Tutoriel supprimé avec succès")
}
// AddTutorialStep ajoute une étape à un tutoriel
func (h *Handler) AddTutorialStep(c *gin.Context) {
_, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
var req AddTutorialStepRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
step, err := h.tutorialManager.AddTutorialStep(
c.Request.Context(),
tutorialID,
req.Title,
req.Description,
req.Content,
req.Order,
req.Timestamp,
req.IsFree,
)
if err != nil {
h.logger.Error("Échec d'ajout d'étape de tutoriel", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec d'ajout d'étape de tutoriel")
return
}
response.Success(c, step, "Étape de tutoriel ajoutée avec succès")
}
// GetTutorialSteps récupère les étapes d'un tutoriel
func (h *Handler) GetTutorialSteps(c *gin.Context) {
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
steps, err := h.tutorialManager.GetTutorialSteps(c.Request.Context(), tutorialID)
if err != nil {
h.logger.Error("Échec de récupération des étapes", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de récupération des étapes")
return
}
response.Success(c, steps, "Étapes récupérées avec succès")
}
// AddTutorialComment ajoute un commentaire à un tutoriel
func (h *Handler) AddTutorialComment(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié")
return
}
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
var req AddTutorialCommentRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Données de requête invalides")
return
}
username, _ := common.GetUsernameFromContext(c)
if username == "" {
username = "Utilisateur anonyme"
}
comment, err := h.tutorialManager.AddTutorialComment(
c.Request.Context(),
tutorialID,
userID.String(),
username,
req.Content,
req.Rating,
)
if err != nil {
h.logger.Error("Échec d'ajout de commentaire", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec d'ajout de commentaire")
return
}
response.Success(c, comment, "Commentaire ajouté avec succès")
}
// GetTutorialComments récupère les commentaires d'un tutoriel
func (h *Handler) GetTutorialComments(c *gin.Context) {
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
comments, err := h.tutorialManager.GetTutorialComments(c.Request.Context(), tutorialID)
if err != nil {
h.logger.Error("Échec de récupération des commentaires", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec de récupération des commentaires")
return
}
response.Success(c, comments, "Commentaires récupérés avec succès")
}
// LikeTutorial ajoute un like à un tutoriel
func (h *Handler) LikeTutorial(c *gin.Context) {
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
err := h.tutorialManager.LikeTutorial(c.Request.Context(), tutorialID)
if err != nil {
h.logger.Error("Échec d'ajout de like", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec d'ajout de like")
return
}
response.Success(c, nil, "Like ajouté avec succès")
}
// DislikeTutorial ajoute un dislike à un tutoriel
func (h *Handler) DislikeTutorial(c *gin.Context) {
tutorialID := c.Param("tutorial_id")
if tutorialID == "" {
response.Error(c, http.StatusBadRequest, "ID de tutoriel requis")
return
}
err := h.tutorialManager.DislikeTutorial(c.Request.Context(), tutorialID)
if err != nil {
h.logger.Error("Échec d'ajout de dislike", zap.Error(err))
response.Error(c, http.StatusInternalServerError, "Échec d'ajout de dislike")
return
}
response.Success(c, nil, "Dislike ajouté avec succès")
}