lifedebugger commited on
Commit
cddf8b2
·
1 Parent(s): 9b01f9a

Deploy files from GitHub repository

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. controller/quiz/quiz_controller.go +24 -0
  2. models/request_model.go +11 -0
  3. router/quiz_route.go +1 -0
  4. services/quiz_service.go +17 -0
  5. space/repositories/quiz_repository.go +2 -1
  6. space/space/models/field_counter.go +1 -1
  7. space/space/pkg/validation/validation.go +2 -2
  8. space/space/services/cv_service.go +7 -2
  9. space/space/space/controller/quiz/quiz_controller.go +142 -4
  10. space/space/space/models/database_orm_model.go +19 -13
  11. space/space/space/models/request_model.go +85 -20
  12. space/space/space/repositories/quiz_repository.go +275 -33
  13. space/space/space/router/quiz_route.go +6 -3
  14. space/space/space/services/quiz_service.go +187 -68
  15. space/space/space/space/response/api_response_v2.go +9 -0
  16. space/space/space/space/space/config/database_connection_config.go +3 -1
  17. space/space/space/space/space/controller/quiz/quiz_controller.go +76 -0
  18. space/space/space/space/space/main.go +6 -0
  19. space/space/space/space/space/models/database_orm_model.go +127 -66
  20. space/space/space/space/space/models/exception_model.go +6 -0
  21. space/space/space/space/space/models/field_counter.go +40 -0
  22. space/space/space/space/space/models/request_model.go +30 -18
  23. space/space/space/space/space/models/response_model.go +0 -5
  24. space/space/space/space/space/repositories/academy_repository.go +48 -0
  25. space/space/space/space/space/repositories/quiz_repository.go +111 -118
  26. space/space/space/space/space/router/quiz_route.go +5 -13
  27. space/space/space/space/space/router/router.go +1 -1
  28. space/space/space/space/space/router/server.go +4 -0
  29. space/space/space/space/space/services/cv_service.go +22 -14
  30. space/space/space/space/space/services/quiz_service.go +198 -35
  31. space/space/space/space/space/space/space/controller/academy/academy_controller.go +11 -5
  32. space/space/space/space/space/space/space/models/database_orm_model.go +3 -1
  33. space/space/space/space/space/space/space/models/request_model.go +56 -4
  34. space/space/space/space/space/space/space/repositories/academy_repository.go +21 -13
  35. space/space/space/space/space/space/space/repositories/quiz_repository.go +19 -0
  36. space/space/space/space/space/space/space/router/quiz_route.go +1 -1
  37. space/space/space/space/space/space/space/services/academy_service.go +17 -17
  38. space/space/space/space/space/space/space/services/quiz_service.go +37 -0
  39. space/space/space/space/space/space/space/space/models/field_counter.go +0 -2
  40. space/space/space/space/space/space/space/space/services/partner_criteria_service.go +2 -2
  41. space/space/space/space/space/space/space/space/space/config/database_connection_config.go +0 -2
  42. space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go +369 -31
  43. space/space/space/space/space/space/space/space/space/controller/cv/cv_controller.go +12 -60
  44. space/space/space/space/space/space/space/space/space/controller/email/email_controller.go +1 -5
  45. space/space/space/space/space/space/space/space/space/controller/marriage_readiness_profile/marriage_readiness_profile_controller.go +2 -6
  46. space/space/space/space/space/space/space/space/space/controller/options/options_controller.go +1 -5
  47. space/space/space/space/space/space/space/space/space/controller/partner_criteria/partner_criteria_controller.go +1 -5
  48. space/space/space/space/space/space/space/space/space/controller/quiz/list_quiz_controller.go +1 -1
  49. space/space/space/space/space/space/space/space/space/main.go +6 -0
  50. space/space/space/space/space/space/space/space/space/models/database_orm_model.go +32 -50
controller/quiz/quiz_controller.go CHANGED
@@ -20,6 +20,7 @@ type QuizController interface {
20
  UserGetQuestionQuiz(ctx *gin.Context)
21
  UserAnswerQuiz(ctx *gin.Context)
22
  UserSubmitQuiz(ctx *gin.Context)
 
23
  UserReviewQuiz(ctx *gin.Context)
24
  }
25
 
@@ -189,6 +190,29 @@ func (c *quizController) UserSubmitQuiz(ctx *gin.Context) {
189
  response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
190
  }
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
193
  academyID := ctx.Query("academy_id")
194
  academyIDInt, err := strconv.Atoi(academyID)
 
20
  UserGetQuestionQuiz(ctx *gin.Context)
21
  UserAnswerQuiz(ctx *gin.Context)
22
  UserSubmitQuiz(ctx *gin.Context)
23
+ UserResultQuiz(ctx *gin.Context)
24
  UserReviewQuiz(ctx *gin.Context)
25
  }
26
 
 
190
  response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
191
  }
192
 
193
+ func (c *quizController) UserResultQuiz(ctx *gin.Context) {
194
+ academyID := ctx.Query("academy_id")
195
+ academyIDInt, err := strconv.Atoi(academyID)
196
+ if err != nil {
197
+ response.HandleError(ctx, err)
198
+ return
199
+ }
200
+
201
+ accountData := middleware.GetAccountData(ctx)
202
+ req := models.ResultQuizRequest{
203
+ AccountID: int64(accountData.UserID),
204
+ AcademyID: int64(academyIDInt),
205
+ }
206
+
207
+ res, err := c.quizService.UserResultQuiz(ctx, &req)
208
+ if err != nil {
209
+ response.HandleError(ctx, err)
210
+ return
211
+ }
212
+
213
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
214
+ }
215
+
216
  func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
217
  academyID := ctx.Query("academy_id")
218
  academyIDInt, err := strconv.Atoi(academyID)
models/request_model.go CHANGED
@@ -234,6 +234,17 @@ type (
234
  RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
235
  }
236
 
 
 
 
 
 
 
 
 
 
 
 
237
  // REVIEW QUIZ
238
  ReviewQuizRequest struct {
239
  AccountID int64 `json:"account_id" validate:"required"`
 
234
  RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
235
  }
236
 
237
+ // RESULT QUIZ
238
+ ResultQuizRequest struct {
239
+ AccountID int64 `json:"account_id" validate:"required"`
240
+ AcademyID int64 `json:"academy_id" validate:"required"`
241
+ }
242
+
243
+ ResultQuizResponse struct {
244
+ QuizAttempt
245
+ RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
246
+ }
247
+
248
  // REVIEW QUIZ
249
  ReviewQuizRequest struct {
250
  AccountID int64 `json:"account_id" validate:"required"`
router/quiz_route.go CHANGED
@@ -12,6 +12,7 @@ func (s *Server) QuizRoute() {
12
  userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
13
  userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
14
  userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
 
15
  userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
16
  }
17
  }
 
12
  userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
13
  userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
14
  userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
15
+ userRouterGroup.GET("/result", middleware.AuthUser, s.quizController.UserResultQuiz)
16
  userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
17
  }
18
  }
services/quiz_service.go CHANGED
@@ -22,6 +22,7 @@ type QuizService interface {
22
  UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
23
  UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
24
  UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
 
25
  UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
26
  }
27
 
@@ -304,6 +305,22 @@ func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuiz
304
  }, nil
305
  }
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
308
  attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
309
  if err != nil {
 
22
  UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
23
  UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
24
  UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
25
+ UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
26
  UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
27
  }
28
 
 
305
  }, nil
306
  }
307
 
308
+ func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error) {
309
+ attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
310
+ if err != nil {
311
+ return nil, response.HandleGormError(err, "Internal Server Error")
312
+ }
313
+ remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, attempt.AccountID, attempt.AcademyID)
314
+ if err != nil {
315
+ return nil, response.HandleGormError(err, "Internal Server Error")
316
+ }
317
+
318
+ return &models.ResultQuizResponse{
319
+ QuizAttempt: *attempt,
320
+ RemainingAttempts: remainingAttempts,
321
+ }, nil
322
+ }
323
+
324
  func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
325
  attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
326
  if err != nil {
space/repositories/quiz_repository.go CHANGED
@@ -65,6 +65,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
65
  quiz_attempts qa
66
  ON q.id = qa.quiz_id
67
  AND qa.account_id = @accountID
 
68
  WHERE
69
  q.academy_id = @academyID
70
  GROUP BY
@@ -77,7 +78,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
77
  "academyID": req.AcademyID,
78
  }).Scan(&quizResponse).Error
79
  if err != nil {
80
- return nil, err
81
  }
82
 
83
  if quizResponse.ID == 0 {
 
65
  quiz_attempts qa
66
  ON q.id = qa.quiz_id
67
  AND qa.account_id = @accountID
68
+ AND qa.finished_at IS NOT NULL
69
  WHERE
70
  q.academy_id = @academyID
71
  GROUP BY
 
78
  "academyID": req.AcademyID,
79
  }).Scan(&quizResponse).Error
80
  if err != nil {
81
+ return nil, fmt.Errorf("failed to query quiz: %w", err)
82
  }
83
 
84
  if quizResponse.ID == 0 {
space/space/models/field_counter.go CHANGED
@@ -164,7 +164,7 @@ func isFieldFilled(field reflect.Value) bool {
164
  case reflect.Float32, reflect.Float64:
165
  return field.Float() != 0
166
  case reflect.Bool:
167
- return field.Bool()
168
  case reflect.Slice, reflect.Map, reflect.Array:
169
  return !field.IsNil() && field.Len() > 0
170
  case reflect.Struct:
 
164
  case reflect.Float32, reflect.Float64:
165
  return field.Float() != 0
166
  case reflect.Bool:
167
+ return true
168
  case reflect.Slice, reflect.Map, reflect.Array:
169
  return !field.IsNil() && field.Len() > 0
170
  case reflect.Struct:
space/space/pkg/validation/validation.go CHANGED
@@ -21,9 +21,9 @@ func New(db *gorm.DB) (*Validator, error) {
21
  return nil, err
22
  }
23
 
24
- // Start background refresh every 10 minutes
25
  ctx := context.Background()
26
- dbSource.StartAutoRefresh(ctx, 10*time.Minute)
27
 
28
  // create validator
29
  validator := NewValidator(dbSource)
 
21
  return nil, err
22
  }
23
 
24
+ // Start background refresh every 5 minutes
25
  ctx := context.Background()
26
+ dbSource.StartAutoRefresh(ctx, 5*time.Minute)
27
 
28
  // create validator
29
  validator := NewValidator(dbSource)
space/space/services/cv_service.go CHANGED
@@ -192,6 +192,9 @@ func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *model
192
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, req *models.GetPersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
193
  res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
194
  if err != nil {
 
 
 
195
  return nil, response.HandleGormError(err, "Internal Server Error")
196
  }
197
 
@@ -312,7 +315,9 @@ func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.SaveP
312
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
313
  res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
314
  if err != nil {
315
- return nil, response.HandleGormError(err, "Internal Server Error")
 
 
316
  }
317
  return res, nil
318
  }
@@ -369,7 +374,7 @@ func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, re
369
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, req *models.GetWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
370
  res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
371
  if err != nil {
372
- return nil, response.HandleGormError(err, "Internal Server Error")
373
  }
374
  return res, nil
375
  }
 
192
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, req *models.GetPersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
193
  res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
194
  if err != nil {
195
+ if errors.Is(err, gorm.ErrRecordNotFound) {
196
+ return &models.PersonalityAndPreferenceCV{}, nil
197
+ }
198
  return nil, response.HandleGormError(err, "Internal Server Error")
199
  }
200
 
 
315
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
316
  res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
317
  if err != nil {
318
+ if errors.Is(err, gorm.ErrRecordNotFound) {
319
+ return &models.PhysicalAndHealthCV{}, nil
320
+ }
321
  }
322
  return res, nil
323
  }
 
374
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, req *models.GetWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
375
  res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
376
  if err != nil {
377
+ return &models.WorshipAndReligiousUnderstandingCV{}, nil
378
  }
379
  return res, nil
380
  }
space/space/space/controller/quiz/quiz_controller.go CHANGED
@@ -17,6 +17,10 @@ type QuizController interface {
17
  // === USER ===
18
  UserGetQuiz(ctx *gin.Context)
19
  UserAttemptQuiz(ctx *gin.Context)
 
 
 
 
20
  }
21
 
22
  type quizController struct {
@@ -30,7 +34,7 @@ func NewQuizController(quizService services.QuizService) QuizController {
30
  }
31
 
32
  func (c *quizController) UserGetQuiz(ctx *gin.Context) {
33
- academyID := ctx.Param("id")
34
  academyIDInt, err := strconv.Atoi(academyID)
35
  if err != nil {
36
  response.HandleError(ctx, err)
@@ -38,6 +42,7 @@ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
38
  }
39
 
40
  accountData := middleware.GetAccountData(ctx)
 
41
  req := models.UserGetQuizRequest{
42
  AccountID: int64(accountData.UserID),
43
  AcademyID: int64(academyIDInt),
@@ -49,11 +54,11 @@ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
49
  return
50
  }
51
 
52
- response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", http.StatusOK, res)
53
  }
54
 
55
  func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
56
- academyID := ctx.Param("id")
57
  academyIDInt, err := strconv.Atoi(academyID)
58
  if err != nil {
59
  response.HandleError(ctx, err)
@@ -61,6 +66,7 @@ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
61
  }
62
 
63
  accountData := middleware.GetAccountData(ctx)
 
64
  req := models.UserAttemptQuizRequest{
65
  AccountID: int64(accountData.UserID),
66
  AcademyID: int64(academyIDInt),
@@ -72,5 +78,137 @@ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
72
  return
73
  }
74
 
75
- response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", http.StatusOK, res)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
 
17
  // === USER ===
18
  UserGetQuiz(ctx *gin.Context)
19
  UserAttemptQuiz(ctx *gin.Context)
20
+ UserGetQuestionQuiz(ctx *gin.Context)
21
+ UserAnswerQuiz(ctx *gin.Context)
22
+ UserSubmitQuiz(ctx *gin.Context)
23
+ UserReviewQuiz(ctx *gin.Context)
24
  }
25
 
26
  type quizController struct {
 
34
  }
35
 
36
  func (c *quizController) UserGetQuiz(ctx *gin.Context) {
37
+ academyID := ctx.Query("academy_id")
38
  academyIDInt, err := strconv.Atoi(academyID)
39
  if err != nil {
40
  response.HandleError(ctx, err)
 
42
  }
43
 
44
  accountData := middleware.GetAccountData(ctx)
45
+
46
  req := models.UserGetQuizRequest{
47
  AccountID: int64(accountData.UserID),
48
  AcademyID: int64(academyIDInt),
 
54
  return
55
  }
56
 
57
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", res, nil)
58
  }
59
 
60
  func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
61
+ academyID := ctx.Query("academy_id")
62
  academyIDInt, err := strconv.Atoi(academyID)
63
  if err != nil {
64
  response.HandleError(ctx, err)
 
66
  }
67
 
68
  accountData := middleware.GetAccountData(ctx)
69
+
70
  req := models.UserAttemptQuizRequest{
71
  AccountID: int64(accountData.UserID),
72
  AcademyID: int64(academyIDInt),
 
78
  return
79
  }
80
 
81
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", res, nil)
82
+ }
83
+
84
+ func (c *quizController) UserGetQuestionQuiz(ctx *gin.Context) {
85
+ academyID := ctx.Query("academy_id")
86
+ academyIDInt, err := strconv.Atoi(academyID)
87
+ if err != nil {
88
+ response.HandleError(ctx, err)
89
+ return
90
+ }
91
+
92
+ questionID := ctx.Query("question_id")
93
+ questionIDInt, err := strconv.Atoi(questionID)
94
+ if err != nil {
95
+ response.HandleError(ctx, err)
96
+ return
97
+ }
98
+
99
+ attemptID := ctx.Query("attempt_id")
100
+ attemptIDInt, err := strconv.Atoi(attemptID)
101
+ if err != nil {
102
+ response.HandleError(ctx, err)
103
+ return
104
+ }
105
+
106
+ accountData := middleware.GetAccountData(ctx)
107
+
108
+ req := models.GetQuestionQuizRequest{
109
+ AccountID: int64(accountData.UserID),
110
+ AcademyID: int64(academyIDInt),
111
+ QuestionID: int64(questionIDInt),
112
+ AttemptID: int64(attemptIDInt),
113
+ }
114
+
115
+ res, err := c.quizService.UserGetQuestionQuiz(ctx, &req)
116
+ if err != nil {
117
+ response.HandleError(ctx, err)
118
+ return
119
+ }
120
+
121
+ response.HandleSuccess(ctx, http.StatusOK, "Question retrieved successfully", res, nil)
122
+ }
123
+
124
+ func (c *quizController) UserAnswerQuiz(ctx *gin.Context) {
125
+ var req models.AnswerQuizRequest
126
+ if err := ctx.ShouldBindJSON(&req); err != nil {
127
+ response.HandleError(ctx, err)
128
+ return
129
+ }
130
+
131
+ academyID := ctx.Query("academy_id")
132
+ academyIDInt, err := strconv.Atoi(academyID)
133
+ if err != nil {
134
+ response.HandleError(ctx, err)
135
+ return
136
+ }
137
+
138
+ attemptID := ctx.Query("attempt_id")
139
+ attemptIDInt, err := strconv.Atoi(attemptID)
140
+ if err != nil {
141
+ response.HandleError(ctx, err)
142
+ return
143
+ }
144
+
145
+ accountData := middleware.GetAccountData(ctx)
146
+
147
+ req.AccountID = int64(accountData.UserID)
148
+ req.AcademyID = int64(academyIDInt)
149
+ req.AttemptID = int64(attemptIDInt)
150
+
151
+ res, err := c.quizService.UserAnswerQuiz(ctx, &req)
152
+ if err != nil {
153
+ response.HandleError(ctx, err)
154
+ return
155
+ }
156
+
157
+ response.HandleSuccess(ctx, http.StatusOK, "Question retrieved successfully", res, nil)
158
+ }
159
+
160
+ func (c *quizController) UserSubmitQuiz(ctx *gin.Context) {
161
+ academyID := ctx.Query("academy_id")
162
+ academyIDInt, err := strconv.Atoi(academyID)
163
+ if err != nil {
164
+ response.HandleError(ctx, err)
165
+ return
166
+ }
167
+
168
+ attemptID := ctx.Query("attempt_id")
169
+ attemptIDInt, err := strconv.Atoi(attemptID)
170
+ if err != nil {
171
+ response.HandleError(ctx, err)
172
+ return
173
+ }
174
+
175
+ accountData := middleware.GetAccountData(ctx)
176
+
177
+ req := models.SubmitQuizRequest{
178
+ AccountID: int64(accountData.UserID),
179
+ AcademyID: int64(academyIDInt),
180
+ AttemptID: int64(attemptIDInt),
181
+ }
182
+
183
+ res, err := c.quizService.UserSubmitQuiz(ctx, &req)
184
+ if err != nil {
185
+ response.HandleError(ctx, err)
186
+ return
187
+ }
188
+
189
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
190
+ }
191
+
192
+ func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
193
+ academyID := ctx.Query("academy_id")
194
+ academyIDInt, err := strconv.Atoi(academyID)
195
+ if err != nil {
196
+ response.HandleError(ctx, err)
197
+ return
198
+ }
199
+
200
+ accountData := middleware.GetAccountData(ctx)
201
+
202
+ req := models.ReviewQuizRequest{
203
+ AccountID: int64(accountData.UserID),
204
+ AcademyID: int64(academyIDInt),
205
+ }
206
+
207
+ res, err := c.quizService.UserReviewQuiz(ctx, &req)
208
+ if err != nil {
209
+ response.HandleError(ctx, err)
210
+ return
211
+ }
212
+
213
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz review retrieved successfully", res.Reviews, nil)
214
  }
space/space/space/models/database_orm_model.go CHANGED
@@ -156,7 +156,7 @@ type (
156
  Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
157
  Content string `gorm:"column:content" json:"content"`
158
  Order int `gorm:"column:order" json:"order"`
159
- Review string `gorm:"column:reviews" json:"reviews"`
160
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
161
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
162
  }
@@ -172,17 +172,23 @@ type (
172
  }
173
 
174
  QuizAttempt struct {
175
- ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
176
- AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
177
- Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
178
- QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
179
- Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
180
- StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
181
- DueAt time.Time `gorm:"column:due_at" json:"due_at"`
182
- FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
183
- Score float64 `gorm:"column:score" json:"score"`
184
- CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
185
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
 
 
 
 
 
186
  }
187
 
188
  UserAnswer struct {
@@ -193,7 +199,7 @@ type (
193
  Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
194
  AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
195
  Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
196
- IsDoubt bool `gorm:"column:id_doubt" json:"id_doubt"`
197
  IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
198
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
199
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
156
  Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
157
  Content string `gorm:"column:content" json:"content"`
158
  Order int `gorm:"column:order" json:"order"`
159
+ Review string `gorm:"column:review" json:"review"`
160
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
161
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
162
  }
 
172
  }
173
 
174
  QuizAttempt struct {
175
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
176
+ AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
177
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
178
+ AcademyID int64 `gorm:"column:academy_id;not null" json:"academy_id"`
179
+ Academy *Academy `gorm:"foreignKey:AcademyID;constraint:OnDelete:CASCADE" json:"academy,omitempty"`
180
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
181
+ Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"-"`
182
+ StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
183
+ DueAt time.Time `gorm:"column:due_at" json:"due_at"`
184
+ FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
185
+ TotalQuestions int64 `gorm:"column:total_questions;not null" json:"total_questions"`
186
+ TotalCorrectAnswer int64 `gorm:"column:total_correct_answer;not null" json:"total_correct_answer"`
187
+ TotalWrongAnswer int64 `gorm:"column:total_wrong_answer;not null" json:"total_wrong_answer"`
188
+ Score float64 `gorm:"column:score" json:"score"`
189
+ IsPassed bool `gorm:"column:is_passed" json:"is_passed"`
190
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
191
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
192
  }
193
 
194
  UserAnswer struct {
 
199
  Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
200
  AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
201
  Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
202
+ IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
203
  IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
204
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
205
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
space/space/space/models/request_model.go CHANGED
@@ -152,6 +152,7 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
152
  }
153
 
154
  type (
 
155
  UserGetQuizRequest struct {
156
  AccountID int64 `json:"account_id" validate:"required"`
157
  AcademyID int64 `json:"academy_id" validate:"required"`
@@ -159,36 +160,105 @@ type (
159
 
160
  UserGetQuizResponse struct {
161
  Quiz
162
- TotalQuestions int64 `json:"total_questions"`
163
- UserAttempts int64 `json:"user_attempts"`
 
164
  }
165
 
 
166
  UserAttemptQuizRequest struct {
167
- AccountID int64 `json:"account_id"`
168
- AcademyID int64 `json:"academy_id"`
169
  }
170
 
171
  UserAttemptQuizQuestionsResponse struct {
172
- ID int64 `gorm:"column:id" json:"id"`
173
- IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
174
- IsAnswered bool `gorm:"column:is_answered" json:"is_answered"`
175
  }
176
 
177
  UserAttemptQuizResponse struct {
178
- QuizAttempt
179
- Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
 
 
 
 
 
 
180
  }
181
 
 
182
  GetQuestionQuizRequest struct {
183
- AccountID int64 `json:"account_id"`
184
- QuizID int64 `json:"quiz_id" validate:"required"`
 
 
185
  }
186
 
187
  GetQuestionQuizResponse struct {
188
- Question Question `json:"question"`
189
- Answer []Answer `json:"answer_options"`
190
- UserAnswer int `json:"current_user_answer"`
191
- IsDoubt bool `json:"is_doubt"`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
  )
194
 
@@ -239,11 +309,6 @@ type QuestionQuizRequest struct {
239
  QuestionNo int `json:"question_no" binding:"required"`
240
  }
241
 
242
- type AnswerQuizRequest struct {
243
- QuestionNo int `json:"question_no" binding:"required"`
244
- Answer int `json:"answer" binding:"required"`
245
- }
246
-
247
  type (
248
  ListCitiesByProvinceIdRequest struct {
249
  ProvinceID int64 `json:"province_id" binding:"required"`
 
152
  }
153
 
154
  type (
155
+ // GET QUIZ
156
  UserGetQuizRequest struct {
157
  AccountID int64 `json:"account_id" validate:"required"`
158
  AcademyID int64 `json:"academy_id" validate:"required"`
 
160
 
161
  UserGetQuizResponse struct {
162
  Quiz
163
+ TotalQuestions int64 `json:"total_questions"`
164
+ UserAttempts int64 `json:"user_attempts"`
165
+ HasActiveAttempt bool `json:"has_active_attempt"`
166
  }
167
 
168
+ // ATTEMP QUIZ
169
  UserAttemptQuizRequest struct {
170
+ AccountID int64 `json:"account_id" validate:"required"`
171
+ AcademyID int64 `json:"academy_id" validate:"required"`
172
  }
173
 
174
  UserAttemptQuizQuestionsResponse struct {
175
+ ID int64 `json:"id"`
176
+ IsDoubt bool `json:"is_doubt"`
177
+ IsAnswered bool `json:"is_answered"`
178
  }
179
 
180
  UserAttemptQuizResponse struct {
181
+ ID int64 `json:"id"`
182
+ AccountID int64 `json:"account_id"`
183
+ QuizID int64 `json:"quiz_id"`
184
+ StartedAt time.Time `json:"started_at"`
185
+ DueAt time.Time `json:"due_at"`
186
+ FinishedAt *time.Time `json:"finished_at"`
187
+ Score float64 `json:"score"`
188
+ Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
189
  }
190
 
191
+ // GET QUESTION
192
  GetQuestionQuizRequest struct {
193
+ AccountID int64 `json:"account_id" validate:"required"`
194
+ AcademyID int64 `json:"academy_id" validate:"required"`
195
+ AttemptID int64 `json:"attempt_id" validate:"required"`
196
+ QuestionID int64 `json:"question_id" validate:"required"`
197
  }
198
 
199
  GetQuestionQuizResponse struct {
200
+ Question struct {
201
+ ID int64 `json:"id"`
202
+ QuizID int64 `json:"quiz_id"`
203
+ Content string `json:"content"`
204
+ } `json:"question"`
205
+ AnswerOptions []struct {
206
+ ID int64 `json:"id"`
207
+ Content string `json:"content"`
208
+ } `json:"answer_options"`
209
+ AnswerID *int64 `json:"answer_id"`
210
+ IsDoubt bool `json:"is_doubt"`
211
+ IsAnswered bool `json:"is_answered"`
212
+ }
213
+
214
+ // ANSWER QUESTION
215
+ AnswerQuizRequest struct {
216
+ AccountID int64 `json:"account_id" validate:"required"`
217
+ AcademyID int64 `json:"academy_id" validate:"required"`
218
+ AttemptID int64 `json:"attempt_id" validate:"required"`
219
+
220
+ QuestionID int64 `json:"question_id" validate:"required"`
221
+ AnswerID *int64 `json:"answer_id"`
222
+ IsDoubt bool `json:"is_doubt"`
223
+ }
224
+
225
+ // SUBMIT QUIZ
226
+ SubmitQuizRequest struct {
227
+ AccountID int64 `json:"account_id" validate:"required"`
228
+ AcademyID int64 `json:"academy_id" validate:"required"`
229
+ AttemptID int64 `json:"attempt_id" validate:"required"`
230
+ }
231
+
232
+ SubmitQuizResponse struct {
233
+ QuizAttempt
234
+ RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
235
+ }
236
+
237
+ // REVIEW QUIZ
238
+ ReviewQuizRequest struct {
239
+ AccountID int64 `json:"account_id" validate:"required"`
240
+ AcademyID int64 `json:"academy_id" validate:"required"`
241
+ }
242
+
243
+ ReviewQuizQuestion struct {
244
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
245
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
246
+ Content string `gorm:"column:content" json:"content"`
247
+ Review string `gorm:"column:review" json:"review"`
248
+ }
249
+
250
+ ReviewQuiz struct {
251
+ Question ReviewQuizQuestion `json:"question"`
252
+ AnswerOptions []struct {
253
+ ID int64 `json:"id"`
254
+ Content string `json:"content"`
255
+ IsCorrect bool `json:"is_correct"`
256
+ } `json:"answer_options"`
257
+ AnswerID int64 `json:"answer_id"`
258
+ }
259
+
260
+ ReviewQuizResponse struct {
261
+ Reviews []ReviewQuiz
262
  }
263
  )
264
 
 
309
  QuestionNo int `json:"question_no" binding:"required"`
310
  }
311
 
 
 
 
 
 
312
  type (
313
  ListCitiesByProvinceIdRequest struct {
314
  ProvinceID int64 `json:"province_id" binding:"required"`
space/space/space/repositories/quiz_repository.go CHANGED
@@ -2,6 +2,8 @@ package repositories
2
 
3
  import (
4
  "context"
 
 
5
 
6
  "api.qobiltu.id/models"
7
  "gorm.io/gorm"
@@ -10,13 +12,25 @@ import (
10
  type QuizRepository interface {
11
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
12
  UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
13
- UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
14
- UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
15
- UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
16
  UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
 
 
 
 
 
 
 
 
 
17
  UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
18
  UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
19
  UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
 
 
 
 
 
 
20
  }
21
 
22
  type quizRepository struct {
@@ -31,24 +45,37 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
31
  var quizResponse models.UserGetQuizResponse
32
 
33
  rawQuery := `
34
- SELECT
35
- q.*,
36
- (SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
37
- COALESCE(COUNT(qa.id), 0) AS user_attempts
38
- FROM
39
- quizzes q
40
- LEFT JOIN
41
- quiz_attempts qa
42
- ON q.id = qa.quiz_id
43
- AND qa.account_id = ?
44
- WHERE
45
- q.academy_id = ?
46
- GROUP BY
47
- q.id, q.academy_id, q.slug, q.title, q.description,
48
- q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
49
- `
 
 
 
 
 
 
 
 
 
 
50
 
51
- err := r.db.Raw(rawQuery, req.AccountID, req.AcademyID).Scan(&quizResponse).Error
 
 
 
52
  if err != nil {
53
  return nil, err
54
  }
@@ -63,7 +90,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
63
  func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
64
  var quizAttempt models.QuizAttempt
65
 
66
- err := r.db.Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
67
  if err != nil {
68
  return nil, err
69
  }
@@ -71,8 +98,19 @@ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID
71
  return &quizAttempt, nil
72
  }
73
 
74
- func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
75
- return r.db.Model(&models.QuizAttempt{}).Where("id = ?", attempt.ID).Updates(attempt).Error
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
  func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
@@ -105,7 +143,7 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
105
  q.id;
106
  `
107
 
108
- err := r.db.Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
109
  if err != nil {
110
  return nil, err
111
  }
@@ -113,19 +151,21 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
113
  return questions, nil
114
  }
115
 
116
- func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
117
- return r.db.Create(attempt).Error
118
- }
119
-
120
- func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
121
- var totalAttempts int64
122
 
123
- err := r.db.Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
 
 
124
  if err != nil {
125
- return 0, err
126
  }
127
 
128
- return totalAttempts, nil
 
 
 
 
129
  }
130
 
131
  func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
@@ -153,3 +193,205 @@ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quiz
153
  func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
154
  return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
155
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import (
4
  "context"
5
+ "encoding/json"
6
+ "fmt"
7
 
8
  "api.qobiltu.id/models"
9
  "gorm.io/gorm"
 
12
  type QuizRepository interface {
13
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
14
  UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
 
 
 
15
  UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
16
+ UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
17
+ UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
18
+
19
+ UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error)
20
+ UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
21
+
22
+ UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error)
23
+ UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error)
24
+
25
  UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
26
  UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
27
  UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
28
+ UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error)
29
+ UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error)
30
+ UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error)
31
+ UserSaveUserAnswer(ctx context.Context, answer *models.UserAnswer) error
32
+ GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error)
33
+ UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error
34
  }
35
 
36
  type quizRepository struct {
 
45
  var quizResponse models.UserGetQuizResponse
46
 
47
  rawQuery := `
48
+ SELECT
49
+ q.*,
50
+ (SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
51
+ COALESCE(COUNT(qa.id), 0) AS user_attempts,
52
+ CASE
53
+ WHEN EXISTS (
54
+ SELECT 1
55
+ FROM quiz_attempts qa2
56
+ WHERE qa2.quiz_id = q.id
57
+ AND qa2.account_id = @accountID
58
+ AND qa2.finished_at IS NULL
59
+ ) THEN TRUE
60
+ ELSE FALSE
61
+ END AS has_active_attempt
62
+ FROM
63
+ quizzes q
64
+ LEFT JOIN
65
+ quiz_attempts qa
66
+ ON q.id = qa.quiz_id
67
+ AND qa.account_id = @accountID
68
+ WHERE
69
+ q.academy_id = @academyID
70
+ GROUP BY
71
+ q.id, q.academy_id, q.slug, q.title, q.description,
72
+ q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
73
+ `
74
 
75
+ err := r.db.Debug().Raw(rawQuery, map[string]any{
76
+ "accountID": req.AccountID,
77
+ "academyID": req.AcademyID,
78
+ }).Scan(&quizResponse).Error
79
  if err != nil {
80
  return nil, err
81
  }
 
90
  func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
91
  var quizAttempt models.QuizAttempt
92
 
93
+ err := r.db.Debug().Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
94
  if err != nil {
95
  return nil, err
96
  }
 
98
  return &quizAttempt, nil
99
  }
100
 
101
+ func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
102
+ var totalAttempts int64
103
+
104
+ err := r.db.Debug().Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
105
+ if err != nil {
106
+ return 0, err
107
+ }
108
+
109
+ return totalAttempts, nil
110
+ }
111
+
112
+ func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
113
+ return r.db.Debug().Create(attempt).Error
114
  }
115
 
116
  func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
 
143
  q.id;
144
  `
145
 
146
+ err := r.db.Debug().Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
147
  if err != nil {
148
  return nil, err
149
  }
 
151
  return questions, nil
152
  }
153
 
154
+ func (r *quizRepository) UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error) {
155
+ var quizAttempt models.QuizAttempt
 
 
 
 
156
 
157
+ err := r.db.Where("id = ?", attemptID).
158
+ Preload("Quiz").
159
+ First(&quizAttempt).Error
160
  if err != nil {
161
+ return nil, err
162
  }
163
 
164
+ return &quizAttempt, nil
165
+ }
166
+
167
+ func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
168
+ return r.db.Save(attempt).Error
169
  }
170
 
171
  func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
 
193
  func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
194
  return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
195
  }
196
+
197
+ func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error) {
198
+ var remainingAttempts int64
199
+
200
+ rawQuery := `
201
+ SELECT
202
+ GREATEST(q.attempt_limit - COALESCE(COUNT(qa.id), 0), 0) AS remaining_attempts
203
+ FROM
204
+ quizzes q
205
+ LEFT JOIN
206
+ quiz_attempts qa
207
+ ON q.id = qa.quiz_id
208
+ AND qa.account_id = ?
209
+ AND qa.finished_at IS NOT NULL
210
+ WHERE
211
+ q.id = ?
212
+ GROUP BY
213
+ q.attempt_limit;
214
+ `
215
+
216
+ err := r.db.Raw(rawQuery, accountID, quizID).Scan(&remainingAttempts).Error
217
+ if err != nil {
218
+ return 0, err
219
+ }
220
+
221
+ return remainingAttempts, nil
222
+ }
223
+
224
+ func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error) {
225
+ var resultJSON string
226
+ var response models.GetQuestionQuizResponse
227
+
228
+ rawQuery := `
229
+ SELECT
230
+ json_build_object(
231
+ 'question', json_build_object(
232
+ 'id', q.id,
233
+ 'quiz_id', q.quiz_id,
234
+ 'content', q.content
235
+ ),
236
+ 'answer_options', json_agg(
237
+ json_build_object(
238
+ 'id', a.id,
239
+ 'content', a.content
240
+ )
241
+ ),
242
+ 'answer_id', ua.selected_answer_id,
243
+ 'is_doubt', COALESCE(ua.is_doubt, FALSE),
244
+ 'is_answered', ua.selected_answer_id IS NOT NULL
245
+ ) AS result
246
+ FROM
247
+ questions q
248
+ LEFT JOIN
249
+ answers a ON q.id = a.question_id
250
+ LEFT JOIN
251
+ user_answers ua
252
+ ON q.id = ua.question_id
253
+ AND ua.quiz_attempt_id = $1
254
+ WHERE
255
+ q.id = $2
256
+ GROUP BY
257
+ q.id, q.quiz_id, q.content, ua.selected_answer_id, ua.is_doubt;
258
+ `
259
+
260
+ err := r.db.Raw(rawQuery, attemptID, questionID).Scan(&resultJSON).Error
261
+ if err != nil {
262
+ return nil, err
263
+ }
264
+
265
+ err = json.Unmarshal([]byte(resultJSON), &response)
266
+ if err != nil {
267
+ return nil, err
268
+ }
269
+
270
+ return &response, nil
271
+ }
272
+
273
+ func (r *quizRepository) UserGetQuestionByID(ctx context.Context, questionID int64) (*models.Question, error) {
274
+ var question models.Question
275
+
276
+ err := r.db.Where("id = ?", questionID).First(&question).Error
277
+ if err != nil {
278
+ return nil, err
279
+ }
280
+
281
+ return &question, nil
282
+ }
283
+
284
+ func (r *quizRepository) UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error) {
285
+ var userAnswer models.UserAnswer
286
+
287
+ err := r.db.Where("quiz_attempt_id = ? AND question_id = ?", attemptID, questionID).First(&userAnswer).Error
288
+ if err != nil {
289
+ return nil, err
290
+ }
291
+
292
+ return &userAnswer, nil
293
+ }
294
+
295
+ func (r *quizRepository) UserSaveUserAnswer(ctx context.Context, userAnswer *models.UserAnswer) error {
296
+ return r.db.Save(userAnswer).Error
297
+ }
298
+
299
+ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error) {
300
+ var resultJSON string
301
+ var reviewQuizResponse models.ReviewQuizResponse
302
+
303
+ reviews := make([]models.ReviewQuiz, 0)
304
+
305
+ rawQuery := `
306
+ SELECT
307
+ COALESCE(
308
+ json_agg(
309
+ json_build_object(
310
+ 'question', json_build_object(
311
+ 'id', q.id,
312
+ 'quiz_id', q.quiz_id,
313
+ 'content', q.content,
314
+ 'review', q.review
315
+ ),
316
+ 'answer_options', (
317
+ SELECT json_agg(
318
+ json_build_object(
319
+ 'id', a.id,
320
+ 'content', a.content,
321
+ 'is_correct', a.is_correct
322
+ )
323
+ ORDER BY a.id
324
+ )
325
+ FROM answers a
326
+ WHERE a.question_id = q.id
327
+ ),
328
+ 'answer_id', ua.selected_answer_id
329
+ ) ORDER BY q.order
330
+ ),
331
+ '[]'::json
332
+ ) AS reviews
333
+ FROM
334
+ questions q
335
+ LEFT JOIN
336
+ user_answers ua
337
+ ON q.id = ua.question_id
338
+ AND ua.quiz_attempt_id = ?
339
+ WHERE
340
+ q.quiz_id = ?
341
+ `
342
+
343
+ err := r.db.Raw(rawQuery, attemptID, quizID).Scan(&resultJSON).Error
344
+ if err != nil {
345
+ return nil, fmt.Errorf("failed to query reviews: %w", err)
346
+ }
347
+
348
+ // Unmarshal the JSON result into the Reviews field
349
+ err = json.Unmarshal([]byte(resultJSON), &reviews)
350
+ if err != nil {
351
+ return nil, fmt.Errorf("failed to unmarshal JSON result: %w", err)
352
+ }
353
+
354
+ reviewQuizResponse.Reviews = reviews
355
+ return &reviewQuizResponse, nil
356
+ }
357
+
358
+ func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error) {
359
+ var attempt models.QuizAttempt
360
+
361
+ err := r.db.Where("account_id = ? AND academy_id = ?", accountID, academyID).Last(&attempt).Error
362
+ if err != nil {
363
+ return nil, err
364
+ }
365
+
366
+ return &attempt, nil
367
+ }
368
+
369
+ func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error) {
370
+ var correctOptionID int64
371
+
372
+ rawQuery := `
373
+ SELECT id
374
+ FROM answers
375
+ WHERE question_id = ?
376
+ AND is_correct = TRUE
377
+ LIMIT 1;
378
+ `
379
+
380
+ err := r.db.Raw(rawQuery, questionID).Scan(&correctOptionID).Error
381
+ if err != nil {
382
+ return 0, err
383
+ }
384
+
385
+ return correctOptionID, nil
386
+ }
387
+
388
+ func (r *quizRepository) UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error {
389
+ r.db.Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
390
+ Delete(&models.AcademyMaterialProgress{})
391
+
392
+ // Delete quiz attempts for the quiz associated with the academy
393
+ r.db.Where("account_id = ? AND quiz_id IN (SELECT id FROM quizzes WHERE academy_id = ?)", accountID, academyID).
394
+ Delete(&models.QuizAttempt{})
395
+
396
+ return nil
397
+ }
space/space/space/router/quiz_route.go CHANGED
@@ -7,8 +7,11 @@ import (
7
  func (s *Server) QuizRoute() {
8
  userRouterGroup := s.router.Group("/api/v1/quiz")
9
  {
10
- // :id is academy id
11
- userRouterGroup.GET("/:id", middleware.AuthUser, s.quizController.UserGetQuiz)
12
- userRouterGroup.POST("/:id/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
 
 
 
13
  }
14
  }
 
7
  func (s *Server) QuizRoute() {
8
  userRouterGroup := s.router.Group("/api/v1/quiz")
9
  {
10
+ userRouterGroup.GET("", middleware.AuthUser, s.quizController.UserGetQuiz)
11
+ userRouterGroup.POST("/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
12
+ userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
13
+ userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
14
+ userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
15
+ userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
16
  }
17
  }
space/space/space/services/quiz_service.go CHANGED
@@ -3,29 +3,26 @@ package services
3
  import (
4
  "context"
5
  "errors"
6
- "math/rand"
7
  "time"
8
 
9
  "api.qobiltu.id/models"
10
  "api.qobiltu.id/pkg/validation"
11
  "api.qobiltu.id/repositories"
12
  "api.qobiltu.id/response"
 
13
  "gorm.io/gorm"
14
  )
15
 
16
  type QuizService interface {
17
  // === ADMIN ===
18
 
19
- // // === USER ===
20
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
21
  UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
22
-
23
- // UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
24
- // UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
25
- // UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
26
- // UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
27
- // UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
28
- // UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
29
  }
30
 
31
  type quizService struct {
@@ -48,6 +45,7 @@ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRe
48
  }
49
 
50
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
 
51
  quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
52
  AccountID: req.AccountID,
53
  AcademyID: req.AcademyID,
@@ -58,66 +56,52 @@ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttem
58
 
59
  quiz := &quizAttempt.Quiz
60
 
 
61
  existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
62
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
63
  return nil, response.HandleGormError(err, "Internal Server Error")
64
  }
65
 
 
66
  if existingAttempt != nil {
67
- return s.handleExistingAttempt(ctx, req, quiz, existingAttempt)
68
  }
69
 
70
  return s.handleNewAttempt(ctx, req, quiz)
71
  }
72
 
73
- func (s *quizService) handleExistingAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
74
  now := time.Now()
75
 
76
- if attempt.DueAt.Before(now) {
77
- if attempt.FinishedAt == nil {
78
- attempt.FinishedAt = &now
79
- attempt.Score = s.calculateQuizScore(ctx, attempt)
80
-
81
- if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
82
- return nil, response.HandleGormError(err, "Internal Server Error")
83
- }
84
-
85
- totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
86
- if err != nil {
87
- return nil, response.HandleGormError(err, "Internal Server Error")
88
- }
89
-
90
- if totalAttempts >= quiz.AttemptLimit {
91
- if err := s.academyRepository.UserResetAcademyProgressByID(ctx, req.AccountID, quiz.AcademyID); err != nil {
92
- return nil, response.HandleGormError(err, "Internal Server Error")
93
- }
94
-
95
- // hapus juga semua attempt quiz by account id dan quiz id
96
- if err := s.quizRepository.UserDeleteAttemptQuizByAccountIDAndQuizID(ctx, req.AccountID, quiz.ID); err != nil {
97
- return nil, response.HandleGormError(err, "Internal Server Error")
98
- }
99
- }
100
-
101
- return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
102
- }
103
-
104
- return nil, models.Exception{QuizAlreadyFinished: true, Message: "Quiz already finished"}
105
  }
106
 
 
107
  questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
108
  if err != nil {
109
  return nil, response.HandleGormError(err, "Internal Server Error")
110
  }
111
 
112
- questions = shuffleWithKey(questions, attempt.ID)
113
 
114
  return &models.UserAttemptQuizResponse{
115
- QuizAttempt: *attempt,
116
- Questions: questions,
 
 
 
 
 
 
117
  }, nil
118
  }
119
 
120
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
 
 
121
  totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
122
  if err != nil {
123
  return nil, response.HandleGormError(err, "Internal Server Error")
@@ -134,67 +118,202 @@ func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAtte
134
  return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
135
  }
136
 
137
- quizAttempt := models.QuizAttempt{
 
138
  AccountID: req.AccountID,
139
  QuizID: quiz.ID,
 
140
  StartedAt: time.Now(),
141
  DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
142
  }
143
 
144
- if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &quizAttempt); err != nil {
145
  return nil, response.HandleGormError(err, "Internal Server Error")
146
  }
147
 
148
- questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, quizAttempt.ID)
 
149
  if err != nil {
150
  return nil, response.HandleGormError(err, "Internal Server Error")
151
  }
152
 
153
- questions = shuffleWithKey(questions, quizAttempt.ID)
154
 
155
  return &models.UserAttemptQuizResponse{
156
- QuizAttempt: quizAttempt,
157
- Questions: questions,
 
 
 
 
 
 
158
  }, nil
159
  }
160
 
161
- func (s *quizService) calculateQuizScore(ctx context.Context, attempt *models.QuizAttempt) float64 {
 
 
 
 
162
  // ambil total question dari quiz
163
  totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
164
  if err != nil {
165
- return 0
166
  }
167
 
168
  if totalQuestion == 0 {
169
- return 0
 
 
 
 
 
170
  }
171
 
172
  // ambil semua user answer yang is_correct nya true
173
  correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
174
  if err != nil {
175
- return 0
176
  }
177
 
178
- // hitung score nya
179
  score := float64(correctAnswer) / float64(totalQuestion) * 100
180
 
181
- return score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
 
184
- // shuffleWithKey mengacak slice dengan menggunakan integer key sebagai seed
185
- // untuk memastikan hasil acak yang konsisten untuk key yang sama
186
- func shuffleWithKey[T any](slice []T, key int64) []T {
187
- // Buat salinan slice untuk menghindari modifikasi original
188
- shuffled := make([]T, len(slice))
189
- copy(shuffled, slice)
190
 
191
- // Buat random source dengan seed dari key
192
- r := rand.New(rand.NewSource(key))
 
 
193
 
194
- // Lakukan shuffling
195
- r.Shuffle(len(shuffled), func(i, j int) {
196
- shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
197
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
- return shuffled
200
  }
 
3
  import (
4
  "context"
5
  "errors"
 
6
  "time"
7
 
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/pkg/validation"
10
  "api.qobiltu.id/repositories"
11
  "api.qobiltu.id/response"
12
+ "api.qobiltu.id/utils"
13
  "gorm.io/gorm"
14
  )
15
 
16
  type QuizService interface {
17
  // === ADMIN ===
18
 
19
+ // === USER ===
20
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
21
  UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
22
+ UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
23
+ UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
24
+ UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
25
+ UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
 
 
 
26
  }
27
 
28
  type quizService struct {
 
45
  }
46
 
47
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
48
+ // ambil data quiz attempt
49
  quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
50
  AccountID: req.AccountID,
51
  AcademyID: req.AcademyID,
 
56
 
57
  quiz := &quizAttempt.Quiz
58
 
59
+ // ambil attempt quiz yg sedang aktif
60
  existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
61
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
62
  return nil, response.HandleGormError(err, "Internal Server Error")
63
  }
64
 
65
+ // jika ada attempt yang sedang active
66
  if existingAttempt != nil {
67
+ return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
68
  }
69
 
70
  return s.handleNewAttempt(ctx, req, quiz)
71
  }
72
 
73
+ func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
74
  now := time.Now()
75
 
76
+ // jika attempt nya telah melewati batas waktu dan belum submit / finish
77
+ if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
78
+ // frontend nge-trigger submit
79
+ return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
 
82
+ // jika masih dalam waktu yang ditentukan
83
  questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
84
  if err != nil {
85
  return nil, response.HandleGormError(err, "Internal Server Error")
86
  }
87
 
88
+ questions = utils.ShuffleWithKey(questions, attempt.ID)
89
 
90
  return &models.UserAttemptQuizResponse{
91
+ ID: attempt.ID,
92
+ AccountID: attempt.AccountID,
93
+ QuizID: attempt.QuizID,
94
+ StartedAt: attempt.StartedAt,
95
+ DueAt: attempt.DueAt,
96
+ FinishedAt: attempt.FinishedAt,
97
+ Score: attempt.Score,
98
+ Questions: questions,
99
  }, nil
100
  }
101
 
102
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
103
+
104
+ // ini sekedar untuk make sure apakah masih bisa attemp dan telah membaca semua materi.
105
  totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
106
  if err != nil {
107
  return nil, response.HandleGormError(err, "Internal Server Error")
 
118
  return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
119
  }
120
 
121
+ // buat attempt quiz
122
+ attempt := models.QuizAttempt{
123
  AccountID: req.AccountID,
124
  QuizID: quiz.ID,
125
+ AcademyID: quiz.AcademyID,
126
  StartedAt: time.Now(),
127
  DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
128
  }
129
 
130
+ if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &attempt); err != nil {
131
  return nil, response.HandleGormError(err, "Internal Server Error")
132
  }
133
 
134
+ // ambil question
135
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
136
  if err != nil {
137
  return nil, response.HandleGormError(err, "Internal Server Error")
138
  }
139
 
140
+ questions = utils.ShuffleWithKey(questions, attempt.ID)
141
 
142
  return &models.UserAttemptQuizResponse{
143
+ ID: attempt.ID,
144
+ AccountID: attempt.AccountID,
145
+ QuizID: attempt.QuizID,
146
+ StartedAt: attempt.StartedAt,
147
+ DueAt: attempt.DueAt,
148
+ FinishedAt: attempt.FinishedAt,
149
+ Score: attempt.Score,
150
+ Questions: questions,
151
  }, nil
152
  }
153
 
154
+ func (s *quizService) calculateQuizScore(
155
+ ctx context.Context,
156
+ quiz *models.Quiz,
157
+ attempt *models.QuizAttempt,
158
+ ) error {
159
  // ambil total question dari quiz
160
  totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
161
  if err != nil {
162
+ return err
163
  }
164
 
165
  if totalQuestion == 0 {
166
+ attempt.Score = 0
167
+ attempt.TotalQuestions = 0
168
+ attempt.TotalCorrectAnswer = 0
169
+ attempt.TotalWrongAnswer = 0
170
+ attempt.IsPassed = false
171
+ return nil
172
  }
173
 
174
  // ambil semua user answer yang is_correct nya true
175
  correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
176
  if err != nil {
177
+ return err
178
  }
179
 
 
180
  score := float64(correctAnswer) / float64(totalQuestion) * 100
181
 
182
+ attempt.Score = score
183
+ attempt.TotalQuestions = totalQuestion
184
+ attempt.TotalCorrectAnswer = correctAnswer
185
+ attempt.TotalWrongAnswer = totalQuestion - correctAnswer
186
+ attempt.IsPassed = score >= float64(quiz.MinScore)
187
+
188
+ now := time.Now()
189
+ attempt.FinishedAt = &now
190
+
191
+ return nil
192
+ }
193
+
194
+ func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
195
+ // pastiin dulu attemp nya ada atau tidak
196
+ _, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
197
+ if err != nil {
198
+ return nil, response.HandleGormError(err, "Internal Server Error")
199
+ }
200
+
201
+ // ambil pertanyaan dan jawaban pengguna
202
+ question, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
203
+ if err != nil {
204
+ if errors.Is(err, gorm.ErrRecordNotFound) {
205
+ return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
206
+ }
207
+ return nil, response.HandleGormError(err, "Internal Server Error")
208
+ }
209
+
210
+ return question, nil
211
  }
212
 
213
+ func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
 
 
 
 
 
214
 
215
+ correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, req.QuestionID)
216
+ if err != nil {
217
+ return nil, response.HandleGormError(err, "Internal Server Error")
218
+ }
219
 
220
+ question, err := s.quizRepository.UserGetUserAnswer(ctx, req.AttemptID, req.QuestionID)
221
+ if err != nil {
222
+
223
+ // jika belum ada answer
224
+ if errors.Is(err, gorm.ErrRecordNotFound) {
225
+ userAnswer := models.UserAnswer{
226
+ QuizAttemptID: req.AttemptID,
227
+ QuestionID: req.QuestionID,
228
+ AnswerID: req.AnswerID,
229
+ IsDoubt: req.IsDoubt,
230
+ IsCorrect: false,
231
+ }
232
+
233
+ if req.AnswerID != nil {
234
+ userAnswer.IsCorrect = *req.AnswerID == correctOptionID
235
+ }
236
+
237
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, &userAnswer); err != nil {
238
+ return nil, response.HandleGormError(err, "Internal Server Error")
239
+ }
240
+
241
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
242
+ if err != nil {
243
+ return nil, response.HandleGormError(err, "Internal Server Error")
244
+ }
245
+
246
+ return res, nil
247
+ }
248
+
249
+ return nil, response.HandleGormError(err, "Internal Server Error")
250
+ }
251
+
252
+ // updating answer
253
+ question.AnswerID = req.AnswerID
254
+ question.IsDoubt = req.IsDoubt
255
+
256
+ if req.AnswerID != nil {
257
+ question.IsCorrect = *req.AnswerID == correctOptionID
258
+ }
259
+
260
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, question); err != nil {
261
+ return nil, response.HandleGormError(err, "Internal Server Error")
262
+ }
263
+
264
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
265
+ if err != nil {
266
+ if errors.Is(err, gorm.ErrRecordNotFound) {
267
+ return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
268
+ }
269
+ return nil, response.HandleGormError(err, "Internal Server Error")
270
+ }
271
+
272
+ return res, nil
273
+ }
274
+
275
+ func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
276
+ attempt, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
277
+ if err != nil {
278
+ return nil, response.HandleGormError(err, "Internal Server Error")
279
+ }
280
+
281
+ if err := s.calculateQuizScore(ctx, attempt.Quiz, attempt); err != nil {
282
+ return nil, response.HandleGormError(err, "Internal Server Error")
283
+ }
284
+
285
+ if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
286
+ return nil, response.HandleGormError(err, "Internal Server Error")
287
+ }
288
+
289
+ remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, attempt.AccountID, attempt.AcademyID)
290
+ if err != nil {
291
+ return nil, response.HandleGormError(err, "Internal Server Error")
292
+ }
293
+
294
+ if remainingAttempts == 0 {
295
+ // hapus progress dan semua attempt pada quiz
296
+ if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, attempt.AccountID, attempt.QuizID); err != nil {
297
+ return nil, response.HandleGormError(err, "Internal Server Error")
298
+ }
299
+ }
300
+
301
+ return &models.SubmitQuizResponse{
302
+ QuizAttempt: *attempt,
303
+ RemainingAttempts: remainingAttempts,
304
+ }, nil
305
+ }
306
+
307
+ func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
308
+ attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
309
+ if err != nil {
310
+ return nil, response.HandleGormError(err, "Internal Server Error")
311
+ }
312
+
313
+ review, err := s.quizRepository.UserGetReviewQuiz(ctx, attempt.ID, attempt.AccountID, attempt.QuizID)
314
+ if err != nil {
315
+ return nil, response.HandleGormError(err, "Internal Server Error")
316
+ }
317
 
318
+ return review, nil
319
  }
space/space/space/space/response/api_response_v2.go CHANGED
@@ -41,6 +41,15 @@ func HandleError(c *gin.Context, err error) {
41
  responseError(c, http.StatusRequestTimeout, exception)
42
  case exception.AttemptNotFound:
43
  responseError(c, http.StatusNotFound, exception)
 
 
 
 
 
 
 
 
 
44
  case exception.ValidationError:
45
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
46
  default:
 
41
  responseError(c, http.StatusRequestTimeout, exception)
42
  case exception.AttemptNotFound:
43
  responseError(c, http.StatusNotFound, exception)
44
+ case exception.QuizTimeExpired:
45
+ responseError(c, http.StatusBadRequest, exception)
46
+ case exception.QuizAlreadyFinished:
47
+ responseError(c, http.StatusBadRequest, exception)
48
+ case exception.QuizAttemptLimit:
49
+ responseError(c, http.StatusBadRequest, exception)
50
+ case exception.AcademyNotFinished:
51
+ responseError(c, http.StatusBadRequest, exception)
52
+
53
  case exception.ValidationError:
54
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
55
  default:
space/space/space/space/space/config/database_connection_config.go CHANGED
@@ -62,11 +62,13 @@ func AutoMigrateAll(db *gorm.DB) {
62
  &models.RegionProvince{},
63
  &models.OptionCategory{},
64
  &models.OptionValues{},
 
65
  &models.Quiz{},
66
- &models.QuizAttempt{},
67
  &models.Question{},
68
  &models.Answer{},
 
69
  &models.UserAnswer{},
 
70
  &models.PersonalityAndPreferenceCV{},
71
  &models.FamilyMemberCV{},
72
  &models.PhysicalAndHealthCV{},
 
62
  &models.RegionProvince{},
63
  &models.OptionCategory{},
64
  &models.OptionValues{},
65
+
66
  &models.Quiz{},
 
67
  &models.Question{},
68
  &models.Answer{},
69
+ &models.QuizAttempt{},
70
  &models.UserAnswer{},
71
+
72
  &models.PersonalityAndPreferenceCV{},
73
  &models.FamilyMemberCV{},
74
  &models.PhysicalAndHealthCV{},
space/space/space/space/space/controller/quiz/quiz_controller.go ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package quiz_controller
2
+
3
+ import (
4
+ "net/http"
5
+ "strconv"
6
+
7
+ "api.qobiltu.id/middleware"
8
+ "api.qobiltu.id/models"
9
+ "api.qobiltu.id/response"
10
+ "api.qobiltu.id/services"
11
+ "github.com/gin-gonic/gin"
12
+ )
13
+
14
+ type QuizController interface {
15
+ // === ADMIN ===
16
+
17
+ // === USER ===
18
+ UserGetQuiz(ctx *gin.Context)
19
+ UserAttemptQuiz(ctx *gin.Context)
20
+ }
21
+
22
+ type quizController struct {
23
+ quizService services.QuizService
24
+ }
25
+
26
+ func NewQuizController(quizService services.QuizService) QuizController {
27
+ return &quizController{
28
+ quizService: quizService,
29
+ }
30
+ }
31
+
32
+ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
33
+ academyID := ctx.Param("id")
34
+ academyIDInt, err := strconv.Atoi(academyID)
35
+ if err != nil {
36
+ response.HandleError(ctx, err)
37
+ return
38
+ }
39
+
40
+ accountData := middleware.GetAccountData(ctx)
41
+ req := models.UserGetQuizRequest{
42
+ AccountID: int64(accountData.UserID),
43
+ AcademyID: int64(academyIDInt),
44
+ }
45
+
46
+ res, err := c.quizService.UserGetQuiz(ctx, &req)
47
+ if err != nil {
48
+ response.HandleError(ctx, err)
49
+ return
50
+ }
51
+
52
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", http.StatusOK, res)
53
+ }
54
+
55
+ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
56
+ academyID := ctx.Param("id")
57
+ academyIDInt, err := strconv.Atoi(academyID)
58
+ if err != nil {
59
+ response.HandleError(ctx, err)
60
+ return
61
+ }
62
+
63
+ accountData := middleware.GetAccountData(ctx)
64
+ req := models.UserAttemptQuizRequest{
65
+ AccountID: int64(accountData.UserID),
66
+ AcademyID: int64(academyIDInt),
67
+ }
68
+
69
+ res, err := c.quizService.UserAttemptQuiz(ctx, &req)
70
+ if err != nil {
71
+ response.HandleError(ctx, err)
72
+ return
73
+ }
74
+
75
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", http.StatusOK, res)
76
+ }
space/space/space/space/space/main.go CHANGED
@@ -12,6 +12,7 @@ import (
12
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
13
  options_controller "api.qobiltu.id/controller/options"
14
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
 
15
  region_controller "api.qobiltu.id/controller/region"
16
  "api.qobiltu.id/pkg/mail"
17
  "api.qobiltu.id/pkg/storage"
@@ -75,6 +76,10 @@ func main() {
75
  academyService := services.NewAcademyService(academyRepository, validator)
76
  academyController := academy_controller.NewAcademyController(academyService)
77
 
 
 
 
 
78
  cvRepository := repositories.NewCVRepository(config.DB)
79
  cvService := services.NewCVService(cvRepository, localStorage, validator)
80
  cvController := cv_controller.NewCVController(cvService)
@@ -98,6 +103,7 @@ func main() {
98
  optionController,
99
  emailController,
100
  academyController,
 
101
  cvController,
102
  marriageReadinessProfileController,
103
  partnerCriteriaController,
 
12
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
13
  options_controller "api.qobiltu.id/controller/options"
14
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
15
+ quiz_controller "api.qobiltu.id/controller/quiz"
16
  region_controller "api.qobiltu.id/controller/region"
17
  "api.qobiltu.id/pkg/mail"
18
  "api.qobiltu.id/pkg/storage"
 
76
  academyService := services.NewAcademyService(academyRepository, validator)
77
  academyController := academy_controller.NewAcademyController(academyService)
78
 
79
+ quizRepository := repositories.NewQuizRepository(config.DB)
80
+ quizService := services.NewQuizService(quizRepository, academyRepository, validator)
81
+ quizController := quiz_controller.NewQuizController(quizService)
82
+
83
  cvRepository := repositories.NewCVRepository(config.DB)
84
  cvService := services.NewCVService(cvRepository, localStorage, validator)
85
  cvController := cv_controller.NewCVController(cvService)
 
103
  optionController,
104
  emailController,
105
  academyController,
106
+ quizController,
107
  cvController,
108
  marriageReadinessProfileController,
109
  partnerCriteriaController,
space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -134,57 +134,71 @@ type RegionCity struct {
134
  FullCode string `json:"full_code"`
135
  ProvinceID uint `json:"province_id"`
136
  }
137
- type Answer struct {
138
- ID uint `gorm:"primaryKey" json:"id"`
139
- QuestionID uint `json:"question_id"`
140
- Content string `json:"content"`
141
- IsCorrect bool `json:"-"`
142
- }
143
- type Question struct {
144
- ID uint `gorm:"primaryKey" json:"id"`
145
- QuizID uint `json:"quiz_id"`
146
- Content string `json:"content"`
147
- Order int `json:"order"`
148
- CorrectAnswer uint `json:"correct_answer"`
149
- Review string `json:"reviews"`
150
- }
151
- type Quiz struct {
152
- ID uint `gorm:"primaryKey" json:"id"`
153
- AcademyID uint `json:"academy_id"`
154
- Slug string `json:"slug" gorm:"uniqueIndex" `
155
- Title string `json:"title"`
156
- Description string `json:"description"`
157
- TotalQuestions int `json:"total_questions"`
158
- AttemptLimit int `json:"attempt_limit"`
159
- TimeLimit int `json:"time_limit"`
160
- MinScore int `json:"min_score"`
161
- CreatedAt time.Time `json:"created_at"`
162
- }
163
 
164
- type QuizAttempt struct {
165
- ID uint `gorm:"primaryKey" json:"id"`
166
- AccountID uint `json:"user_id"`
167
- QuizID uint `json:"quiz_id"`
168
- StartedAt time.Time `json:"started_at"`
169
- DueAt time.Time `json:"due_at"`
170
- FinishedAt *time.Time `json:"finished_at"`
171
- Score float64 `json:"score"`
172
- }
173
- type UserAnswer struct {
174
- ID uint `gorm:"primaryKey" json:"id"`
175
- QuizAttemptID uint `json:"quiz_attempt_id"`
176
- QuestionID uint `json:"question_id"`
177
- SelectedAnswer uint `json:"selected_answer"`
178
- IsDoubt bool `json:"id_doubt"`
179
- IsCorrect bool `json:"is_correct"`
180
- }
181
- type QuizResult struct {
182
- QuizAttemptID uint `gorm:"column:quiz_attempt_id" json:"quiz_attempt_id"`
183
- TotalQuestions int `gorm:"column:total_questions" json:"total_questions"`
184
- CorrectAnswers int `gorm:"column:correct_answers" json:"correct_answers"`
185
- AverageScore float64 `gorm:"column:average_score" json:"average_score"`
186
- IsPassed bool `gorm:"column:is_passed" json:"is_passed"`
187
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  type (
190
  PersonalityAndPreferenceCV struct {
@@ -243,27 +257,34 @@ type (
243
  }
244
 
245
  WorshipAndReligiousUnderstandingCV struct {
246
- ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" counter:"skip"`
247
- AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id" counter:"skip"`
248
- Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
 
 
249
  ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
250
  CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
251
  TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
252
  DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
253
- QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
254
- QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
255
- WeeklyReligiousStudyFrequency *string `gorm:"column:weekly_religious_study_frequency" json:"weekly_religious_study_frequency"` // kajian_yang_diikuti_dalam_sepekan
256
  DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
257
  AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
 
 
 
258
  HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
259
- ListeningToMusic *string `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
260
- OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
261
- OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
262
- OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
263
- WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
264
- FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
265
- CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
266
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
 
 
 
 
 
267
  FieldCounter
268
  }
269
 
@@ -335,6 +356,46 @@ func (w WorshipAndReligiousUnderstandingCV) GetFilledFields() []string {
335
  return w.FieldCounter.GetFilledFields(w)
336
  }
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  type (
339
  MarriageReadinessProfile struct {
340
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
@@ -431,9 +492,9 @@ func (AcademyMaterial) TableName() string { return "academy_materials
431
  func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
432
  func (RegionProvince) TableName() string { return "region_provinces" }
433
  func (RegionCity) TableName() string { return "region_cities" }
434
- func (Answer) TableName() string { return "answers" }
435
- func (Question) TableName() string { return "questions" }
436
  func (Quiz) TableName() string { return "quizzes" }
 
 
437
  func (QuizAttempt) TableName() string { return "quiz_attempts" }
438
  func (UserAnswer) TableName() string { return "user_answers" }
439
  func (OptionCategory) TableName() string { return "option_categories" }
 
134
  FullCode string `json:"full_code"`
135
  ProvinceID uint `json:"province_id"`
136
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ type (
139
+ Quiz struct {
140
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
141
+ AcademyID int64 `gorm:"column:academy_id;not null" json:"academy_id"`
142
+ Academy *Academy `gorm:"foreignKey:AcademyID;constraint:OnDelete:CASCADE" json:"academy,omitempty"`
143
+ Slug string `gorm:"column:slug;uniqueIndex" json:"slug" `
144
+ Title string `gorm:"column:title" json:"title"`
145
+ Description string `gorm:"column:description" json:"description"`
146
+ AttemptLimit int64 `gorm:"column:attempt_limit" json:"attempt_limit"`
147
+ TimeLimit int64 `gorm:"column:time_limit" json:"time_limit"`
148
+ MinScore int64 `gorm:"column:min_score" json:"min_score"`
149
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
150
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
151
+ }
152
+
153
+ Question struct {
154
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
155
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
156
+ Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
157
+ Content string `gorm:"column:content" json:"content"`
158
+ Order int `gorm:"column:order" json:"order"`
159
+ Review string `gorm:"column:reviews" json:"reviews"`
160
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
161
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
162
+ }
163
+
164
+ Answer struct {
165
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
166
+ QuestionID int64 `gorm:"column:question_id;not null" json:"question_id"`
167
+ Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
168
+ Content string `gorm:"column:content" json:"content"`
169
+ IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
170
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
171
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
172
+ }
173
+
174
+ QuizAttempt struct {
175
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
176
+ AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
177
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
178
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
179
+ Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
180
+ StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
181
+ DueAt time.Time `gorm:"column:due_at" json:"due_at"`
182
+ FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
183
+ Score float64 `gorm:"column:score" json:"score"`
184
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
185
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
186
+ }
187
+
188
+ UserAnswer struct {
189
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
190
+ QuizAttemptID int64 `gorm:"column:quiz_attempt_id;not null" json:"quiz_attempt_id"`
191
+ QuizAttempt *QuizAttempt `gorm:"foreignKey:QuizAttemptID;constraint:OnDelete:CASCADE" json:"quiz_attempt,omitempty"`
192
+ QuestionID int64 `gorm:"column:question_id;not null" json:"question_id"`
193
+ Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
194
+ AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
195
+ Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
196
+ IsDoubt bool `gorm:"column:id_doubt" json:"id_doubt"`
197
+ IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
198
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
199
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
200
+ }
201
+ )
202
 
203
  type (
204
  PersonalityAndPreferenceCV struct {
 
257
  }
258
 
259
  WorshipAndReligiousUnderstandingCV struct {
260
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" counter:"skip"`
261
+ AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id" counter:"skip"`
262
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
263
+
264
+ // Data ibadah
265
  ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
266
  CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
267
  TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
268
  DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
 
 
 
269
  DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
270
  AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
271
+ QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
272
+ QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
273
+ WeeklyReligiousStudyFrequency *string `gorm:"column:weekly_religious_study_frequency" json:"weekly_religious_study_frequency"` // jumlah kajian yang diikuti dalam seminggu
274
  HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
275
+
276
+ // Data Pemahaman Agama
277
+ ListeningToMusic *bool `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
278
+ OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
279
+ OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
280
+ OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
281
+ OpinionOnBeard *string `gorm:"column:opinion_on_beard" json:"opinion_on_beard"` // pendapat_tentang_jenggot_pada_laki_laki
282
+ OpinionOnPantsAboveAnkle *string `gorm:"column:opinion_on_pants_above_ankle" json:"opinion_on_pants_above_ankle"` // pendapat_tentang_celana_di_atas_mata_kaki
283
+ WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
284
+ FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
285
+
286
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
287
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
288
  FieldCounter
289
  }
290
 
 
356
  return w.FieldCounter.GetFilledFields(w)
357
  }
358
 
359
+ var WorshipFields = []string{
360
+ "ObligatoryPrayer",
361
+ "CongregationalPrayer",
362
+ "TahajjudPrayer",
363
+ "DhuhaPrayer",
364
+ "DaudFasting",
365
+ "AyyamulBidhFasting",
366
+ "QuranReadingAbility",
367
+ "QuranMemorization",
368
+ "WeeklyReligiousStudyFrequency",
369
+ "HajjOrUmrah",
370
+ }
371
+
372
+ var ReligiousUnderstandingFields = []string{
373
+ "ListeningToMusic",
374
+ "OpinionOnIkhtilat",
375
+ "OpinionOnTouchingNonMahram",
376
+ "OpinionOnVeil",
377
+ "OpinionOnBeard",
378
+ "OpinionOnPantsAboveAnkle",
379
+ "WeeklyReligiousStudies",
380
+ "FollowedUstadz",
381
+ }
382
+
383
+ func (w WorshipAndReligiousUnderstandingCV) GetTotalFieldsWorship() int {
384
+ return w.FieldCounter.CountFieldsByNames(w, WorshipFields)
385
+ }
386
+
387
+ func (w WorshipAndReligiousUnderstandingCV) GetTotalFieldsReligiousUnderstanding() int {
388
+ return w.FieldCounter.CountFieldsByNames(w, ReligiousUnderstandingFields)
389
+ }
390
+
391
+ func (w WorshipAndReligiousUnderstandingCV) GetFilledFieldsWorship() []string {
392
+ return w.FieldCounter.GetFilledFieldsByNames(w, WorshipFields)
393
+ }
394
+
395
+ func (w WorshipAndReligiousUnderstandingCV) GetFilledFieldsReligiousUnderstanding() []string {
396
+ return w.FieldCounter.GetFilledFieldsByNames(w, ReligiousUnderstandingFields)
397
+ }
398
+
399
  type (
400
  MarriageReadinessProfile struct {
401
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
 
492
  func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
493
  func (RegionProvince) TableName() string { return "region_provinces" }
494
  func (RegionCity) TableName() string { return "region_cities" }
 
 
495
  func (Quiz) TableName() string { return "quizzes" }
496
+ func (Question) TableName() string { return "questions" }
497
+ func (Answer) TableName() string { return "answers" }
498
  func (QuizAttempt) TableName() string { return "quiz_attempts" }
499
  func (UserAnswer) TableName() string { return "user_answers" }
500
  func (OptionCategory) TableName() string { return "option_categories" }
space/space/space/space/space/models/exception_model.go CHANGED
@@ -18,6 +18,12 @@ type Exception struct {
18
  Forbidden bool `json:"forbidden,omitempty"`
19
  ValidationError bool `json:"validation_error,omitempty"`
20
 
 
 
 
 
 
 
21
  Message string `json:"message,omitempty"`
22
  Err error `json:"-"`
23
  ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
 
18
  Forbidden bool `json:"forbidden,omitempty"`
19
  ValidationError bool `json:"validation_error,omitempty"`
20
 
21
+ // quiz context
22
+ QuizTimeExpired bool `json:"quiz_time_expired,omitempty"`
23
+ QuizAttemptLimit bool `json:"quiz_attempt_limit,omitempty"`
24
+ QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
25
+ AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
26
+
27
  Message string `json:"message,omitempty"`
28
  Err error `json:"-"`
29
  ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
space/space/space/space/space/models/field_counter.go CHANGED
@@ -97,6 +97,46 @@ func (fc FieldCounter) GetFilledFields(s any) []string {
97
  return filledFields
98
  }
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  // isFieldFilled memeriksa apakah field memiliki nilai non-zero
101
  func isFieldFilled(field reflect.Value) bool {
102
  // Jika field tidak dapat di-address atau diakses, anggap kosong
 
97
  return filledFields
98
  }
99
 
100
+ // GetFilledFieldsByNames mengembalikan field terisi berdasarkan nama field yang diberikan
101
+ func (fc FieldCounter) GetFilledFieldsByNames(s any, fieldNames []string) []string {
102
+ var filledFields []string
103
+ v := reflect.ValueOf(s)
104
+
105
+ if v.Kind() == reflect.Ptr {
106
+ if v.IsNil() {
107
+ return filledFields
108
+ }
109
+ v = v.Elem()
110
+ }
111
+
112
+ t := v.Type()
113
+ fieldNameSet := make(map[string]struct{}, len(fieldNames))
114
+ for _, name := range fieldNames {
115
+ fieldNameSet[name] = struct{}{}
116
+ }
117
+
118
+ for i := 0; i < v.NumField(); i++ {
119
+ field := v.Field(i)
120
+ fieldType := t.Field(i)
121
+ name := fieldType.Name
122
+
123
+ if shouldSkipField(fieldType) {
124
+ continue
125
+ }
126
+
127
+ if _, ok := fieldNameSet[name]; ok && isFieldFilled(field) {
128
+ filledFields = append(filledFields, name)
129
+ }
130
+ }
131
+
132
+ return filledFields
133
+ }
134
+
135
+ // CountFieldsByNames menghitung total field dari daftar yang diberikan (dianggap valid dan bukan skip)
136
+ func (fc FieldCounter) CountFieldsByNames(s any, fieldNames []string) int {
137
+ return len(fieldNames)
138
+ }
139
+
140
  // isFieldFilled memeriksa apakah field memiliki nilai non-zero
141
  func isFieldFilled(field reflect.Value) bool {
142
  // Jika field tidak dapat di-address atau diakses, anggap kosong
space/space/space/space/space/models/request_model.go CHANGED
@@ -152,22 +152,31 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
152
  }
153
 
154
  type (
155
- ListQuizRequest struct {
156
- AccountID int64 `json:"account_id"`
157
  AcademyID int64 `json:"academy_id" validate:"required"`
158
  }
159
 
160
- ListQuizResponse struct {
161
- Quiz []Quiz `json:"quiz"`
 
 
162
  }
163
 
164
- AttemptQuizRequest struct {
165
  AccountID int64 `json:"account_id"`
166
- QuizID int64 `json:"quiz_id" validate:"required"`
 
 
 
 
 
 
167
  }
168
 
169
- AttemptQuizResponse struct {
170
- QuizAttempt QuizAttempt `json:"quiz_attempt"`
 
171
  }
172
 
173
  GetQuestionQuizRequest struct {
@@ -356,10 +365,12 @@ type (
356
  DaudFasting *string `json:"daud_fasting"` // puasa_daud
357
  AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
358
  HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
359
- ListeningToMusic *string `json:"listening_to_music"` // mendengarkan_musik
360
  OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
361
  OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
362
  OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
 
 
363
  WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
364
  FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
365
  }
@@ -467,15 +478,16 @@ type (
467
  }
468
 
469
  GetProgressCVResponse struct {
470
- AccountDetailsProgress float64 `json:"account_details_progress"`
471
- PersonalityAndPreferenceCVProgress float64 `json:"personality_and_preference_cv_progress"`
472
- FamilyMemberCVProgress float64 `json:"family_member_cv_progress"`
473
- PhysicalAndHealthCVProgress float64 `json:"physical_and_health_cv_progress"`
474
- WorshipAndReligiousUnderstandingCVProgress float64 `json:"worship_and_religious_understanding_cv_progress"`
475
- EducationCVProgress float64 `json:"education_cv_progress"`
476
- JobCVProgress float64 `json:"job_cv_progress"`
477
- AchievementCVProgress float64 `json:"achievement_cv_progress"`
478
- TotalProgress float64 `json:"total_progress"`
 
479
  }
480
  )
481
 
 
152
  }
153
 
154
  type (
155
+ UserGetQuizRequest struct {
156
+ AccountID int64 `json:"account_id" validate:"required"`
157
  AcademyID int64 `json:"academy_id" validate:"required"`
158
  }
159
 
160
+ UserGetQuizResponse struct {
161
+ Quiz
162
+ TotalQuestions int64 `json:"total_questions"`
163
+ UserAttempts int64 `json:"user_attempts"`
164
  }
165
 
166
+ UserAttemptQuizRequest struct {
167
  AccountID int64 `json:"account_id"`
168
+ AcademyID int64 `json:"academy_id"`
169
+ }
170
+
171
+ UserAttemptQuizQuestionsResponse struct {
172
+ ID int64 `gorm:"column:id" json:"id"`
173
+ IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
174
+ IsAnswered bool `gorm:"column:is_answered" json:"is_answered"`
175
  }
176
 
177
+ UserAttemptQuizResponse struct {
178
+ QuizAttempt
179
+ Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
180
  }
181
 
182
  GetQuestionQuizRequest struct {
 
365
  DaudFasting *string `json:"daud_fasting"` // puasa_daud
366
  AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
367
  HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
368
+ ListeningToMusic *bool `json:"listening_to_music"` // mendengarkan_musik
369
  OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
370
  OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
371
  OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
372
+ OpinionOnBeard *string `json:"opinion_on_beard"` // pendapat_tentang_jenggot_pada_laki_laki
373
+ OpinionOnPantsAboveAnkle *string `json:"opinion_on_pants_above_ankle"` // pendapat_tentang_celana_di_atas_mata_kaki
374
  WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
375
  FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
376
  }
 
478
  }
479
 
480
  GetProgressCVResponse struct {
481
+ AccountDetailsProgress float64 `json:"account_details_progress"`
482
+ PersonalityAndPreferenceCVProgress float64 `json:"personality_and_preference_cv_progress"`
483
+ FamilyMemberCVProgress float64 `json:"family_member_cv_progress"`
484
+ PhysicalAndHealthCVProgress float64 `json:"physical_and_health_cv_progress"`
485
+ WorshipCVProgress float64 `json:"worship_cv_progress"`
486
+ ReligiousUnderstandingCVProgress float64 `json:"religious_understanding_cv_progress"`
487
+ EducationCVProgress float64 `json:"education_cv_progress"`
488
+ JobCVProgress float64 `json:"job_cv_progress"`
489
+ AchievementCVProgress float64 `json:"achievement_cv_progress"`
490
+ TotalProgress float64 `json:"total_progress"`
491
  }
492
  )
493
 
space/space/space/space/space/models/response_model.go CHANGED
@@ -45,11 +45,6 @@ type QuestionResponse struct {
45
  IsDoubt bool `json:"is_doubt"`
46
  }
47
 
48
- type QuizResultResponse struct {
49
- QuizAttempt QuizAttempt `json:"quiz_attempt"`
50
- Result QuizResult `json:"result"`
51
- }
52
-
53
  type OnExamUserAnswerResponse struct {
54
  ID uint `gorm:"primaryKey" json:"id"`
55
  QuizAttemptID uint `json:"quiz_attempt_id"`
 
45
  IsDoubt bool `json:"is_doubt"`
46
  }
47
 
 
 
 
 
 
48
  type OnExamUserAnswerResponse struct {
49
  ID uint `gorm:"primaryKey" json:"id"`
50
  QuizAttemptID uint `json:"quiz_attempt_id"`
space/space/space/space/space/repositories/academy_repository.go CHANGED
@@ -26,12 +26,14 @@ type AcademyRepository interface {
26
 
27
  // === USER ===
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
 
29
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
30
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
31
  UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
32
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
33
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
34
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
 
35
  }
36
 
37
  type academyRepository struct {
@@ -286,6 +288,37 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
286
  return academies, pageInfo, nil
287
  }
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error) {
290
  materials := make([]models.UserAcademyMaterialResponse, 0)
291
  offset := req.Filter.GetOffset()
@@ -414,3 +447,18 @@ func (r *academyRepository) UserGetAcademyMaterialProgressByAccountID(ctx contex
414
  func (r *academyRepository) UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error {
415
  return r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).Delete(&models.AcademyMaterialProgress{}).Error
416
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  // === USER ===
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
+ UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error)
30
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
31
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
32
  UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
33
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
34
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
35
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
36
+ UserResetAcademyProgressByID(ctx context.Context, accountID int64, academyID int64) error
37
  }
38
 
39
  type academyRepository struct {
 
288
  return academies, pageInfo, nil
289
  }
290
 
291
+ func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error) {
292
+ var totalMaterial int64
293
+ var totalReadMaterial int64
294
+
295
+ // Hitung total materi dalam academy
296
+ err := r.db.WithContext(ctx).
297
+ Model(&models.AcademyMaterial{}).
298
+ Where("academy_id = ?", academyID).
299
+ Count(&totalMaterial).Error
300
+ if err != nil {
301
+ return 0, err
302
+ }
303
+
304
+ // Hitung total materi yang sudah dibaca oleh user
305
+ err = r.db.WithContext(ctx).
306
+ Model(&models.AcademyMaterialProgress{}).
307
+ Joins("JOIN academy_materials am ON am.id = academy_materials_progress.academy_material_id").
308
+ Where("academy_materials_progress.account_id = ? AND am.academy_id = ?", accountID, academyID).
309
+ Count(&totalReadMaterial).Error
310
+ if err != nil {
311
+ return 0, err
312
+ }
313
+
314
+ // Hitung persentase
315
+ if totalMaterial == 0 {
316
+ return 0, nil
317
+ }
318
+ percentage := float64(totalReadMaterial) / float64(totalMaterial) * 100
319
+ return percentage, nil
320
+ }
321
+
322
  func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error) {
323
  materials := make([]models.UserAcademyMaterialResponse, 0)
324
  offset := req.Filter.GetOffset()
 
447
  func (r *academyRepository) UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error {
448
  return r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).Delete(&models.AcademyMaterialProgress{}).Error
449
  }
450
+
451
+ func (r *academyRepository) UserResetAcademyProgressByID(ctx context.Context, accountID int64, academyID int64) error {
452
+ // Subquery untuk mendapatkan semua material ID dari academyID
453
+ subQuery := r.db.
454
+ WithContext(ctx).
455
+ Model(&models.AcademyMaterial{}).
456
+ Select("id").
457
+ Where("academy_id = ?", academyID)
458
+
459
+ // Delete progress berdasarkan account_id dan academy_material_id yang ada di subquery
460
+ return r.db.
461
+ WithContext(ctx).
462
+ Where("account_id = ? AND academy_material_id IN (?)", accountID, subQuery).
463
+ Delete(&models.AcademyMaterialProgress{}).Error
464
+ }
space/space/space/space/space/repositories/quiz_repository.go CHANGED
@@ -8,7 +8,15 @@ import (
8
  )
9
 
10
  type QuizRepository interface {
11
- UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
 
 
 
 
 
 
 
 
12
  }
13
 
14
  type quizRepository struct {
@@ -19,144 +27,129 @@ func NewQuizRepository(db *gorm.DB) QuizRepository {
19
  return &quizRepository{db: db}
20
  }
21
 
22
- func (r *quizRepository) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
23
- panic("not implemented")
24
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
27
- repo := Construct[models.Quiz, []models.Quiz](
28
- models.Quiz{AcademyID: academyId},
29
- )
30
- repo.Transactions(
31
- WhereGivenConstructor[models.Quiz, []models.Quiz],
32
- Find[models.Quiz, []models.Quiz],
33
- )
34
- return *repo
35
- }
36
 
37
- func GetAllUserAttempt(userId uint, quizId uint) Repository[models.QuizAttempt, []models.QuizAttempt] {
38
- repo := Construct[models.QuizAttempt, []models.QuizAttempt](
39
- models.QuizAttempt{AccountID: userId, QuizID: quizId},
40
- )
41
- repo.Transactions(
42
- WhereGivenConstructor[models.QuizAttempt, []models.QuizAttempt],
43
- Find[models.QuizAttempt, []models.QuizAttempt],
44
- )
45
- return *repo
46
  }
47
 
48
- func GetQuizbyId(quizId uint) Repository[models.Quiz, models.Quiz] {
49
- repo := Construct[models.Quiz, models.Quiz](
50
- models.Quiz{ID: quizId},
51
- )
52
- repo.Transactions(
53
- WhereGivenConstructor[models.Quiz, models.Quiz],
54
- Find[models.Quiz, models.Quiz],
55
- )
56
 
57
- return *repo
58
- }
 
 
59
 
60
- func GetUserLastAttempt(userId uint, quizId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
61
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
62
- models.QuizAttempt{AccountID: userId, QuizID: quizId},
63
- )
64
- repo.Transaction.Where(&repo.Constructor).Last(&repo.Result)
65
- repo.RowsError = repo.Transaction.Error
66
- repo.NoRecord = false
67
- // fmt.Println(repo.Transaction.RowsAffected) Kenapa 0 !!!!
68
- return *repo
69
  }
70
 
71
- func GetAttemptByIdandUserId(attemptId uint, userId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
72
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
73
- models.QuizAttempt{
74
- ID: attemptId,
75
- AccountID: userId,
76
- },
77
- )
78
- repo.Transactions(
79
- WhereGivenConstructor[models.QuizAttempt, models.QuizAttempt],
80
- Find[models.QuizAttempt, models.QuizAttempt],
81
- )
82
- return *repo
83
  }
84
 
85
- func GetAttemptByUserId(userId uint) Repository[models.QuizAttempt, []models.QuizAttempt] {
86
- repo := Construct[models.QuizAttempt, []models.QuizAttempt](
87
- models.QuizAttempt{
88
- AccountID: userId,
89
- },
90
- )
91
- repo.Transactions(
92
- WhereGivenConstructor[models.QuizAttempt, []models.QuizAttempt],
93
- Find[models.QuizAttempt, []models.QuizAttempt],
94
- )
95
- return *repo
96
- }
97
- func CreateAttempt(quizAttempt models.QuizAttempt) Repository[models.QuizAttempt, models.QuizAttempt] {
98
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
99
- quizAttempt,
100
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- Create(repo)
103
- return *repo
104
  }
105
 
106
- func GetUserAnswerByAttemptQuestionId(attemptId uint, questionId uint) Repository[models.UserAnswer, models.UserAnswer] {
107
- repo := Construct[models.UserAnswer, models.UserAnswer](
108
- models.UserAnswer{
109
- QuizAttemptID: attemptId,
110
- QuestionID: questionId,
111
- },
112
- )
113
- repo.Transactions(
114
- WhereGivenConstructor[models.UserAnswer, models.UserAnswer],
115
- Find[models.UserAnswer, models.UserAnswer],
116
- )
117
- return *repo
118
  }
119
 
120
- func CreateUserAnswer(userAnswer models.UserAnswer) Repository[models.UserAnswer, models.UserAnswer] {
121
- repo := Construct[models.UserAnswer, models.UserAnswer](
122
- userAnswer,
123
- )
124
 
125
- Create(repo)
126
- return *repo
127
- }
 
128
 
129
- func UpdateUserAnswer(userAnswer models.UserAnswer) Repository[models.UserAnswer, models.UserAnswer] {
130
- repo := Construct[models.UserAnswer, models.UserAnswer](
131
- userAnswer,
132
- )
133
- Update(repo)
134
- return *repo
135
  }
136
 
137
- func UpdateQuizAttempt(quizAttempt models.QuizAttempt) Repository[models.QuizAttempt, models.QuizAttempt] {
138
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
139
- quizAttempt,
140
- )
141
- Update(repo)
142
- return *repo
143
- }
144
 
145
- func CountUserAttemptScore(attemptId uint) Repository[models.QuizAttempt, models.QuizResult] {
146
- repo := Construct[models.QuizAttempt, models.QuizResult](
147
- models.QuizAttempt{ID: attemptId},
148
- )
149
- repo.Transaction.Model(&repo.Constructor).Raw("SELECT quiz_attempt_id,COUNT(*) AS total_questions,SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS correct_answers,CAST(SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) AS average_score FROM user_answers WHERE quiz_attempt_id = ? GROUP BY quiz_attempt_id", attemptId).Scan(&repo.Result)
150
- repo.RowsError = repo.Transaction.Error
151
- repo.NoRecord = false
152
- return *repo
153
  }
154
 
155
- func GetUserAnswerByAttemptId(attemptId uint) Repository[models.UserAnswer, []models.OnExamUserAnswerResponse] {
156
- repo := Construct[models.UserAnswer, []models.OnExamUserAnswerResponse](
157
- models.UserAnswer{QuizAttemptID: attemptId},
158
- )
 
 
 
 
 
 
159
 
160
- repo.Transaction.Model(&repo.Constructor).Find(&repo.Result)
161
- return *repo
162
  }
 
8
  )
9
 
10
  type QuizRepository interface {
11
+ UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
12
+ UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
13
+ UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
14
+ UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
15
+ UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
16
+ UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
17
+ UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
18
+ UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
19
+ UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
20
  }
21
 
22
  type quizRepository struct {
 
27
  return &quizRepository{db: db}
28
  }
29
 
30
+ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
31
+ var quizResponse models.UserGetQuizResponse
32
+
33
+ rawQuery := `
34
+ SELECT
35
+ q.*,
36
+ (SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
37
+ COALESCE(COUNT(qa.id), 0) AS user_attempts
38
+ FROM
39
+ quizzes q
40
+ LEFT JOIN
41
+ quiz_attempts qa
42
+ ON q.id = qa.quiz_id
43
+ AND qa.account_id = ?
44
+ WHERE
45
+ q.academy_id = ?
46
+ GROUP BY
47
+ q.id, q.academy_id, q.slug, q.title, q.description,
48
+ q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
49
+ `
50
 
51
+ err := r.db.Raw(rawQuery, req.AccountID, req.AcademyID).Scan(&quizResponse).Error
52
+ if err != nil {
53
+ return nil, err
54
+ }
 
 
 
 
 
 
55
 
56
+ if quizResponse.ID == 0 {
57
+ return nil, gorm.ErrRecordNotFound
58
+ }
59
+
60
+ return &quizResponse, nil
 
 
 
 
61
  }
62
 
63
+ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
64
+ var quizAttempt models.QuizAttempt
 
 
 
 
 
 
65
 
66
+ err := r.db.Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
 
71
+ return &quizAttempt, nil
 
 
 
 
 
 
 
 
72
  }
73
 
74
+ func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
75
+ return r.db.Model(&models.QuizAttempt{}).Where("id = ?", attempt.ID).Updates(attempt).Error
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
+ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
79
+ questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
80
+
81
+ rawQuery := `
82
+ SELECT
83
+ q.id,
84
+ COALESCE(ua.is_doubt, FALSE) AS is_doubt,
85
+ CASE
86
+ WHEN ua.id IS NOT NULL
87
+ AND EXISTS (
88
+ SELECT 1
89
+ FROM quiz_attempts qa
90
+ WHERE qa.id = ?
91
+ AND qa.account_id = ?
92
+ AND qa.quiz_id = ?
93
+ ) THEN TRUE
94
+ ELSE FALSE
95
+ END AS is_answered
96
+ FROM
97
+ questions q
98
+ LEFT JOIN
99
+ user_answers ua
100
+ ON q.id = ua.question_id
101
+ AND ua.quiz_attempt_id = ?
102
+ WHERE
103
+ q.quiz_id = ?
104
+ ORDER BY
105
+ q.id;
106
+ `
107
+
108
+ err := r.db.Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
109
+ if err != nil {
110
+ return nil, err
111
+ }
112
 
113
+ return questions, nil
 
114
  }
115
 
116
+ func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
117
+ return r.db.Create(attempt).Error
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
120
+ func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
121
+ var totalAttempts int64
 
 
122
 
123
+ err := r.db.Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
124
+ if err != nil {
125
+ return 0, err
126
+ }
127
 
128
+ return totalAttempts, nil
 
 
 
 
 
129
  }
130
 
131
+ func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
132
+ var totalQuestion int64
133
+
134
+ err := r.db.Model(&models.Question{}).Where("quiz_id = ?", quizID).Count(&totalQuestion).Error
135
+ if err != nil {
136
+ return 0, err
137
+ }
138
 
139
+ return totalQuestion, nil
 
 
 
 
 
 
 
140
  }
141
 
142
+ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error) {
143
+ var totalCorrectAnswer int64
144
+
145
+ err := r.db.Model(&models.UserAnswer{}).Where("quiz_attempt_id = ? AND is_correct = TRUE", quizAttemptID).Count(&totalCorrectAnswer).Error
146
+ if err != nil {
147
+ return 0, err
148
+ }
149
+
150
+ return totalCorrectAnswer, nil
151
+ }
152
 
153
+ func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
154
+ return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
155
  }
space/space/space/space/space/router/quiz_route.go CHANGED
@@ -1,22 +1,14 @@
1
  package router
2
 
3
  import (
4
- QuizController "api.qobiltu.id/controller/quiz"
5
  "api.qobiltu.id/middleware"
6
- "github.com/gin-gonic/gin"
7
  )
8
 
9
- func QuizRoute(router *gin.Engine) {
10
- routerGroup := router.Group("/api/v1/quiz")
11
  {
12
- routerGroup.GET("/:academy_id/list", middleware.AuthUser, QuizController.List)
13
- routerGroup.POST("/:academy_id/:quiz_id/attempt", middleware.AuthUser, QuizController.Attempt)
14
- routerGroup.GET("/:academy_id/:quiz_id/question", middleware.AuthUser, QuizController.Question)
15
- routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
16
- routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
17
- routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
18
- routerGroup.GET("result", middleware.AuthUser, QuizController.Result)
19
- routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
20
- routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
21
  }
22
  }
 
1
  package router
2
 
3
  import (
 
4
  "api.qobiltu.id/middleware"
 
5
  )
6
 
7
+ func (s *Server) QuizRoute() {
8
+ userRouterGroup := s.router.Group("/api/v1/quiz")
9
  {
10
+ // :id is academy id
11
+ userRouterGroup.GET("/:id", middleware.AuthUser, s.quizController.UserGetQuiz)
12
+ userRouterGroup.POST("/:id/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
 
 
 
 
 
 
13
  }
14
  }
space/space/space/space/space/router/router.go CHANGED
@@ -10,11 +10,11 @@ func (s *Server) setupRoutes() {
10
 
11
  AuthRoute(s.router)
12
  UserRoute(s.router)
13
- QuizRoute(s.router)
14
 
15
  s.OptionsRoute()
16
  s.EmailRoute()
17
  s.AcademyRoute()
 
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
 
10
 
11
  AuthRoute(s.router)
12
  UserRoute(s.router)
 
13
 
14
  s.OptionsRoute()
15
  s.EmailRoute()
16
  s.AcademyRoute()
17
+ s.QuizRoute()
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
space/space/space/space/space/router/server.go CHANGED
@@ -7,6 +7,7 @@ import (
7
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
8
  options_controller "api.qobiltu.id/controller/options"
9
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
 
10
  region_controller "api.qobiltu.id/controller/region"
11
  "github.com/gin-gonic/gin"
12
  )
@@ -17,6 +18,7 @@ type Server struct {
17
  optionsController options_controller.OptionsController
18
  emailController email_controller.EmailController
19
  academyController academy_controller.AcademyController
 
20
  cvController cv_controller.CVController
21
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
22
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
@@ -27,6 +29,7 @@ func NewServer(
27
  optionsController options_controller.OptionsController,
28
  emailController email_controller.EmailController,
29
  academyController academy_controller.AcademyController,
 
30
  cvController cv_controller.CVController,
31
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
32
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
@@ -40,6 +43,7 @@ func NewServer(
40
  optionsController: optionsController,
41
  emailController: emailController,
42
  academyController: academyController,
 
43
  cvController: cvController,
44
  marriageReadinessProfileController: marriageReadinessProfileController,
45
  partnerCriteriaController: partnerCriteriaController,
 
7
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
8
  options_controller "api.qobiltu.id/controller/options"
9
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
10
+ quiz_controller "api.qobiltu.id/controller/quiz"
11
  region_controller "api.qobiltu.id/controller/region"
12
  "github.com/gin-gonic/gin"
13
  )
 
18
  optionsController options_controller.OptionsController
19
  emailController email_controller.EmailController
20
  academyController academy_controller.AcademyController
21
+ quizController quiz_controller.QuizController
22
  cvController cv_controller.CVController
23
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
24
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
 
29
  optionsController options_controller.OptionsController,
30
  emailController email_controller.EmailController,
31
  academyController academy_controller.AcademyController,
32
+ quizController quiz_controller.QuizController,
33
  cvController cv_controller.CVController,
34
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
35
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
 
43
  optionsController: optionsController,
44
  emailController: emailController,
45
  academyController: academyController,
46
+ quizController: quizController,
47
  cvController: cvController,
48
  marriageReadinessProfileController: marriageReadinessProfileController,
49
  partnerCriteriaController: partnerCriteriaController,
space/space/space/space/space/services/cv_service.go CHANGED
@@ -3,6 +3,7 @@ package services
3
  import (
4
  "context"
5
  "errors"
 
6
  "math"
7
  "strconv"
8
  "strings"
@@ -318,6 +319,7 @@ func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhy
318
 
319
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
320
  if err := s.validator.Validate(req); err != nil {
 
321
  return nil, response.HandleValidationError(err)
322
  }
323
 
@@ -339,16 +341,19 @@ func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, re
339
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.CongregationalPrayer, req.CongregationalPrayer)
340
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.TahajjudPrayer, req.TahajjudPrayer)
341
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
342
- utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
343
- utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
344
- utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudyFrequency, req.WeeklyReligiousStudyFrequency)
345
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
346
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
 
 
 
347
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
 
348
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.ListeningToMusic, req.ListeningToMusic)
349
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnIkhtilat, req.OpinionOnIkhtilat)
350
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram, req.OpinionOnTouchingNonMahram)
351
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnVeil, req.OpinionOnVeil)
 
 
352
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudies, req.WeeklyReligiousStudies)
353
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.FollowedUstadz, req.FollowedUstadz)
354
 
@@ -694,24 +699,27 @@ func calculateProgress(
694
  accountDetailsPercentage := float64(len(accountDetails.GetFilledFields())) / float64(accountDetails.TotalFields()) * 100
695
  personalityAndPreferenceCVPercentage := float64(len(personalityAndPreferenceCV.GetFilledFields())) / float64(personalityAndPreferenceCV.TotalFields()) * 100
696
  physicalAndHealthCVPercentage := float64(len(physicalAndHealthCV.GetFilledFields())) / float64(physicalAndHealthCV.TotalFields()) * 100
697
- worshipAndReligiousUnderstandingCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFields())) / float64(worshipAndReligiousUnderstandingCV.TotalFields()) * 100
 
 
698
 
699
  educationCVPercentage := fullIfPresent(educationCV)
700
  familyMemberCVPercentage := fullIfPresent(familyMemberCV)
701
  jobCVPercentage := fullIfPresent(jobCV)
702
  achievementCVPercentage := fullIfPresent(achievementCV)
703
 
704
- overallProgress := (accountDetailsPercentage + personalityAndPreferenceCVPercentage + physicalAndHealthCVPercentage + worshipAndReligiousUnderstandingCVPercentage + educationCVPercentage + familyMemberCVPercentage + jobCVPercentage + achievementCVPercentage) / 8
705
 
706
  return &models.GetProgressCVResponse{
707
- AccountDetailsProgress: math.Round(accountDetailsPercentage*100) / 100,
708
- PersonalityAndPreferenceCVProgress: math.Round(personalityAndPreferenceCVPercentage*100) / 100,
709
- FamilyMemberCVProgress: math.Round(familyMemberCVPercentage*100) / 100,
710
- PhysicalAndHealthCVProgress: math.Round(physicalAndHealthCVPercentage*100) / 100,
711
- WorshipAndReligiousUnderstandingCVProgress: math.Round(worshipAndReligiousUnderstandingCVPercentage*100) / 100,
712
- EducationCVProgress: math.Round(educationCVPercentage*100) / 100,
713
- JobCVProgress: math.Round(jobCVPercentage*100) / 100,
714
- AchievementCVProgress: math.Round(achievementCVPercentage*100) / 100,
715
- TotalProgress: math.Round(overallProgress*100) / 100,
 
716
  }
717
  }
 
3
  import (
4
  "context"
5
  "errors"
6
+ "fmt"
7
  "math"
8
  "strconv"
9
  "strings"
 
319
 
320
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
321
  if err := s.validator.Validate(req); err != nil {
322
+ fmt.Println(err)
323
  return nil, response.HandleValidationError(err)
324
  }
325
 
 
341
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.CongregationalPrayer, req.CongregationalPrayer)
342
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.TahajjudPrayer, req.TahajjudPrayer)
343
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
 
 
 
344
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
345
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
346
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
347
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
348
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudyFrequency, req.WeeklyReligiousStudyFrequency)
349
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
350
+
351
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.ListeningToMusic, req.ListeningToMusic)
352
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnIkhtilat, req.OpinionOnIkhtilat)
353
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram, req.OpinionOnTouchingNonMahram)
354
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnVeil, req.OpinionOnVeil)
355
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnBeard, req.OpinionOnBeard)
356
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnPantsAboveAnkle, req.OpinionOnPantsAboveAnkle)
357
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudies, req.WeeklyReligiousStudies)
358
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.FollowedUstadz, req.FollowedUstadz)
359
 
 
699
  accountDetailsPercentage := float64(len(accountDetails.GetFilledFields())) / float64(accountDetails.TotalFields()) * 100
700
  personalityAndPreferenceCVPercentage := float64(len(personalityAndPreferenceCV.GetFilledFields())) / float64(personalityAndPreferenceCV.TotalFields()) * 100
701
  physicalAndHealthCVPercentage := float64(len(physicalAndHealthCV.GetFilledFields())) / float64(physicalAndHealthCV.TotalFields()) * 100
702
+
703
+ worshipCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFieldsWorship())) / float64(worshipAndReligiousUnderstandingCV.GetTotalFieldsWorship()) * 100
704
+ religiousUnderstandingCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFieldsReligiousUnderstanding())) / float64(worshipAndReligiousUnderstandingCV.GetTotalFieldsReligiousUnderstanding()) * 100
705
 
706
  educationCVPercentage := fullIfPresent(educationCV)
707
  familyMemberCVPercentage := fullIfPresent(familyMemberCV)
708
  jobCVPercentage := fullIfPresent(jobCV)
709
  achievementCVPercentage := fullIfPresent(achievementCV)
710
 
711
+ overallProgress := (accountDetailsPercentage + personalityAndPreferenceCVPercentage + physicalAndHealthCVPercentage + worshipCVPercentage + religiousUnderstandingCVPercentage + educationCVPercentage + familyMemberCVPercentage + jobCVPercentage + achievementCVPercentage) / 9
712
 
713
  return &models.GetProgressCVResponse{
714
+ AccountDetailsProgress: math.Round(accountDetailsPercentage*100) / 100,
715
+ PersonalityAndPreferenceCVProgress: math.Round(personalityAndPreferenceCVPercentage*100) / 100,
716
+ FamilyMemberCVProgress: math.Round(familyMemberCVPercentage*100) / 100,
717
+ PhysicalAndHealthCVProgress: math.Round(physicalAndHealthCVPercentage*100) / 100,
718
+ WorshipCVProgress: math.Round(worshipCVPercentage*100) / 100,
719
+ ReligiousUnderstandingCVProgress: math.Round(religiousUnderstandingCVPercentage*100) / 100,
720
+ EducationCVProgress: math.Round(educationCVPercentage*100) / 100,
721
+ JobCVProgress: math.Round(jobCVPercentage*100) / 100,
722
+ AchievementCVProgress: math.Round(achievementCVPercentage*100) / 100,
723
+ TotalProgress: math.Round(overallProgress*100) / 100,
724
  }
725
  }
space/space/space/space/space/services/quiz_service.go CHANGED
@@ -1,37 +1,200 @@
1
  package services
2
 
3
- // type QuizService interface {
4
- // // === ADMIN ===
5
-
6
- // // === USER ===
7
- // UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
8
- // UserAttemptQuiz(ctx context.Context, req *models.AttemptQuizRequest) (*models.AttemptQuizResponse, error)
9
- // UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
10
- // // UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
11
- // // UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
12
- // // UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
13
- // // UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
14
- // // UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
15
- // }
16
-
17
- // type quizService struct {
18
- // quizRepository repositories.QuizRepository
19
- // validator *validation.Validator
20
- // }
21
-
22
- // func NewQuizService(quizRepository repositories.QuizRepository, validator *validation.Validator) QuizService {
23
- // return &quizService{quizRepository: quizRepository, validator: validator}
24
- // }
25
-
26
- // func (s *quizService) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
27
- // quiz, paging, err := s.quizRepository.UserListQuiz(ctx, req)
28
- // if err != nil {
29
- // return nil, nil, response.HandleGormError(err, "Internal Server Error")
30
- // }
31
-
32
- // return quiz, paging, nil
33
- // }
34
-
35
- // func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.AttemptQuizRequest) (*models.AttemptQuizResponse, error) {
36
- // panic("not implemented")
37
- // }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  package services
2
 
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "math/rand"
7
+ "time"
8
+
9
+ "api.qobiltu.id/models"
10
+ "api.qobiltu.id/pkg/validation"
11
+ "api.qobiltu.id/repositories"
12
+ "api.qobiltu.id/response"
13
+ "gorm.io/gorm"
14
+ )
15
+
16
+ type QuizService interface {
17
+ // === ADMIN ===
18
+
19
+ // // === USER ===
20
+ UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
21
+ UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
22
+
23
+ // UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
24
+ // UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
25
+ // UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
26
+ // UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
27
+ // UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
28
+ // UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
29
+ }
30
+
31
+ type quizService struct {
32
+ quizRepository repositories.QuizRepository
33
+ academyRepository repositories.AcademyRepository
34
+ validator *validation.Validator
35
+ }
36
+
37
+ func NewQuizService(quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
38
+ return &quizService{quizRepository: quizRepository, academyRepository: academyRepository, validator: validator}
39
+ }
40
+
41
+ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
42
+ quiz, err := s.quizRepository.UserGetQuiz(ctx, req)
43
+ if err != nil {
44
+ return nil, response.HandleGormError(err, "Internal Server Error")
45
+ }
46
+
47
+ return quiz, nil
48
+ }
49
+
50
+ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
51
+ quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
52
+ AccountID: req.AccountID,
53
+ AcademyID: req.AcademyID,
54
+ })
55
+ if err != nil {
56
+ return nil, response.HandleGormError(err, "Internal Server Error")
57
+ }
58
+
59
+ quiz := &quizAttempt.Quiz
60
+
61
+ existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
62
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
63
+ return nil, response.HandleGormError(err, "Internal Server Error")
64
+ }
65
+
66
+ if existingAttempt != nil {
67
+ return s.handleExistingAttempt(ctx, req, quiz, existingAttempt)
68
+ }
69
+
70
+ return s.handleNewAttempt(ctx, req, quiz)
71
+ }
72
+
73
+ func (s *quizService) handleExistingAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
74
+ now := time.Now()
75
+
76
+ if attempt.DueAt.Before(now) {
77
+ if attempt.FinishedAt == nil {
78
+ attempt.FinishedAt = &now
79
+ attempt.Score = s.calculateQuizScore(ctx, attempt)
80
+
81
+ if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
82
+ return nil, response.HandleGormError(err, "Internal Server Error")
83
+ }
84
+
85
+ totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
86
+ if err != nil {
87
+ return nil, response.HandleGormError(err, "Internal Server Error")
88
+ }
89
+
90
+ if totalAttempts >= quiz.AttemptLimit {
91
+ if err := s.academyRepository.UserResetAcademyProgressByID(ctx, req.AccountID, quiz.AcademyID); err != nil {
92
+ return nil, response.HandleGormError(err, "Internal Server Error")
93
+ }
94
+
95
+ // hapus juga semua attempt quiz by account id dan quiz id
96
+ if err := s.quizRepository.UserDeleteAttemptQuizByAccountIDAndQuizID(ctx, req.AccountID, quiz.ID); err != nil {
97
+ return nil, response.HandleGormError(err, "Internal Server Error")
98
+ }
99
+ }
100
+
101
+ return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
102
+ }
103
+
104
+ return nil, models.Exception{QuizAlreadyFinished: true, Message: "Quiz already finished"}
105
+ }
106
+
107
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
108
+ if err != nil {
109
+ return nil, response.HandleGormError(err, "Internal Server Error")
110
+ }
111
+
112
+ questions = shuffleWithKey(questions, attempt.ID)
113
+
114
+ return &models.UserAttemptQuizResponse{
115
+ QuizAttempt: *attempt,
116
+ Questions: questions,
117
+ }, nil
118
+ }
119
+
120
+ func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
121
+ totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
122
+ if err != nil {
123
+ return nil, response.HandleGormError(err, "Internal Server Error")
124
+ }
125
+ if totalAttempts >= quiz.AttemptLimit {
126
+ return nil, models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
127
+ }
128
+
129
+ percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, req.AccountID, quiz.AcademyID)
130
+ if err != nil {
131
+ return nil, response.HandleGormError(err, "Internal Server Error")
132
+ }
133
+ if percentage < 100 {
134
+ return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
135
+ }
136
+
137
+ quizAttempt := models.QuizAttempt{
138
+ AccountID: req.AccountID,
139
+ QuizID: quiz.ID,
140
+ StartedAt: time.Now(),
141
+ DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
142
+ }
143
+
144
+ if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &quizAttempt); err != nil {
145
+ return nil, response.HandleGormError(err, "Internal Server Error")
146
+ }
147
+
148
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, quizAttempt.ID)
149
+ if err != nil {
150
+ return nil, response.HandleGormError(err, "Internal Server Error")
151
+ }
152
+
153
+ questions = shuffleWithKey(questions, quizAttempt.ID)
154
+
155
+ return &models.UserAttemptQuizResponse{
156
+ QuizAttempt: quizAttempt,
157
+ Questions: questions,
158
+ }, nil
159
+ }
160
+
161
+ func (s *quizService) calculateQuizScore(ctx context.Context, attempt *models.QuizAttempt) float64 {
162
+ // ambil total question dari quiz
163
+ totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
164
+ if err != nil {
165
+ return 0
166
+ }
167
+
168
+ if totalQuestion == 0 {
169
+ return 0
170
+ }
171
+
172
+ // ambil semua user answer yang is_correct nya true
173
+ correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
174
+ if err != nil {
175
+ return 0
176
+ }
177
+
178
+ // hitung score nya
179
+ score := float64(correctAnswer) / float64(totalQuestion) * 100
180
+
181
+ return score
182
+ }
183
+
184
+ // shuffleWithKey mengacak slice dengan menggunakan integer key sebagai seed
185
+ // untuk memastikan hasil acak yang konsisten untuk key yang sama
186
+ func shuffleWithKey[T any](slice []T, key int64) []T {
187
+ // Buat salinan slice untuk menghindari modifikasi original
188
+ shuffled := make([]T, len(slice))
189
+ copy(shuffled, slice)
190
+
191
+ // Buat random source dengan seed dari key
192
+ r := rand.New(rand.NewSource(key))
193
+
194
+ // Lakukan shuffling
195
+ r.Shuffle(len(shuffled), func(i, j int) {
196
+ shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
197
+ })
198
+
199
+ return shuffled
200
+ }
space/space/space/space/space/space/space/controller/academy/academy_controller.go CHANGED
@@ -349,15 +349,21 @@ func (c *academyController) UserGetAcademyMaterialBySlug(ctx *gin.Context) {
349
  }
350
 
351
  func (c *academyController) UserToggleAcademyMaterialProgress(ctx *gin.Context) {
352
- slug := ctx.Param("materialSlug")
 
 
 
 
 
 
 
353
 
354
  accountData := middleware.GetAccountData(ctx)
355
  accountID := int64(accountData.UserID)
356
 
357
- req := models.ToggleAcademyMaterialProgressRequest{
358
- AccountID: accountID,
359
- Slug: slug,
360
- }
361
 
362
  err := c.academyService.UserToggleAcademyMaterialProgress(ctx, &req)
363
  if err != nil {
 
349
  }
350
 
351
  func (c *academyController) UserToggleAcademyMaterialProgress(ctx *gin.Context) {
352
+ var req models.ToggleAcademyMaterialProgressRequest
353
+ if err := ctx.ShouldBindJSON(&req); err != nil {
354
+ response.HandleError(ctx, err)
355
+ return
356
+ }
357
+
358
+ academySlug := ctx.Param("slug")
359
+ materialSlug := ctx.Param("materialSlug")
360
 
361
  accountData := middleware.GetAccountData(ctx)
362
  accountID := int64(accountData.UserID)
363
 
364
+ req.AccountID = accountID
365
+ req.AcademySlug = academySlug
366
+ req.MaterialSlug = materialSlug
 
367
 
368
  err := c.academyService.UserToggleAcademyMaterialProgress(ctx, &req)
369
  if err != nil {
space/space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -80,6 +80,7 @@ type (
80
  Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
81
  Description string `gorm:"column:description" json:"description"`
82
  Order uint `gorm:"column:order" json:"order"`
 
83
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
84
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
85
  }
@@ -101,7 +102,8 @@ type (
101
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
102
  AcademyMaterialID int64 `gorm:"primaryKey;column:academy_material_id" json:"academy_material_id"`
103
  AcademyMaterial *AcademyMaterial `gorm:"foreignKey:AcademyMaterialID;constraint:OnDelete:CASCADE" json:"academy_material,omitempty"`
104
- ReadAt *time.Time `gorm:"column:read_at" json:"read_at"`
 
105
  }
106
  )
107
 
 
80
  Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
81
  Description string `gorm:"column:description" json:"description"`
82
  Order uint `gorm:"column:order" json:"order"`
83
+ Image *string `gorm:"column:image" json:"image"`
84
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
85
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
86
  }
 
102
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
103
  AcademyMaterialID int64 `gorm:"primaryKey;column:academy_material_id" json:"academy_material_id"`
104
  AcademyMaterial *AcademyMaterial `gorm:"foreignKey:AcademyMaterialID;constraint:OnDelete:CASCADE" json:"academy_material,omitempty"`
105
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
106
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
107
  }
108
  )
109
 
space/space/space/space/space/space/space/models/request_model.go CHANGED
@@ -56,8 +56,26 @@ type (
56
  }
57
 
58
  UserAcademyMaterialResponse struct {
59
- AcademyMaterial
60
- IsRead bool `gorm:"is_read" json:"is_read"`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
  CreateAcademyMaterialRequest struct {
@@ -108,8 +126,10 @@ type (
108
  }
109
 
110
  ToggleAcademyMaterialProgressRequest struct {
111
- AccountID int64 `json:"account_id"`
112
- Slug string `json:"slug"`
 
 
113
  }
114
  )
115
 
@@ -131,6 +151,38 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
131
  }
132
  }
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  type (
135
  MultipleOptionsRequest struct {
136
  OptionName string `json:"option_name" validate:"required"`
 
56
  }
57
 
58
  UserAcademyMaterialResponse struct {
59
+ ID int64 `gorm:"column:id" json:"id"`
60
+ AcademyID int64 `gorm:"column:academy_id" json:"academy_id"`
61
+ Title string `gorm:"column:title" json:"title"`
62
+ Slug string `gorm:"column:slug" json:"slug"`
63
+ Order uint `gorm:"column:order" json:"order"`
64
+ CreatedAt *time.Time `gorm:"column:read_created_at" json:"created_at"`
65
+ UpdatedAt *time.Time `gorm:"column:read_updated_at" json:"updated_at"`
66
+ IsRead bool `gorm:"is_read" json:"is_read"`
67
+ }
68
+
69
+ UserAcademyMaterialDetailResponse struct {
70
+ ID int64 `gorm:"column:id" json:"id"`
71
+ AcademyID int64 `gorm:"column:academy_id" json:"academy_id"`
72
+ Title string `gorm:"column:title" json:"title"`
73
+ Slug string `gorm:"column:slug" json:"slug"`
74
+ Content string `gorm:"column:content" json:"content"`
75
+ Order uint `gorm:"column:order" json:"order"`
76
+ CreatedAt *time.Time `gorm:"column:read_created_at" json:"created_at"`
77
+ UpdatedAt *time.Time `gorm:"column:read_updated_at" json:"updated_at"`
78
+ IsRead bool `gorm:"is_read" json:"is_read"`
79
  }
80
 
81
  CreateAcademyMaterialRequest struct {
 
126
  }
127
 
128
  ToggleAcademyMaterialProgressRequest struct {
129
+ AccountID int64 `json:"-"`
130
+ MaterialSlug string `json:"-"`
131
+ AcademySlug string `json:"-"`
132
+ IsRead bool `json:"is_read"`
133
  }
134
  )
135
 
 
151
  }
152
  }
153
 
154
+ type (
155
+ ListQuizRequest struct {
156
+ AccountID int64 `json:"account_id"`
157
+ AcademyID int64 `json:"academy_id" validate:"required"`
158
+ }
159
+
160
+ ListQuizResponse struct {
161
+ Quiz []Quiz `json:"quiz"`
162
+ }
163
+
164
+ AttemptQuizRequest struct {
165
+ AccountID int64 `json:"account_id"`
166
+ QuizID int64 `json:"quiz_id" validate:"required"`
167
+ }
168
+
169
+ AttemptQuizResponse struct {
170
+ QuizAttempt QuizAttempt `json:"quiz_attempt"`
171
+ }
172
+
173
+ GetQuestionQuizRequest struct {
174
+ AccountID int64 `json:"account_id"`
175
+ QuizID int64 `json:"quiz_id" validate:"required"`
176
+ }
177
+
178
+ GetQuestionQuizResponse struct {
179
+ Question Question `json:"question"`
180
+ Answer []Answer `json:"answer_options"`
181
+ UserAnswer int `json:"current_user_answer"`
182
+ IsDoubt bool `json:"is_doubt"`
183
+ }
184
+ )
185
+
186
  type (
187
  MultipleOptionsRequest struct {
188
  OptionName string `json:"option_name" validate:"required"`
space/space/space/space/space/space/space/repositories/academy_repository.go CHANGED
@@ -28,7 +28,7 @@ type AcademyRepository interface {
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
30
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
31
- UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialResponse, error)
32
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
33
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
34
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
@@ -245,7 +245,7 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
245
  Model(&models.Academy{}).
246
  Select(`academy.*,
247
  COUNT(DISTINCT am.id) AS total_material,
248
- COUNT(DISTINCT CASE WHEN amp.read_at IS NOT NULL THEN amp.academy_material_id END) AS total_read_material`).
249
  Joins("LEFT JOIN academy_materials am ON am.academy_id = academy.id").
250
  Joins(`LEFT JOIN "academy_materials_progress" amp ON amp.academy_material_id = am.id AND amp.account_id = ?`, req.AccountID).
251
  Group("academy.id")
@@ -294,14 +294,16 @@ func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *mo
294
  q := r.db.WithContext(ctx).
295
  Model(&models.AcademyMaterial{}).
296
  Select(`academy_materials.*,
297
- CASE
298
- WHEN amp.read_at IS NOT NULL THEN true
299
- ELSE false
300
- END AS is_read`).
 
 
301
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
302
  Joins(`LEFT JOIN academy_materials_progress amp ON
303
- amp.academy_material_id = academy_materials.id AND
304
- amp.account_id = ?`, req.AccountID).
305
  Where("academy.slug = ?", req.Slug)
306
 
307
  if req.Filter.HasKeyword() {
@@ -359,8 +361,8 @@ func (r *academyRepository) UserGetAcademyBySlug(ctx context.Context, slug strin
359
  return &academy, nil
360
  }
361
 
362
- func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialResponse, error) {
363
- var result models.UserAcademyMaterialResponse
364
 
365
  err := r.db.WithContext(ctx).
366
  Model(&models.AcademyMaterial{}).
@@ -369,22 +371,28 @@ func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, sl
369
  academy.title as academy_title,
370
  academy.slug as academy_slug,
371
  CASE
372
- WHEN amp.read_at IS NOT NULL THEN true
373
  ELSE false
374
- END AS is_read
 
 
375
  `).
376
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
377
  Joins(`LEFT JOIN academy_materials_progress amp ON
378
  amp.academy_material_id = academy_materials.id AND
379
  amp.account_id = ?`, accountID).
380
  Where("academy_materials.slug = ?", slug).
381
- First(&result).
382
  Error
383
 
384
  if err != nil {
385
  return nil, err
386
  }
387
 
 
 
 
 
388
  return &result, nil
389
  }
390
 
 
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
30
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
31
+ UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
32
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
33
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
34
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
 
245
  Model(&models.Academy{}).
246
  Select(`academy.*,
247
  COUNT(DISTINCT am.id) AS total_material,
248
+ COUNT(DISTINCT CASE WHEN amp.created_at IS NOT NULL THEN amp.academy_material_id END) AS total_read_material`).
249
  Joins("LEFT JOIN academy_materials am ON am.academy_id = academy.id").
250
  Joins(`LEFT JOIN "academy_materials_progress" amp ON amp.academy_material_id = am.id AND amp.account_id = ?`, req.AccountID).
251
  Group("academy.id")
 
294
  q := r.db.WithContext(ctx).
295
  Model(&models.AcademyMaterial{}).
296
  Select(`academy_materials.*,
297
+ amp.created_at AS read_created_at,
298
+ amp.updated_at AS read_updated_at,
299
+ CASE
300
+ WHEN amp.created_at IS NOT NULL THEN true
301
+ ELSE false
302
+ END AS is_read`).
303
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
304
  Joins(`LEFT JOIN academy_materials_progress amp ON
305
+ amp.academy_material_id = academy_materials.id AND
306
+ amp.account_id = ?`, req.AccountID).
307
  Where("academy.slug = ?", req.Slug)
308
 
309
  if req.Filter.HasKeyword() {
 
361
  return &academy, nil
362
  }
363
 
364
+ func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error) {
365
+ var result models.UserAcademyMaterialDetailResponse
366
 
367
  err := r.db.WithContext(ctx).
368
  Model(&models.AcademyMaterial{}).
 
371
  academy.title as academy_title,
372
  academy.slug as academy_slug,
373
  CASE
374
+ WHEN amp.created_at IS NOT NULL THEN true
375
  ELSE false
376
+ END AS is_read,
377
+ amp.created_at AS read_created_at,
378
+ amp.updated_at AS read_updated_at
379
  `).
380
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
381
  Joins(`LEFT JOIN academy_materials_progress amp ON
382
  amp.academy_material_id = academy_materials.id AND
383
  amp.account_id = ?`, accountID).
384
  Where("academy_materials.slug = ?", slug).
385
+ Scan(&result).
386
  Error
387
 
388
  if err != nil {
389
  return nil, err
390
  }
391
 
392
+ if result.ID == 0 {
393
+ return nil, gorm.ErrRecordNotFound
394
+ }
395
+
396
  return &result, nil
397
  }
398
 
space/space/space/space/space/space/space/repositories/quiz_repository.go CHANGED
@@ -1,9 +1,28 @@
1
  package repositories
2
 
3
  import (
 
 
4
  "api.qobiltu.id/models"
 
5
  )
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
8
  repo := Construct[models.Quiz, []models.Quiz](
9
  models.Quiz{AcademyID: academyId},
 
1
  package repositories
2
 
3
  import (
4
+ "context"
5
+
6
  "api.qobiltu.id/models"
7
+ "gorm.io/gorm"
8
  )
9
 
10
+ type QuizRepository interface {
11
+ UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
12
+ }
13
+
14
+ type quizRepository struct {
15
+ db *gorm.DB
16
+ }
17
+
18
+ func NewQuizRepository(db *gorm.DB) QuizRepository {
19
+ return &quizRepository{db: db}
20
+ }
21
+
22
+ func (r *quizRepository) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
23
+ panic("not implemented")
24
+ }
25
+
26
  func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
27
  repo := Construct[models.Quiz, []models.Quiz](
28
  models.Quiz{AcademyID: academyId},
space/space/space/space/space/space/space/router/quiz_route.go CHANGED
@@ -15,7 +15,7 @@ func QuizRoute(router *gin.Engine) {
15
  routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
16
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
17
  routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
18
- routerGroup.GET("result/", middleware.AuthUser, QuizController.Result)
19
  routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
20
  routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
21
  }
 
15
  routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
16
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
17
  routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
18
+ routerGroup.GET("result", middleware.AuthUser, QuizController.Result)
19
  routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
20
  routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
21
  }
space/space/space/space/space/space/space/services/academy_service.go CHANGED
@@ -33,7 +33,7 @@ type AcademyService interface {
33
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
34
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
35
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
36
- UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialResponse, error)
37
  UserToggleAcademyMaterialProgress(ctx context.Context, req *models.ToggleAcademyMaterialProgressRequest) error
38
  }
39
 
@@ -277,7 +277,7 @@ func (s *academyService) UserGetAcademyBySlug(ctx context.Context, slug string)
277
  return academy, nil
278
  }
279
 
280
- func (s *academyService) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialResponse, error) {
281
  academyMaterial, err := s.academyRepository.UserGetAcademyMaterialBySlug(ctx, slug, accountID)
282
  if err != nil {
283
  return nil, response.HandleGormError(err, "Internal Server Error")
@@ -287,29 +287,29 @@ func (s *academyService) UserGetAcademyMaterialBySlug(ctx context.Context, slug
287
  }
288
 
289
  func (s *academyService) UserToggleAcademyMaterialProgress(ctx context.Context, req *models.ToggleAcademyMaterialProgressRequest) error {
290
-
291
- academyMaterial, err := s.academyRepository.UserGetAcademyMaterialBySlug(ctx, req.Slug, req.AccountID)
292
  if err != nil {
293
  return response.HandleGormError(err, "Internal Server Error")
294
  }
295
 
296
- if academyMaterial.IsRead {
297
- err = s.academyRepository.UserDeleteAcademyMaterialProgress(ctx, req.AccountID, academyMaterial.ID)
 
298
  if err != nil {
299
  return response.HandleGormError(err, "Internal Server Error")
300
  }
301
- } else {
302
- now := time.Now()
303
- academyMaterialProgress := &models.AcademyMaterialProgress{
304
- AccountID: req.AccountID,
305
- AcademyMaterialID: academyMaterial.ID,
306
- ReadAt: &now,
307
- }
308
 
309
- err = s.academyRepository.UserSaveAcademyMaterialProgress(ctx, academyMaterialProgress)
310
- if err != nil {
311
- return response.HandleGormError(err, "Internal Server Error")
312
- }
 
 
 
 
 
313
  }
314
 
315
  return nil
 
33
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
34
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
35
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
36
+ UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
37
  UserToggleAcademyMaterialProgress(ctx context.Context, req *models.ToggleAcademyMaterialProgressRequest) error
38
  }
39
 
 
277
  return academy, nil
278
  }
279
 
280
+ func (s *academyService) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error) {
281
  academyMaterial, err := s.academyRepository.UserGetAcademyMaterialBySlug(ctx, slug, accountID)
282
  if err != nil {
283
  return nil, response.HandleGormError(err, "Internal Server Error")
 
287
  }
288
 
289
  func (s *academyService) UserToggleAcademyMaterialProgress(ctx context.Context, req *models.ToggleAcademyMaterialProgressRequest) error {
290
+ material, err := s.academyRepository.UserGetAcademyMaterialBySlug(ctx, req.MaterialSlug, req.AccountID)
 
291
  if err != nil {
292
  return response.HandleGormError(err, "Internal Server Error")
293
  }
294
 
295
+ // langsung hapus progress jika isRead adalah false
296
+ if !req.IsRead {
297
+ err := s.academyRepository.UserDeleteAcademyMaterialProgress(ctx, req.AccountID, material.ID)
298
  if err != nil {
299
  return response.HandleGormError(err, "Internal Server Error")
300
  }
301
+ return nil
302
+ }
 
 
 
 
 
303
 
304
+ academyMaterialProgress := models.AcademyMaterialProgress{
305
+ AccountID: req.AccountID,
306
+ AcademyMaterialID: material.ID,
307
+ CreatedAt: time.Now(),
308
+ }
309
+
310
+ err = s.academyRepository.UserSaveAcademyMaterialProgress(ctx, &academyMaterialProgress)
311
+ if err != nil {
312
+ return response.HandleGormError(err, "Internal Server Error")
313
  }
314
 
315
  return nil
space/space/space/space/space/space/space/services/quiz_service.go ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ // type QuizService interface {
4
+ // // === ADMIN ===
5
+
6
+ // // === USER ===
7
+ // UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
8
+ // UserAttemptQuiz(ctx context.Context, req *models.AttemptQuizRequest) (*models.AttemptQuizResponse, error)
9
+ // UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
10
+ // // UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
11
+ // // UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
12
+ // // UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
13
+ // // UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
14
+ // // UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
15
+ // }
16
+
17
+ // type quizService struct {
18
+ // quizRepository repositories.QuizRepository
19
+ // validator *validation.Validator
20
+ // }
21
+
22
+ // func NewQuizService(quizRepository repositories.QuizRepository, validator *validation.Validator) QuizService {
23
+ // return &quizService{quizRepository: quizRepository, validator: validator}
24
+ // }
25
+
26
+ // func (s *quizService) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
27
+ // quiz, paging, err := s.quizRepository.UserListQuiz(ctx, req)
28
+ // if err != nil {
29
+ // return nil, nil, response.HandleGormError(err, "Internal Server Error")
30
+ // }
31
+
32
+ // return quiz, paging, nil
33
+ // }
34
+
35
+ // func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.AttemptQuizRequest) (*models.AttemptQuizResponse, error) {
36
+ // panic("not implemented")
37
+ // }
space/space/space/space/space/space/space/space/models/field_counter.go CHANGED
@@ -1,7 +1,6 @@
1
  package models
2
 
3
  import (
4
- "fmt"
5
  "reflect"
6
  "time"
7
  )
@@ -23,7 +22,6 @@ func shouldSkipField(field reflect.StructField) bool {
23
  }
24
 
25
  counterTag := field.Tag.Get("counter")
26
- fmt.Println(field.Name, "=", counterTag)
27
  return counterTag == "skip"
28
  }
29
 
 
1
  package models
2
 
3
  import (
 
4
  "reflect"
5
  "time"
6
  )
 
22
  }
23
 
24
  counterTag := field.Tag.Get("counter")
 
25
  return counterTag == "skip"
26
  }
27
 
space/space/space/space/space/space/space/space/services/partner_criteria_service.go CHANGED
@@ -59,9 +59,9 @@ func (s *partnerCriteriaService) SavePartnerCriteria(ctx context.Context, req *m
59
  utils.AssignIfNotNil(&partnerCriteria.ExpectedMaxHeightLimit, req.ExpectedMaxHeightLimit)
60
 
61
  utils.AssignIfNotNil(&partnerCriteria.ExpectedPartnerIncome, req.ExpectedPartnerIncome)
62
- utils.AssignIfNotNil(&partnerCriteria.ExpectedIncomeSources, req.ExpectedIncomeSources)
63
  utils.AssignIfNotNil(&partnerCriteria.ExpectedLastEducation, req.ExpectedLastEducation)
64
- utils.AssignIfNotNil(&partnerCriteria.ExpectedJobType, req.ExpectedJobType)
 
65
 
66
  res, err := s.partnerCriteriaRepository.SavePartnerCriteria(ctx, partnerCriteria)
67
  if err != nil {
 
59
  utils.AssignIfNotNil(&partnerCriteria.ExpectedMaxHeightLimit, req.ExpectedMaxHeightLimit)
60
 
61
  utils.AssignIfNotNil(&partnerCriteria.ExpectedPartnerIncome, req.ExpectedPartnerIncome)
 
62
  utils.AssignIfNotNil(&partnerCriteria.ExpectedLastEducation, req.ExpectedLastEducation)
63
+ // utils.AssignIfNotNil(&partnerCriteria.ExpectedIncomeSources, req.ExpectedIncomeSources)
64
+ // utils.AssignIfNotNil(&partnerCriteria.ExpectedJobType, req.ExpectedJobType)
65
 
66
  res, err := s.partnerCriteriaRepository.SavePartnerCriteria(ctx, partnerCriteria)
67
  if err != nil {
space/space/space/space/space/space/space/space/space/config/database_connection_config.go CHANGED
@@ -57,9 +57,7 @@ func AutoMigrateAll(db *gorm.DB) {
57
  &models.ForgotPassword{},
58
  &models.Academy{},
59
  &models.AcademyMaterial{},
60
- &models.AcademyContent{},
61
  &models.AcademyMaterialProgress{},
62
- &models.AcademyContentProgress{},
63
  &models.RegionCity{},
64
  &models.RegionProvince{},
65
  &models.OptionCategory{},
 
57
  &models.ForgotPassword{},
58
  &models.Academy{},
59
  &models.AcademyMaterial{},
 
60
  &models.AcademyMaterialProgress{},
 
61
  &models.RegionCity{},
62
  &models.RegionProvince{},
63
  &models.OptionCategory{},
space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go CHANGED
@@ -1,31 +1,369 @@
1
- package academy
2
-
3
- import (
4
- "api.qobiltu.id/controller"
5
- "api.qobiltu.id/models"
6
- "api.qobiltu.id/services"
7
- "github.com/gin-gonic/gin"
8
- )
9
-
10
- func List(c *gin.Context) {
11
- academy := services.AcademyService{}
12
- academyController := controller.Controller[any, models.Academy, []models.Academy]{
13
- Service: &academy.Service,
14
- }
15
- academyController.Service.Constructor.Slug = c.Param("slug")
16
- academy.Retrieve()
17
- academyController.Response(c)
18
-
19
- }
20
-
21
- func Create(c *gin.Context) {
22
- createAcademy := services.CreateAcademyService{}
23
- academyController := controller.Controller[models.AllAcademyResponse, models.AllAcademyResponse, models.AllAcademyResponse]{
24
- Service: &createAcademy.Service,
25
- }
26
- academyController.RequestJSON(c, func() {
27
- academyController.Service.Constructor.Academies = academyController.Request.Academies
28
- createAcademy.Create()
29
- })
30
-
31
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package academy_controller
2
+
3
+ import (
4
+ "net/http"
5
+ "strconv"
6
+
7
+ "api.qobiltu.id/middleware"
8
+ "api.qobiltu.id/models"
9
+ "api.qobiltu.id/response"
10
+ "api.qobiltu.id/services"
11
+ "github.com/gin-gonic/gin"
12
+ )
13
+
14
+ type AcademyController interface {
15
+ // === ADMIN ===
16
+ AdminCreateAcademy(ctx *gin.Context)
17
+ AdminGetAcademy(ctx *gin.Context)
18
+ AdminUpdateAcademy(ctx *gin.Context)
19
+ AdminDeleteAcademy(ctx *gin.Context)
20
+ AdminListAcademy(ctx *gin.Context)
21
+ AdminReorderAcademy(ctx *gin.Context)
22
+
23
+ AdminCreateAcademyMaterial(ctx *gin.Context)
24
+ AdminGetAcademyMaterial(ctx *gin.Context)
25
+ AdminUpdateAcademyMaterial(ctx *gin.Context)
26
+ AdminDeleteAcademyMaterial(ctx *gin.Context)
27
+ AdminListAcademyMaterial(ctx *gin.Context)
28
+ AdminReorderAcademyMaterial(ctx *gin.Context)
29
+
30
+ // === USER ===
31
+ UserListAcademy(ctx *gin.Context)
32
+ UserListAcademyMaterial(ctx *gin.Context)
33
+ UserGetAcademyMaterialBySlug(ctx *gin.Context)
34
+ UserToggleAcademyMaterialProgress(ctx *gin.Context)
35
+ }
36
+
37
+ type academyController struct {
38
+ academyService services.AcademyService
39
+ }
40
+
41
+ func NewAcademyController(academyService services.AcademyService) AcademyController {
42
+ return &academyController{
43
+ academyService: academyService,
44
+ }
45
+ }
46
+
47
+ func (c *academyController) AdminListAcademy(ctx *gin.Context) {
48
+ req := models.NewListAcademyRequest()
49
+ if err := ctx.ShouldBindQuery(&req); err != nil {
50
+ response.HandleError(ctx, err)
51
+ return
52
+ }
53
+
54
+ res, paging, err := c.academyService.AdminListAcademy(ctx, &req)
55
+ if err != nil {
56
+ response.HandleError(ctx, err)
57
+ return
58
+ }
59
+
60
+ response.HandleSuccess(ctx, http.StatusOK, "Academy list retrieved successfully", res, paging)
61
+ }
62
+
63
+ func (c *academyController) AdminCreateAcademy(ctx *gin.Context) {
64
+ var req models.CreateAcademyRequest
65
+ if err := ctx.ShouldBindJSON(&req); err != nil {
66
+ response.HandleError(ctx, err)
67
+ return
68
+ }
69
+
70
+ accountData := middleware.GetAccountData(ctx)
71
+ req.AccountID = int64(accountData.UserID)
72
+
73
+ res, err := c.academyService.AdminCreateAcademy(ctx, &req)
74
+ if err != nil {
75
+ response.HandleError(ctx, err)
76
+ return
77
+ }
78
+
79
+ response.HandleSuccess(ctx, http.StatusOK, "Academy created successfully", res, nil)
80
+ }
81
+
82
+ func (c *academyController) AdminGetAcademy(ctx *gin.Context) {
83
+ id := ctx.Param("id")
84
+ idInt, err := strconv.ParseInt(id, 10, 64)
85
+ if err != nil {
86
+ response.HandleError(ctx, err)
87
+ return
88
+ }
89
+
90
+ res, err := c.academyService.AdminGetAcademyByID(ctx, idInt)
91
+ if err != nil {
92
+ response.HandleError(ctx, err)
93
+ return
94
+ }
95
+
96
+ response.HandleSuccess(ctx, http.StatusOK, "Academy retrieved successfully", res, nil)
97
+ }
98
+
99
+ func (c *academyController) AdminUpdateAcademy(ctx *gin.Context) {
100
+ id := ctx.Param("id")
101
+ idInt, err := strconv.ParseInt(id, 10, 64)
102
+ if err != nil {
103
+ response.HandleError(ctx, err)
104
+ return
105
+ }
106
+
107
+ var req models.UpdateAcademyRequest
108
+ if err := ctx.ShouldBindJSON(&req); err != nil {
109
+ response.HandleError(ctx, err)
110
+ return
111
+ }
112
+
113
+ req.ID = idInt
114
+
115
+ res, err := c.academyService.AdminUpdateAcademy(ctx, &req)
116
+ if err != nil {
117
+ response.HandleError(ctx, err)
118
+ return
119
+ }
120
+
121
+ response.HandleSuccess(ctx, http.StatusOK, "Academy updated successfully", res, nil)
122
+ }
123
+
124
+ func (c *academyController) AdminDeleteAcademy(ctx *gin.Context) {
125
+ id := ctx.Param("id")
126
+ idInt, err := strconv.ParseInt(id, 10, 64)
127
+ if err != nil {
128
+ response.HandleError(ctx, err)
129
+ return
130
+ }
131
+
132
+ err = c.academyService.AdminDeleteAcademy(ctx, idInt)
133
+ if err != nil {
134
+ response.HandleError(ctx, err)
135
+ return
136
+ }
137
+
138
+ response.HandleSuccess(ctx, http.StatusOK, "Academy deleted successfully", nil, nil)
139
+ }
140
+
141
+ func (c *academyController) AdminReorderAcademy(ctx *gin.Context) {
142
+ var req models.ReorderAcademyRequest
143
+ if err := ctx.ShouldBindJSON(&req); err != nil {
144
+ response.HandleError(ctx, err)
145
+ return
146
+ }
147
+
148
+ err := c.academyService.AdminReorderAcademy(ctx, &req)
149
+ if err != nil {
150
+ response.HandleError(ctx, err)
151
+ return
152
+ }
153
+
154
+ response.HandleSuccess(ctx, http.StatusOK, "Academy reordered successfully", nil, nil)
155
+ }
156
+
157
+ func (c *academyController) AdminCreateAcademyMaterial(ctx *gin.Context) {
158
+ var req models.CreateAcademyMaterialRequest
159
+ if err := ctx.ShouldBindJSON(&req); err != nil {
160
+ response.HandleError(ctx, err)
161
+ return
162
+ }
163
+
164
+ academyID := ctx.Param("id")
165
+ academyIDInt, err := strconv.ParseInt(academyID, 10, 64)
166
+ if err != nil {
167
+ response.HandleError(ctx, err)
168
+ return
169
+ }
170
+
171
+ req.AcademyID = academyIDInt
172
+
173
+ res, err := c.academyService.AdminCreateAcademyMaterial(ctx, &req)
174
+ if err != nil {
175
+ response.HandleError(ctx, err)
176
+ return
177
+ }
178
+
179
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material created successfully", res, nil)
180
+ }
181
+
182
+ func (c *academyController) AdminGetAcademyMaterial(ctx *gin.Context) {
183
+ id := ctx.Param("materialId")
184
+ idInt, err := strconv.ParseInt(id, 10, 64)
185
+ if err != nil {
186
+ response.HandleError(ctx, err)
187
+ return
188
+ }
189
+
190
+ res, err := c.academyService.AdminGetAcademyMaterialByID(ctx, idInt)
191
+ if err != nil {
192
+ response.HandleError(ctx, err)
193
+ return
194
+ }
195
+
196
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material retrieved successfully", res, nil)
197
+ }
198
+
199
+ func (c *academyController) AdminUpdateAcademyMaterial(ctx *gin.Context) {
200
+ id := ctx.Param("materialId")
201
+ idInt, err := strconv.ParseInt(id, 10, 64)
202
+ if err != nil {
203
+ response.HandleError(ctx, err)
204
+ return
205
+ }
206
+
207
+ var req models.UpdateAcademyMaterialRequest
208
+ if err := ctx.ShouldBindJSON(&req); err != nil {
209
+ response.HandleError(ctx, err)
210
+ return
211
+ }
212
+
213
+ req.ID = idInt
214
+
215
+ res, err := c.academyService.AdminUpdateAcademyMaterial(ctx, &req)
216
+ if err != nil {
217
+ response.HandleError(ctx, err)
218
+ return
219
+ }
220
+
221
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material updated successfully", res, nil)
222
+ }
223
+
224
+ func (c *academyController) AdminDeleteAcademyMaterial(ctx *gin.Context) {
225
+ id := ctx.Param("materialId")
226
+ idInt, err := strconv.ParseInt(id, 10, 64)
227
+ if err != nil {
228
+ response.HandleError(ctx, err)
229
+ return
230
+ }
231
+
232
+ err = c.academyService.AdminDeleteAcademyMaterial(ctx, idInt)
233
+ if err != nil {
234
+ response.HandleError(ctx, err)
235
+ return
236
+ }
237
+
238
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material deleted successfully", nil, nil)
239
+ }
240
+
241
+ func (c *academyController) AdminReorderAcademyMaterial(ctx *gin.Context) {
242
+ req := models.ReorderAcademyMaterialRequest{}
243
+ if err := ctx.ShouldBindJSON(&req); err != nil {
244
+ response.HandleError(ctx, err)
245
+ return
246
+ }
247
+
248
+ academyID := ctx.Param("id")
249
+ academyIDInt, err := strconv.ParseInt(academyID, 10, 64)
250
+ if err != nil {
251
+ response.HandleError(ctx, err)
252
+ return
253
+ }
254
+
255
+ req.AcademyID = academyIDInt
256
+
257
+ err = c.academyService.AdminReorderAcademyMaterial(ctx, &req)
258
+ if err != nil {
259
+ response.HandleError(ctx, err)
260
+ return
261
+ }
262
+
263
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material reordered successfully", nil, nil)
264
+ }
265
+
266
+ func (c *academyController) AdminListAcademyMaterial(ctx *gin.Context) {
267
+ req := models.NewListAcademyMaterialRequest()
268
+ if err := ctx.ShouldBindQuery(&req); err != nil {
269
+ response.HandleError(ctx, err)
270
+ return
271
+ }
272
+
273
+ academyID := ctx.Param("id")
274
+ academyIDInt, err := strconv.ParseInt(academyID, 10, 64)
275
+ if err != nil {
276
+ response.HandleError(ctx, err)
277
+ return
278
+ }
279
+
280
+ accountData := middleware.GetAccountData(ctx)
281
+ req.AccountID = int64(accountData.UserID)
282
+ req.AcademyID = academyIDInt
283
+
284
+ res, paging, err := c.academyService.AdminListAcademyMaterial(ctx, &req)
285
+ if err != nil {
286
+ response.HandleError(ctx, err)
287
+ return
288
+ }
289
+
290
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material list retrieved successfully", res, paging)
291
+ }
292
+
293
+ // === USER ===
294
+
295
+ func (c *academyController) UserListAcademy(ctx *gin.Context) {
296
+ req := models.NewListAcademyRequest()
297
+ if err := ctx.ShouldBindQuery(&req); err != nil {
298
+ response.HandleError(ctx, err)
299
+ return
300
+ }
301
+
302
+ accountData := middleware.GetAccountData(ctx)
303
+ req.AccountID = int64(accountData.UserID)
304
+
305
+ res, paging, err := c.academyService.UserListAcademy(ctx, &req)
306
+ if err != nil {
307
+ response.HandleError(ctx, err)
308
+ return
309
+ }
310
+
311
+ response.HandleSuccess(ctx, http.StatusOK, "Academy list retrieved successfully", res, paging)
312
+ }
313
+
314
+ func (c *academyController) UserListAcademyMaterial(ctx *gin.Context) {
315
+ req := models.NewListAcademyMaterialRequest()
316
+ if err := ctx.ShouldBindQuery(&req); err != nil {
317
+ response.HandleError(ctx, err)
318
+ return
319
+ }
320
+
321
+ accountData := middleware.GetAccountData(ctx)
322
+ req.AccountID = int64(accountData.UserID)
323
+
324
+ slug := ctx.Param("slug")
325
+ req.Slug = slug
326
+
327
+ res, paging, err := c.academyService.UserListAcademyMaterial(ctx, &req)
328
+ if err != nil {
329
+ response.HandleError(ctx, err)
330
+ return
331
+ }
332
+
333
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material list retrieved successfully", res, paging)
334
+ }
335
+
336
+ func (c *academyController) UserGetAcademyMaterialBySlug(ctx *gin.Context) {
337
+ slug := ctx.Param("materialSlug")
338
+
339
+ accountData := middleware.GetAccountData(ctx)
340
+ accountID := int64(accountData.UserID)
341
+
342
+ res, err := c.academyService.UserGetAcademyMaterialBySlug(ctx, slug, accountID)
343
+ if err != nil {
344
+ response.HandleError(ctx, err)
345
+ return
346
+ }
347
+
348
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material retrieved successfully", res, nil)
349
+ }
350
+
351
+ func (c *academyController) UserToggleAcademyMaterialProgress(ctx *gin.Context) {
352
+ slug := ctx.Param("materialSlug")
353
+
354
+ accountData := middleware.GetAccountData(ctx)
355
+ accountID := int64(accountData.UserID)
356
+
357
+ req := models.ToggleAcademyMaterialProgressRequest{
358
+ AccountID: accountID,
359
+ Slug: slug,
360
+ }
361
+
362
+ err := c.academyService.UserToggleAcademyMaterialProgress(ctx, &req)
363
+ if err != nil {
364
+ response.HandleError(ctx, err)
365
+ return
366
+ }
367
+
368
+ response.HandleSuccess(ctx, http.StatusOK, "Academy material progress toggled successfully", nil, nil)
369
+ }
space/space/space/space/space/space/space/space/space/controller/cv/cv_controller.go CHANGED
@@ -66,11 +66,7 @@ func NewCVController(cvService services.CVService) CVController {
66
  func (c *cvController) SaveAccountDetails(ctx *gin.Context) {
67
  var req models.SaveAccountDetailsRequest
68
  if err := ctx.ShouldBindJSON(&req); err != nil {
69
- response.HandleError(ctx, models.Exception{
70
- Message: "Invalid body request",
71
- BadRequest: true,
72
- Err: err,
73
- })
74
  return
75
  }
76
 
@@ -108,11 +104,7 @@ func (c *cvController) GetAccountDetails(ctx *gin.Context) {
108
  func (c *cvController) SavePersonalityAndPreference(ctx *gin.Context) {
109
  var req models.SavePersonalityAndPreferenceRequest
110
  if err := ctx.ShouldBindJSON(&req); err != nil {
111
- response.HandleError(ctx, models.Exception{
112
- Message: "Invalid body request",
113
- BadRequest: true,
114
- Err: err,
115
- })
116
  return
117
  }
118
 
@@ -150,11 +142,7 @@ func (c *cvController) GetPersonalityAndPreference(ctx *gin.Context) {
150
  func (c *cvController) CreateFamilyMember(ctx *gin.Context) {
151
  var req models.CreateFamilyMemberRequest
152
  if err := ctx.ShouldBindJSON(&req); err != nil {
153
- response.HandleError(ctx, models.Exception{
154
- Message: "Invalid body request",
155
- BadRequest: true,
156
- Err: err,
157
- })
158
  return
159
  }
160
 
@@ -180,11 +168,7 @@ func (c *cvController) UpdateFamilyMember(ctx *gin.Context) {
180
 
181
  var req models.UpdateFamilyMemberRequest
182
  if err := ctx.ShouldBindJSON(&req); err != nil {
183
- response.HandleError(ctx, models.Exception{
184
- Message: "Invalid body request",
185
- BadRequest: true,
186
- Err: err,
187
- })
188
  return
189
  }
190
 
@@ -263,11 +247,7 @@ func (c *cvController) DeleteFamilyMember(ctx *gin.Context) {
263
  func (c *cvController) SavePhysicalAndHealth(ctx *gin.Context) {
264
  var req models.SavePhysicalAndHealthRequest
265
  if err := ctx.ShouldBindJSON(&req); err != nil {
266
- response.HandleError(ctx, models.Exception{
267
- Message: "Invalid body request",
268
- BadRequest: true,
269
- Err: err,
270
- })
271
  return
272
  }
273
 
@@ -305,11 +285,7 @@ func (c *cvController) GetPhysicalAndHealth(ctx *gin.Context) {
305
  func (c *cvController) SaveWorshipAndReligiousUnderstanding(ctx *gin.Context) {
306
  var req models.SaveWorshipAndReligiousUnderstandingRequest
307
  if err := ctx.ShouldBindJSON(&req); err != nil {
308
- response.HandleError(ctx, models.Exception{
309
- Message: "Invalid body request",
310
- BadRequest: true,
311
- Err: err,
312
- })
313
  return
314
  }
315
 
@@ -346,11 +322,7 @@ func (c *cvController) GetWorshipAndReligiousUnderstanding(ctx *gin.Context) {
346
  func (c *cvController) CreateEducation(ctx *gin.Context) {
347
  var req models.CreateEducationRequest
348
  if err := ctx.ShouldBindJSON(&req); err != nil {
349
- response.HandleError(ctx, models.Exception{
350
- Message: "Invalid body request",
351
- BadRequest: true,
352
- Err: err,
353
- })
354
  return
355
  }
356
 
@@ -376,11 +348,7 @@ func (c *cvController) UpdateEducation(ctx *gin.Context) {
376
 
377
  var req models.UpdateEducationRequest
378
  if err := ctx.ShouldBindJSON(&req); err != nil {
379
- response.HandleError(ctx, models.Exception{
380
- Message: "Invalid body request",
381
- BadRequest: true,
382
- Err: err,
383
- })
384
  return
385
  }
386
 
@@ -458,11 +426,7 @@ func (c *cvController) DeleteEducation(ctx *gin.Context) {
458
  func (c *cvController) CreateJob(ctx *gin.Context) {
459
  var req models.CreateJobRequest
460
  if err := ctx.ShouldBindJSON(&req); err != nil {
461
- response.HandleError(ctx, models.Exception{
462
- Message: "Invalid body request",
463
- BadRequest: true,
464
- Err: err,
465
- })
466
  return
467
  }
468
 
@@ -488,11 +452,7 @@ func (c *cvController) UpdateJob(ctx *gin.Context) {
488
 
489
  var req models.UpdateJobRequest
490
  if err := ctx.ShouldBindJSON(&req); err != nil {
491
- response.HandleError(ctx, models.Exception{
492
- Message: "Invalid body request",
493
- BadRequest: true,
494
- Err: err,
495
- })
496
  return
497
  }
498
 
@@ -570,11 +530,7 @@ func (c *cvController) DeleteJob(ctx *gin.Context) {
570
  func (c *cvController) CreateAchievement(ctx *gin.Context) {
571
  var req models.CreateAchievementRequest
572
  if err := ctx.ShouldBindJSON(&req); err != nil {
573
- response.HandleError(ctx, models.Exception{
574
- Message: "Invalid body request",
575
- BadRequest: true,
576
- Err: err,
577
- })
578
  return
579
  }
580
 
@@ -600,11 +556,7 @@ func (c *cvController) UpdateAchievement(ctx *gin.Context) {
600
 
601
  var req models.UpdateAchievementRequest
602
  if err := ctx.ShouldBindJSON(&req); err != nil {
603
- response.HandleError(ctx, models.Exception{
604
- Message: "Invalid body request",
605
- BadRequest: true,
606
- Err: err,
607
- })
608
  return
609
  }
610
 
 
66
  func (c *cvController) SaveAccountDetails(ctx *gin.Context) {
67
  var req models.SaveAccountDetailsRequest
68
  if err := ctx.ShouldBindJSON(&req); err != nil {
69
+ response.HandleError(ctx, err)
 
 
 
 
70
  return
71
  }
72
 
 
104
  func (c *cvController) SavePersonalityAndPreference(ctx *gin.Context) {
105
  var req models.SavePersonalityAndPreferenceRequest
106
  if err := ctx.ShouldBindJSON(&req); err != nil {
107
+ response.HandleError(ctx, err)
 
 
 
 
108
  return
109
  }
110
 
 
142
  func (c *cvController) CreateFamilyMember(ctx *gin.Context) {
143
  var req models.CreateFamilyMemberRequest
144
  if err := ctx.ShouldBindJSON(&req); err != nil {
145
+ response.HandleError(ctx, err)
 
 
 
 
146
  return
147
  }
148
 
 
168
 
169
  var req models.UpdateFamilyMemberRequest
170
  if err := ctx.ShouldBindJSON(&req); err != nil {
171
+ response.HandleError(ctx, err)
 
 
 
 
172
  return
173
  }
174
 
 
247
  func (c *cvController) SavePhysicalAndHealth(ctx *gin.Context) {
248
  var req models.SavePhysicalAndHealthRequest
249
  if err := ctx.ShouldBindJSON(&req); err != nil {
250
+ response.HandleError(ctx, err)
 
 
 
 
251
  return
252
  }
253
 
 
285
  func (c *cvController) SaveWorshipAndReligiousUnderstanding(ctx *gin.Context) {
286
  var req models.SaveWorshipAndReligiousUnderstandingRequest
287
  if err := ctx.ShouldBindJSON(&req); err != nil {
288
+ response.HandleError(ctx, err)
 
 
 
 
289
  return
290
  }
291
 
 
322
  func (c *cvController) CreateEducation(ctx *gin.Context) {
323
  var req models.CreateEducationRequest
324
  if err := ctx.ShouldBindJSON(&req); err != nil {
325
+ response.HandleError(ctx, err)
 
 
 
 
326
  return
327
  }
328
 
 
348
 
349
  var req models.UpdateEducationRequest
350
  if err := ctx.ShouldBindJSON(&req); err != nil {
351
+ response.HandleError(ctx, err)
 
 
 
 
352
  return
353
  }
354
 
 
426
  func (c *cvController) CreateJob(ctx *gin.Context) {
427
  var req models.CreateJobRequest
428
  if err := ctx.ShouldBindJSON(&req); err != nil {
429
+ response.HandleError(ctx, err)
 
 
 
 
430
  return
431
  }
432
 
 
452
 
453
  var req models.UpdateJobRequest
454
  if err := ctx.ShouldBindJSON(&req); err != nil {
455
+ response.HandleError(ctx, err)
 
 
 
 
456
  return
457
  }
458
 
 
530
  func (c *cvController) CreateAchievement(ctx *gin.Context) {
531
  var req models.CreateAchievementRequest
532
  if err := ctx.ShouldBindJSON(&req); err != nil {
533
+ response.HandleError(ctx, err)
 
 
 
 
534
  return
535
  }
536
 
 
556
 
557
  var req models.UpdateAchievementRequest
558
  if err := ctx.ShouldBindJSON(&req); err != nil {
559
+ response.HandleError(ctx, err)
 
 
 
 
560
  return
561
  }
562
 
space/space/space/space/space/space/space/space/space/controller/email/email_controller.go CHANGED
@@ -43,11 +43,7 @@ func (c *emailController) CreateEmailVerification(ctx *gin.Context) {
43
  func (c *emailController) Verify(ctx *gin.Context) {
44
  var req models.ValidateEmailVerificationRequest
45
  if err := ctx.ShouldBindJSON(&req); err != nil {
46
- response.HandleError(ctx, models.Exception{
47
- Message: "Invalid body request",
48
- BadRequest: true,
49
- Err: err,
50
- })
51
  return
52
  }
53
 
 
43
  func (c *emailController) Verify(ctx *gin.Context) {
44
  var req models.ValidateEmailVerificationRequest
45
  if err := ctx.ShouldBindJSON(&req); err != nil {
46
+ response.HandleError(ctx, err)
 
 
 
 
47
  return
48
  }
49
 
space/space/space/space/space/space/space/space/space/controller/marriage_readiness_profile/marriage_readiness_profile_controller.go CHANGED
@@ -1,4 +1,4 @@
1
- package cv_controller
2
 
3
  import (
4
  "net/http"
@@ -28,11 +28,7 @@ func NewMarriageReadinessProfileController(marriageReadinessProfileService servi
28
  func (c *marriageReadinessProfileController) SaveMarriageReadinessProfile(ctx *gin.Context) {
29
  var req models.SaveMarriageReadinessProfileRequest
30
  if err := ctx.ShouldBindJSON(&req); err != nil {
31
- response.HandleError(ctx, models.Exception{
32
- Message: "Invalid body request",
33
- BadRequest: true,
34
- Err: err,
35
- })
36
  return
37
  }
38
 
 
1
+ package marriage_readiness_profile_controller
2
 
3
  import (
4
  "net/http"
 
28
  func (c *marriageReadinessProfileController) SaveMarriageReadinessProfile(ctx *gin.Context) {
29
  var req models.SaveMarriageReadinessProfileRequest
30
  if err := ctx.ShouldBindJSON(&req); err != nil {
31
+ response.HandleError(ctx, err)
 
 
 
 
32
  return
33
  }
34
 
space/space/space/space/space/space/space/space/space/controller/options/options_controller.go CHANGED
@@ -28,11 +28,7 @@ func NewOptionsController(optionService services.OptionsService) OptionsControll
28
  func (c *optionsController) CreateOptions(ctx *gin.Context) {
29
  var req []models.MultipleOptionsRequest
30
  if err := ctx.ShouldBindJSON(&req); err != nil {
31
- response.HandleError(ctx, models.Exception{
32
- Message: "Invalid body request",
33
- BadRequest: true,
34
- Err: err,
35
- })
36
  return
37
  }
38
 
 
28
  func (c *optionsController) CreateOptions(ctx *gin.Context) {
29
  var req []models.MultipleOptionsRequest
30
  if err := ctx.ShouldBindJSON(&req); err != nil {
31
+ response.HandleError(ctx, err)
 
 
 
 
32
  return
33
  }
34
 
space/space/space/space/space/space/space/space/space/controller/partner_criteria/partner_criteria_controller.go CHANGED
@@ -28,11 +28,7 @@ func NewPartnerCriteriaController(partnerCriteriaService services.PartnerCriteri
28
  func (c *partnerCriteriaController) SavePartnerCriteria(ctx *gin.Context) {
29
  var req models.SavePartnerCriteriaRequest
30
  if err := ctx.ShouldBindJSON(&req); err != nil {
31
- response.HandleError(ctx, models.Exception{
32
- Message: "Invalid body request",
33
- BadRequest: true,
34
- Err: err,
35
- })
36
  return
37
  }
38
 
 
28
  func (c *partnerCriteriaController) SavePartnerCriteria(ctx *gin.Context) {
29
  var req models.SavePartnerCriteriaRequest
30
  if err := ctx.ShouldBindJSON(&req); err != nil {
31
+ response.HandleError(ctx, err)
 
 
 
 
32
  return
33
  }
34
 
space/space/space/space/space/space/space/space/space/controller/quiz/list_quiz_controller.go CHANGED
@@ -16,7 +16,7 @@ func List(c *gin.Context) {
16
  }
17
  quizListController.HeaderParse(c, func() {
18
  academy_id, _ := strconv.Atoi(c.Param("academy_id"))
19
- quizList.Constructor.ID = uint(academy_id)
20
  quizList.Retrieve()
21
  quizListController.Response(c)
22
  })
 
16
  }
17
  quizListController.HeaderParse(c, func() {
18
  academy_id, _ := strconv.Atoi(c.Param("academy_id"))
19
+ quizList.Constructor.ID = int64(academy_id)
20
  quizList.Retrieve()
21
  quizListController.Response(c)
22
  })
space/space/space/space/space/space/space/space/space/main.go CHANGED
@@ -6,6 +6,7 @@ import (
6
  "strconv"
7
 
8
  "api.qobiltu.id/config"
 
9
  cv_controller "api.qobiltu.id/controller/cv"
10
  email_controller "api.qobiltu.id/controller/email"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
@@ -70,6 +71,10 @@ func main() {
70
  emailService := services.NewEmailService(emailRepository, taskDistributor)
71
  emailController := email_controller.NewEmailController(emailService)
72
 
 
 
 
 
73
  cvRepository := repositories.NewCVRepository(config.DB)
74
  cvService := services.NewCVService(cvRepository, localStorage, validator)
75
  cvController := cv_controller.NewCVController(cvService)
@@ -92,6 +97,7 @@ func main() {
92
  regionController,
93
  optionController,
94
  emailController,
 
95
  cvController,
96
  marriageReadinessProfileController,
97
  partnerCriteriaController,
 
6
  "strconv"
7
 
8
  "api.qobiltu.id/config"
9
+ academy_controller "api.qobiltu.id/controller/academy"
10
  cv_controller "api.qobiltu.id/controller/cv"
11
  email_controller "api.qobiltu.id/controller/email"
12
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
 
71
  emailService := services.NewEmailService(emailRepository, taskDistributor)
72
  emailController := email_controller.NewEmailController(emailService)
73
 
74
+ academyRepository := repositories.NewAcademyRepository(config.DB)
75
+ academyService := services.NewAcademyService(academyRepository, validator)
76
+ academyController := academy_controller.NewAcademyController(academyService)
77
+
78
  cvRepository := repositories.NewCVRepository(config.DB)
79
  cvService := services.NewCVService(cvRepository, localStorage, validator)
80
  cvController := cv_controller.NewCVController(cvService)
 
97
  regionController,
98
  optionController,
99
  emailController,
100
+ academyController,
101
  cvController,
102
  marriageReadinessProfileController,
103
  partnerCriteriaController,
space/space/space/space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -30,7 +30,7 @@ type AccountDetails struct {
30
  Gender *string `gorm:"column:gender" json:"gender"`
31
  LastEducation *string `gorm:"column:last_education" json:"last_education"`
32
  MaritalStatus *string `gorm:"column:marital_status" json:"marital_status"`
33
- Avatar *string `gorm:"column:avatar" json:"avatar"`
34
  PhoneNumber *string `gorm:"column:phone_number" json:"phone_number"`
35
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
36
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
@@ -73,38 +73,37 @@ type ForgotPassword struct {
73
  ExpiredAt time.Time `json:"expired_at"`
74
  }
75
 
76
- type Academy struct {
77
- ID uint `gorm:"primaryKey" json:"id"`
78
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
79
- Title string `json:"title"`
80
- Slug string `json:"slug" gorm:"uniqueIndex" `
81
- TotalMaterial int `json:"total_material"`
82
- CompletedMaterial int `json:"completed_material"`
83
- TotalQuiz int `json:"total_quiz"`
84
- PassedQuiz int `json:"passed_quiz"`
85
- IsCompletedRead bool `json:"is_read"`
86
- IsPassedExam bool `json:"is_passed_exam"`
87
- Description string `json:"description"`
88
- }
89
 
90
- type AcademyMaterial struct {
91
- ID uint `gorm:"primaryKey" json:"id"`
92
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
93
- AcademyID uint `json:"academy_id"`
94
- Title string `json:"title"`
95
- Slug string `json:"slug" gorm:"uniqueIndex"`
96
- IsCompleted bool `json:"is_completed"`
97
- Description string `json:"description"`
98
- }
 
 
99
 
100
- type AcademyContent struct {
101
- ID uint `gorm:"primaryKey" json:"id"`
102
- UUID uuid.UUID `json:"uuid"`
103
- Title string `json:"title"`
104
- Order uint `json:"order"`
105
- AcademyMaterialID uint `json:"academy_material_id"`
106
- Description string `json:"description"`
107
- }
108
 
109
  type OptionCategory struct {
110
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
@@ -119,21 +118,6 @@ type OptionValues struct {
119
  OptionValue string `json:"option_value"`
120
  }
121
 
122
- type AcademyMaterialProgress struct {
123
- ID uint `gorm:"primaryKey" json:"id"`
124
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
125
- AccountID uint `json:"account_id"`
126
- AcademyMaterialID uint `json:"academy_material_id"`
127
- Progress uint `json:"progress"`
128
- }
129
-
130
- type AcademyContentProgress struct {
131
- ID uint `gorm:"primaryKey" json:"id"`
132
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
133
- AccountID uint `json:"account_id"`
134
- AcademyID uint `json:"academy_id"`
135
- }
136
-
137
  type RegionProvince struct {
138
  ID uint `json:"id"`
139
  Name string `json:"name"`
@@ -424,9 +408,9 @@ type (
424
 
425
  // Pendidikan & Pekerjaan
426
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
427
- ExpectedIncomeSources *pq.StringArray `gorm:"column:expected_income_sources;type:varchar(255)[]" json:"expected_income_sources"` // sumber penghasilan pasangan
428
  ExpectedLastEducation *pq.StringArray `gorm:"column:expected_last_education;type:varchar(255)[]" json:"expected_last_education"` // pendidikan terakhir yang diharapkan
429
- ExpectedJobType *string `gorm:"column:expected_job_type" json:"expected_job_type"` // jenis pekerjaan pasangan yang diharapkan
 
430
 
431
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
432
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
@@ -442,9 +426,7 @@ func (FCM) TableName() string { return "fcm" }
442
  func (ForgotPassword) TableName() string { return "forgot_password" }
443
  func (Academy) TableName() string { return "academy" }
444
  func (AcademyMaterial) TableName() string { return "academy_materials" }
445
- func (AcademyContent) TableName() string { return "academy_contents" }
446
  func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
447
- func (AcademyContentProgress) TableName() string { return "academy_contents_progress" }
448
  func (RegionProvince) TableName() string { return "region_provinces" }
449
  func (RegionCity) TableName() string { return "region_cities" }
450
  func (Answer) TableName() string { return "answers" }
 
30
  Gender *string `gorm:"column:gender" json:"gender"`
31
  LastEducation *string `gorm:"column:last_education" json:"last_education"`
32
  MaritalStatus *string `gorm:"column:marital_status" json:"marital_status"`
33
+ Avatar *string `gorm:"column:avatar" json:"avatar" counter:"skip"`
34
  PhoneNumber *string `gorm:"column:phone_number" json:"phone_number"`
35
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
36
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
 
73
  ExpiredAt time.Time `json:"expired_at"`
74
  }
75
 
76
+ type (
77
+ Academy struct {
78
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
79
+ Title string `gorm:"column:title" json:"title"`
80
+ Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
81
+ Description string `gorm:"column:description" json:"description"`
82
+ Order uint `gorm:"column:order" json:"order"`
83
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
84
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
85
+ }
 
 
 
86
 
87
+ AcademyMaterial struct {
88
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
89
+ AcademyID int64 `gorm:"column:academy_id;not null" json:"academy_id"`
90
+ Academy *Academy `gorm:"foreignKey:AcademyID;constraint:OnDelete:CASCADE" json:"academy,omitempty"`
91
+ Title string `gorm:"column:title" json:"title"`
92
+ Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
93
+ Content string `gorm:"column:content" json:"content"`
94
+ Order uint `gorm:"column:order" json:"order"`
95
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
96
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
97
+ }
98
 
99
+ AcademyMaterialProgress struct {
100
+ AccountID int64 `gorm:"primaryKey;column:account_id" json:"account_id" counter:"skip"`
101
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
102
+ AcademyMaterialID int64 `gorm:"primaryKey;column:academy_material_id" json:"academy_material_id"`
103
+ AcademyMaterial *AcademyMaterial `gorm:"foreignKey:AcademyMaterialID;constraint:OnDelete:CASCADE" json:"academy_material,omitempty"`
104
+ ReadAt *time.Time `gorm:"column:read_at" json:"read_at"`
105
+ }
106
+ )
107
 
108
  type OptionCategory struct {
109
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
 
118
  OptionValue string `json:"option_value"`
119
  }
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  type RegionProvince struct {
122
  ID uint `json:"id"`
123
  Name string `json:"name"`
 
408
 
409
  // Pendidikan & Pekerjaan
410
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
 
411
  ExpectedLastEducation *pq.StringArray `gorm:"column:expected_last_education;type:varchar(255)[]" json:"expected_last_education"` // pendidikan terakhir yang diharapkan
412
+ // ExpectedIncomeSources *pq.StringArray `gorm:"column:expected_income_sources;type:varchar(255)[]" json:"expected_income_sources"` // sumber penghasilan pasangan
413
+ // ExpectedJobType *string `gorm:"column:expected_job_type" json:"expected_job_type"` // jenis pekerjaan pasangan yang diharapkan
414
 
415
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
416
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
426
  func (ForgotPassword) TableName() string { return "forgot_password" }
427
  func (Academy) TableName() string { return "academy" }
428
  func (AcademyMaterial) TableName() string { return "academy_materials" }
 
429
  func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
 
430
  func (RegionProvince) TableName() string { return "region_provinces" }
431
  func (RegionCity) TableName() string { return "region_cities" }
432
  func (Answer) TableName() string { return "answers" }