lifedebugger commited on
Commit
57e875f
·
1 Parent(s): 4f90512

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. config/database_connection_config.go +3 -1
  2. controller/quiz/quiz_controller.go +76 -0
  3. main.go +6 -0
  4. models/database_orm_model.go +127 -66
  5. models/exception_model.go +5 -0
  6. models/field_counter.go +40 -0
  7. models/request_model.go +30 -18
  8. models/response_model.go +0 -5
  9. repositories/academy_repository.go +33 -0
  10. repositories/quiz_repository.go +93 -141
  11. router/quiz_route.go +5 -13
  12. router/router.go +1 -1
  13. router/server.go +4 -0
  14. services/cv_service.go +22 -14
  15. services/quiz_service.go +159 -35
  16. space/space/controller/academy/academy_controller.go +11 -5
  17. space/space/models/database_orm_model.go +3 -1
  18. space/space/models/request_model.go +56 -4
  19. space/space/repositories/academy_repository.go +21 -13
  20. space/space/repositories/quiz_repository.go +19 -0
  21. space/space/router/quiz_route.go +1 -1
  22. space/space/services/academy_service.go +17 -17
  23. space/space/services/quiz_service.go +37 -0
  24. space/space/space/models/field_counter.go +0 -2
  25. space/space/space/services/partner_criteria_service.go +2 -2
  26. space/space/space/space/config/database_connection_config.go +0 -2
  27. space/space/space/space/controller/academy/academy_controller.go +369 -31
  28. space/space/space/space/controller/cv/cv_controller.go +12 -60
  29. space/space/space/space/controller/email/email_controller.go +1 -5
  30. space/space/space/space/controller/marriage_readiness_profile/marriage_readiness_profile_controller.go +2 -6
  31. space/space/space/space/controller/options/options_controller.go +1 -5
  32. space/space/space/space/controller/partner_criteria/partner_criteria_controller.go +1 -5
  33. space/space/space/space/controller/quiz/list_quiz_controller.go +1 -1
  34. space/space/space/space/main.go +6 -0
  35. space/space/space/space/models/database_orm_model.go +32 -50
  36. space/space/space/space/models/filter_and_paging.go +107 -0
  37. space/space/space/space/models/request_model.go +107 -2
  38. space/space/space/space/models/response_model.go +0 -13
  39. space/space/space/space/repositories/academy_repository.go +406 -103
  40. space/space/space/space/repositories/question_repository.go +11 -0
  41. space/space/space/space/response/api_response_v2.go +37 -11
  42. space/space/space/space/router/academy_route.go +25 -8
  43. space/space/space/space/router/router.go +1 -1
  44. space/space/space/space/router/server.go +4 -0
  45. space/space/space/space/services/academy_quiz_service.go +2 -2
  46. space/space/space/space/services/academy_service.go +316 -150
  47. space/space/space/space/space/space/assets/efs.go +8 -0
  48. space/space/space/space/space/space/assets/seeds/city.json +4114 -0
  49. space/space/space/space/space/space/assets/seeds/province.json +192 -0
  50. space/space/space/space/space/space/controller/options/options_controller.go +72 -0
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{},
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
+ accountID := ctx.GetInt64("account_id")
57
+ academyID := ctx.Param("id")
58
+ academyIDInt, err := strconv.Atoi(academyID)
59
+ if err != nil {
60
+ response.HandleError(ctx, err)
61
+ return
62
+ }
63
+
64
+ req := models.UserAttemptQuizRequest{
65
+ AccountID: accountID,
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
+ }
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,
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" }
models/exception_model.go CHANGED
@@ -18,6 +18,11 @@ 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
+ QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
24
+ AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
25
+
26
  Message string `json:"message,omitempty"`
27
  Err error `json:"-"`
28
  ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
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
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
 
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"`
repositories/academy_repository.go CHANGED
@@ -26,6 +26,7 @@ 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)
@@ -286,6 +287,38 @@ 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()
 
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)
 
287
  return academies, pageInfo, nil
288
  }
289
 
290
+ func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error) {
291
+ var totalMaterial int64
292
+ var totalReadMaterial int64
293
+
294
+ // Hitung total materi dalam academy
295
+ err := r.db.WithContext(ctx).
296
+ Model(&models.AcademyMaterial{}).
297
+ Where("academy_id = ?", academyID).
298
+ Count(&totalMaterial).Error
299
+ if err != nil {
300
+ return 0, err
301
+ }
302
+
303
+ // Hitung total materi yang sudah dibaca oleh user
304
+ err = r.db.WithContext(ctx).
305
+ Model(&models.AcademyMaterialProgress{}).
306
+ Joins("JOIN academy_materials am ON am.id = academy_material_progress.academy_material_id").
307
+ Where("academy_material_progress.account_id = ? AND am.academy_id = ?", accountID, academyID).
308
+ Count(&totalReadMaterial).Error
309
+ if err != nil {
310
+ return 0, err
311
+ }
312
+
313
+ // Hitung persentase
314
+ if totalMaterial == 0 {
315
+ return 0, nil
316
+ }
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()
repositories/quiz_repository.go CHANGED
@@ -8,7 +8,11 @@ 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 +23,92 @@ 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
  }
17
 
18
  type quizRepository struct {
 
23
  return &quizRepository{db: db}
24
  }
25
 
26
+ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
27
+ var quizResponse models.UserGetQuizResponse
28
+
29
+ rawQuery := `
30
+ SELECT
31
+ q.*,
32
+ (SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
33
+ COALESCE(COUNT(qa.id), 0) AS user_attempts
34
+ FROM
35
+ quizzes q
36
+ LEFT JOIN
37
+ quiz_attempts qa
38
+ ON q.id = qa.quiz_id
39
+ AND qa.account_id = ?
40
+ WHERE
41
+ q.academy_id = ?
42
+ GROUP BY
43
+ q.id, q.academy_id, q.slug, q.title, q.description,
44
+ q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
45
+ `
46
+
47
+ err := r.db.Raw(rawQuery, req.AccountID, req.AcademyID).Scan(&quizResponse).Error
48
+ if err != nil {
49
+ return nil, err
50
+ }
51
+
52
+ if quizResponse.ID == 0 {
53
+ return nil, gorm.ErrRecordNotFound
54
+ }
55
+
56
+ return &quizResponse, nil
57
+ }
58
+
59
+ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
60
+ var quizAttempt models.QuizAttempt
61
+
62
+ err := r.db.Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
63
+ if err != nil {
64
+ return nil, err
65
+ }
66
+
67
+ return &quizAttempt, nil
68
+ }
69
+
70
+ func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
71
+ return r.db.Model(&models.QuizAttempt{}).Where("id = ?", attempt.ID).Updates(attempt).Error
72
+ }
73
+
74
+ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
75
+ questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
76
+
77
+ rawQuery := `
78
+ SELECT
79
+ q.id,
80
+ COALESCE(ua.is_doubt, FALSE) AS is_doubt,
81
+ CASE
82
+ WHEN ua.id IS NOT NULL
83
+ AND EXISTS (
84
+ SELECT 1
85
+ FROM quiz_attempts qa
86
+ WHERE qa.id = ?
87
+ AND qa.account_id = ?
88
+ AND qa.quiz_id = ?
89
+ ) THEN TRUE
90
+ ELSE FALSE
91
+ END AS is_answered
92
+ FROM
93
+ questions q
94
+ LEFT JOIN
95
+ user_answers ua
96
+ ON q.id = ua.question_id
97
+ AND ua.quiz_attempt_id = ?
98
+ WHERE
99
+ q.quiz_id = ?
100
+ ORDER BY
101
+ q.id;
102
+ `
103
+
104
+ err := r.db.Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
105
+ if err != nil {
106
+ return nil, err
107
+ }
108
+
109
+ return questions, nil
110
+ }
111
+
112
+ func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
113
+ return r.db.Create(attempt).Error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
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
  }
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()
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,
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
  }
services/quiz_service.go CHANGED
@@ -1,37 +1,161 @@
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
+
52
+ // Ambil dulu data quiz nya berdasarkan academy id
53
+ quiz, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
54
+ AccountID: req.AccountID,
55
+ AcademyID: req.AcademyID,
56
+ })
57
+ if err != nil {
58
+ return nil, response.HandleGormError(err, "Internal Server Error")
59
+ }
60
+
61
+ // Cek dulu apakah ada attempt quiz pengguna yang sedang aktif atau tidak
62
+ existingQuizAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
63
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
64
+ return nil, response.HandleGormError(err, "Internal Server Error")
65
+ }
66
+
67
+ // Jika ada attemp quiz, cek dulu apakah sudah melewati batas waktu pengerjaan atau belum
68
+ if existingQuizAttempt.ID != 0 {
69
+ now := time.Now()
70
+
71
+ // jika sudah melewati batas waktu dan belum selesai, maka selesaikan quiz nya dan kembalikan error
72
+ if existingQuizAttempt.DueAt.Before(now) {
73
+ if existingQuizAttempt.FinishedAt == nil {
74
+ existingQuizAttempt.FinishedAt = &now
75
+ existingQuizAttempt.Score = calculateQuizScore(quiz.ID, existingQuizAttempt)
76
+ err = s.quizRepository.UserUpdateAttemptQuiz(ctx, existingQuizAttempt)
77
+ if err != nil {
78
+ return nil, response.HandleGormError(err, "Internal Server Error")
79
+ }
80
+
81
+ // cek dulu total attemp nya, jika telah melebihi batas attempt limit dari quiz, maka reset progress baca materi di academy akun tsb nya jadi 0
82
+
83
+ return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
84
+ }
85
+ }
86
+
87
+ // jika sudah selesai, maka kembalikan error quiz sudah selesai
88
+ if existingQuizAttempt.FinishedAt != nil {
89
+ return nil, models.Exception{QuizAlreadyFinished: true, Message: "Quiz already finished"}
90
+ }
91
+
92
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, existingQuizAttempt.ID)
93
+ if err != nil {
94
+ return nil, response.HandleGormError(err, "Internal Server Error")
95
+ }
96
+
97
+ questions = shuffleWithKey(questions, existingQuizAttempt.ID)
98
+ return &models.UserAttemptQuizResponse{
99
+ QuizAttempt: *existingQuizAttempt,
100
+ Questions: questions,
101
+ }, nil
102
+ }
103
+
104
+ // Cek apakah semua materi telah di baca atau belum
105
+ percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, req.AccountID, quiz.AcademyID)
106
+ if err != nil {
107
+ return nil, response.HandleGormError(err, "Internal Server Error")
108
+ }
109
+
110
+ if percentage < 100 {
111
+ return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
112
+ }
113
+
114
+ // Buat attemp quiz baru
115
+ quizAttempt := models.QuizAttempt{
116
+ AccountID: req.AccountID,
117
+ QuizID: quiz.ID,
118
+ StartedAt: time.Now(),
119
+ DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
120
+ }
121
+
122
+ err = s.quizRepository.UserCreateAttemptQuiz(ctx, &quizAttempt)
123
+ if err != nil {
124
+ return nil, response.HandleGormError(err, "Internal Server Error")
125
+ }
126
+
127
+ // Ambil question nya dan random / acak pakai attemp id
128
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, quizAttempt.ID)
129
+ if err != nil {
130
+ return nil, response.HandleGormError(err, "Internal Server Error")
131
+ }
132
+
133
+ questions = shuffleWithKey(questions, quizAttempt.ID)
134
+
135
+ return &models.UserAttemptQuizResponse{
136
+ QuizAttempt: quizAttempt,
137
+ Questions: questions,
138
+ }, nil
139
+ }
140
+
141
+ func calculateQuizScore(quizID int64, attempt *models.QuizAttempt) float64 {
142
+ return 0
143
+ }
144
+
145
+ // shuffleWithKey mengacak slice dengan menggunakan integer key sebagai seed
146
+ // untuk memastikan hasil acak yang konsisten untuk key yang sama
147
+ func shuffleWithKey[T any](slice []T, key int64) []T {
148
+ // Buat salinan slice untuk menghindari modifikasi original
149
+ shuffled := make([]T, len(slice))
150
+ copy(shuffled, slice)
151
+
152
+ // Buat random source dengan seed dari key
153
+ r := rand.New(rand.NewSource(key))
154
+
155
+ // Lakukan shuffling
156
+ r.Shuffle(len(shuffled), func(i, j int) {
157
+ shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
158
+ })
159
+
160
+ return shuffled
161
+ }
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" }
space/space/space/space/models/filter_and_paging.go ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "strings"
7
+ )
8
+
9
+ const (
10
+ AscOrder string = "ASC"
11
+ DescOrder string = "DESC"
12
+ DefaultPageLimit int = 50
13
+ DefaultCurrentPage int = 1
14
+ DefaultColumnOrder string = AscOrder
15
+ DefaultMaxPageLimit int = 100
16
+ )
17
+
18
+ type Filter struct {
19
+ CurrentPage int `json:"current_page" form:"current_page" query:"current_page"` // page (berpindah-pindah halaman)
20
+ PerPage int `json:"per_page" form:"per_page" query:"per_page"` // limit (batas data yang ditampilkan)
21
+ Keyword string `json:"keyword" form:"keyword" query:"keyword"` // search keyword (keyword pencarian)
22
+ SortBy string `json:"sort_by" form:"sort_by" query:"sort_by"` // column name to sort
23
+ Order string `json:"order" form:"order" query:"order"` // asc or desc order
24
+ }
25
+
26
+ func NewFilter() Filter {
27
+ return Filter{
28
+ CurrentPage: DefaultCurrentPage,
29
+ PerPage: DefaultPageLimit,
30
+ Order: DefaultColumnOrder,
31
+ }
32
+ }
33
+
34
+ func (f *Filter) GetLimit() int {
35
+ return f.PerPage
36
+ }
37
+
38
+ func (f *Filter) GetOffset() int {
39
+ offset := (f.CurrentPage - 1) * f.PerPage // example: (1 - 1) * 10 = 0, (2 - 1) * 10 = 10
40
+ return offset
41
+ }
42
+
43
+ func (f *Filter) HasKeyword() bool {
44
+ return f.Keyword != ""
45
+ }
46
+
47
+ func (f *Filter) HasSort() bool {
48
+ return f.SortBy != ""
49
+ }
50
+
51
+ func (f *Filter) IsDesc() bool {
52
+ return strings.ToUpper(f.Order) == DescOrder
53
+ }
54
+
55
+ // ===============================
56
+
57
+ type Paging struct {
58
+ HasPreviousPage bool `json:"has_previous_page"`
59
+ HasNextPage bool `json:"has_next_page"`
60
+ CurrentPage int `json:"current_page"`
61
+ PerPage int `json:"per_page"`
62
+ TotalData int `json:"total_data"`
63
+ LastPage int `json:"last_page"`
64
+ From int `json:"from"`
65
+ To int `json:"to"`
66
+ TotalDataInCurrentPage int `json:"total_data_in_current_page"`
67
+ Label string `json:"label"`
68
+ }
69
+
70
+ var ErrPaging = errors.New("per_page harus lebih besar dari 0 dan offset tidak boleh negatif")
71
+
72
+ func NewPaging(currentPage, perPage, offset, totalData int) (*Paging, error) {
73
+ if perPage <= 0 || offset < 0 {
74
+ return nil, ErrPaging
75
+ }
76
+
77
+ lastPage := totalData / perPage
78
+ if totalData%perPage != 0 {
79
+ lastPage++
80
+ }
81
+
82
+ to := min(offset+perPage, totalData)
83
+ from := int(0)
84
+ if to > offset {
85
+ from = offset + 1
86
+ }
87
+
88
+ if currentPage > lastPage {
89
+ currentPage = lastPage
90
+ }
91
+
92
+ totalDataInCurrentPage := to - offset
93
+ label := fmt.Sprintf("Menampilkan %d sampai %d dari %d data", from, to, totalData)
94
+
95
+ return &Paging{
96
+ HasPreviousPage: currentPage > 1,
97
+ HasNextPage: currentPage < lastPage,
98
+ CurrentPage: currentPage,
99
+ PerPage: perPage,
100
+ TotalData: totalData,
101
+ LastPage: lastPage,
102
+ From: from,
103
+ To: to,
104
+ TotalDataInCurrentPage: totalDataInCurrentPage,
105
+ Label: label,
106
+ }, nil
107
+ }
space/space/space/space/models/request_model.go CHANGED
@@ -28,6 +28,109 @@ type CreateVerifyEmailRequest struct {
28
  Token uint `json:"token" binding:"required"`
29
  }
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  type (
32
  MultipleOptionsRequest struct {
33
  OptionName string `json:"option_name" validate:"required"`
@@ -395,9 +498,11 @@ type (
395
 
396
  // Pendidikan & Pekerjaan
397
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
398
- ExpectedIncomeSources *pq.StringArray `gorm:"column:expected_income_sources;type:varchar(255)[]" json:"expected_income_sources"` // sumber penghasilan pasangan
399
  ExpectedLastEducation *pq.StringArray `gorm:"column:expected_last_education;type:varchar(255)[]" json:"expected_last_education"` // pendidikan terakhir yang diharapkan
400
- ExpectedJobType *string `gorm:"column:expected_job_type" json:"expected_job_type"` // jenis pekerjaan pasangan yang diharapkan
 
 
 
401
 
402
  }
403
 
 
28
  Token uint `json:"token" binding:"required"`
29
  }
30
 
31
+ type (
32
+ CreateAcademyRequest struct {
33
+ AccountID int64 `json:"account_id"`
34
+ Title string `json:"title" validate:"required"`
35
+ Description string `json:"description"`
36
+ Order *uint `json:"order"`
37
+ }
38
+
39
+ UpdateAcademyRequest struct {
40
+ ID int64 `json:"id"`
41
+ Title *string `json:"title"`
42
+ Description *string `json:"description"`
43
+ Order *uint `json:"order"`
44
+ }
45
+
46
+ AcademyResponse struct {
47
+ Academy
48
+ TotalMaterial int64 `gorm:"total_material" json:"total_material"`
49
+ }
50
+
51
+ UserAcademyResponse struct {
52
+ Academy
53
+ TotalMaterial int64 `gorm:"total_material" json:"total_material"`
54
+ TotalReadMaterial int64 `gorm:"total_read_material" json:"total_read_material"`
55
+ PercentageReadMaterial float64 `gorm:"percentage_read_material" json:"percentage_read_material"`
56
+ }
57
+
58
+ UserAcademyMaterialResponse struct {
59
+ AcademyMaterial
60
+ IsRead bool `gorm:"is_read" json:"is_read"`
61
+ }
62
+
63
+ CreateAcademyMaterialRequest struct {
64
+ AcademyID int64 `json:"academy_id" validate:"required"`
65
+ Title string `json:"title" validate:"required"`
66
+ Content string `json:"content"`
67
+ Order *uint `json:"order"`
68
+ }
69
+
70
+ UpdateAcademyMaterialRequest struct {
71
+ ID int64 `json:"id"`
72
+ Title *string `json:"title"`
73
+ Content *string `json:"content"`
74
+ Order *uint `json:"order"`
75
+ }
76
+
77
+ ListAcademyRequest struct {
78
+ AccountID int64
79
+ Filter
80
+ }
81
+
82
+ ListAcademyMaterialRequest struct {
83
+ AccountID int64
84
+ AcademyID int64
85
+ Slug string
86
+ Filter
87
+ }
88
+
89
+ ListAcademyContentRequest struct {
90
+ AccountID int64
91
+ AcademyID int64
92
+ AcademyMaterialID int64
93
+ Filter
94
+ }
95
+
96
+ ReorderRequest struct {
97
+ ID int64 `json:"id"`
98
+ Order int64 `json:"order"`
99
+ }
100
+
101
+ ReorderAcademyRequest struct {
102
+ Reorder []ReorderRequest `json:"reorder"`
103
+ }
104
+
105
+ ReorderAcademyMaterialRequest struct {
106
+ AcademyID int64 `json:"academy_id"`
107
+ Reorder []ReorderRequest `json:"reorder"`
108
+ }
109
+
110
+ ToggleAcademyMaterialProgressRequest struct {
111
+ AccountID int64 `json:"account_id"`
112
+ Slug string `json:"slug"`
113
+ }
114
+ )
115
+
116
+ func NewListAcademyRequest() ListAcademyRequest {
117
+ return ListAcademyRequest{
118
+ Filter: NewFilter(),
119
+ }
120
+ }
121
+
122
+ func NewListAcademyMaterialRequest() ListAcademyMaterialRequest {
123
+ return ListAcademyMaterialRequest{
124
+ Filter: NewFilter(),
125
+ }
126
+ }
127
+
128
+ func NewListAcademyContentRequest() ListAcademyContentRequest {
129
+ return ListAcademyContentRequest{
130
+ Filter: NewFilter(),
131
+ }
132
+ }
133
+
134
  type (
135
  MultipleOptionsRequest struct {
136
  OptionName string `json:"option_name" validate:"required"`
 
498
 
499
  // Pendidikan & Pekerjaan
500
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
 
501
  ExpectedLastEducation *pq.StringArray `gorm:"column:expected_last_education;type:varchar(255)[]" json:"expected_last_education"` // pendidikan terakhir yang diharapkan
502
+
503
+ // NOT USED
504
+ // ExpectedIncomeSources *pq.StringArray `gorm:"column:expected_income_sources;type:varchar(255)[]" json:"expected_income_sources"` // sumber penghasilan pasangan
505
+ // ExpectedJobType *string `gorm:"column:expected_job_type" json:"expected_job_type"` // jenis pekerjaan pasangan yang diharapkan
506
 
507
  }
508
 
space/space/space/space/models/response_model.go CHANGED
@@ -33,19 +33,6 @@ type UserProfileResponse struct {
33
  Details AccountDetails `json:"details"`
34
  }
35
 
36
- type AcademyMaterialResponse struct {
37
- Materials AcademyMaterial
38
- Contents AcademyContent
39
- }
40
- type AcademyResponse struct {
41
- Academy Academy `json:"academy"`
42
- Materials []AcademyMaterialResponse `json:"academy_materials"`
43
- }
44
-
45
- type AllAcademyResponse struct {
46
- Academies []AcademyResponse `json:"academy_dasar"`
47
- }
48
-
49
  type AttemptExamResponse struct {
50
  Exam Quiz `json:"exam"`
51
  Questions []Question `json:"questions"`
 
33
  Details AccountDetails `json:"details"`
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  type AttemptExamResponse struct {
37
  Exam Quiz `json:"exam"`
38
  Questions []Question `json:"questions"`
space/space/space/space/repositories/academy_repository.go CHANGED
@@ -1,105 +1,408 @@
1
  package repositories
2
 
3
- import "api.qobiltu.id/models"
4
-
5
- func GetAllAcademy() Repository[models.Academy, []models.Academy] {
6
- repo := Construct[models.Academy, []models.Academy](
7
- models.Academy{},
8
- )
9
- repo.Transactions(
10
- WhereGivenConstructor[models.Academy, []models.Academy],
11
- Find[models.Academy, []models.Academy],
12
- )
13
- return *repo
14
- }
15
-
16
- func GetAcademyDataBySlug(slug string) Repository[models.Academy, models.Academy] {
17
- repo := Construct[models.Academy, models.Academy](
18
- models.Academy{Slug: slug},
19
- )
20
- repo.Transactions(
21
- WhereGivenConstructor[models.Academy, models.Academy],
22
- Find[models.Academy, models.Academy],
23
- )
24
- return *repo
25
- }
26
-
27
- func GetAllAcademyMaterialsByAcademyID(acaddemyId uint) Repository[models.AcademyMaterial, []models.AcademyMaterial] {
28
- repo := Construct[models.AcademyMaterial, []models.AcademyMaterial](
29
- models.AcademyMaterial{AcademyID: acaddemyId},
30
- )
31
- repo.Transactions(
32
- WhereGivenConstructor[models.AcademyMaterial, []models.AcademyMaterial],
33
- Find[models.AcademyMaterial, []models.AcademyMaterial],
34
- )
35
- return *repo
36
- }
37
-
38
- func GetAllAcademyContentsByMaterialID(materialId uint) Repository[models.AcademyContent, models.AcademyContent] {
39
- repo := Construct[models.AcademyContent, models.AcademyContent](
40
- models.AcademyContent{AcademyMaterialID: materialId},
41
- )
42
- repo.Transactions(
43
- WhereGivenConstructor[models.AcademyContent, models.AcademyContent],
44
- Find[models.AcademyContent, models.AcademyContent],
45
- )
46
- return *repo
47
- }
48
-
49
- func CreateAcademy(academies models.Academy) Repository[models.Academy, models.Academy] {
50
- repo := Construct[models.Academy, models.Academy](
51
- academies,
52
- )
53
-
54
- Create(repo)
55
- return *repo
56
- }
57
-
58
- func CreateAcademyMaterial(academyMaterial models.AcademyMaterial) Repository[models.AcademyMaterial, models.AcademyMaterial] {
59
- repo := Construct[models.AcademyMaterial, models.AcademyMaterial](
60
- academyMaterial,
61
- )
62
-
63
- Create(repo)
64
- return *repo
65
- }
66
-
67
- func CreateAcademyContent(academyContent models.AcademyContent) Repository[models.AcademyContent, models.AcademyContent] {
68
- repo := Construct[models.AcademyContent, models.AcademyContent](
69
- academyContent,
70
- )
71
- Create(repo)
72
- return *repo
73
- }
74
-
75
- func GetAcademyMaterialBySlug(slug string) Repository[models.AcademyMaterial, models.AcademyMaterial] {
76
- repo := Construct[models.AcademyMaterial, models.AcademyMaterial](
77
- models.AcademyMaterial{Slug: slug},
78
- )
79
- repo.Transactions(
80
- WhereGivenConstructor[models.AcademyMaterial, models.AcademyMaterial],
81
- Find[models.AcademyMaterial, models.AcademyMaterial],
82
- )
83
- return *repo
84
- }
85
-
86
- func GetAcademyByID(id uint) Repository[models.Academy, models.Academy] {
87
- repo := Construct[models.Academy, models.Academy](
88
- models.Academy{ID: id},
89
- )
90
- repo.Transactions(
91
- WhereGivenConstructor[models.Academy, models.Academy],
92
- Find[models.Academy, models.Academy],
93
- )
94
- return *repo
95
- }
96
-
97
- func UpdateAcademyMaterialCompletedById(id_material uint) Repository[models.AcademyMaterial, models.AcademyMaterial] {
98
- repo := Construct[models.AcademyMaterial, models.AcademyMaterial](
99
- models.AcademyMaterial{ID: id_material, IsCompleted: true},
100
- )
101
- Update(repo)
102
- return *repo
103
- }
104
-
105
- // func UpdateAcademyProgressById(id_academy uint)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  package repositories
2
 
3
+ import (
4
+ "context"
5
+
6
+ "api.qobiltu.id/models"
7
+ "gorm.io/gorm"
8
+ "gorm.io/gorm/clause"
9
+ )
10
+
11
+ type AcademyRepository interface {
12
+ // === ADMIN ===
13
+ AdminSaveAcademy(ctx context.Context, req *models.Academy) (*models.AcademyResponse, error)
14
+ AdminGetAcademyByID(ctx context.Context, id int64) (*models.AcademyResponse, error)
15
+ AdminListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.AcademyResponse, *models.Paging, error)
16
+ AdminDeleteAcademy(ctx context.Context, id int64) error
17
+ AdminReorderAcademy(ctx context.Context, req *models.ReorderAcademyRequest) error
18
+ AdminGetLastOrderAcademy(ctx context.Context) (uint, error)
19
+
20
+ AdminSaveAcademyMaterial(ctx context.Context, req *models.AcademyMaterial) error
21
+ AdminGetAcademyMaterialByID(ctx context.Context, id int64) (*models.AcademyMaterial, error)
22
+ AdminListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.AcademyMaterial, *models.Paging, error)
23
+ AdminDeleteAcademyMaterial(ctx context.Context, id int64) error
24
+ AdminReorderAcademyMaterial(ctx context.Context, req *models.ReorderAcademyMaterialRequest) error
25
+ AdminGetLastOrderAcademyMaterial(ctx context.Context) (uint, error)
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.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
35
+ }
36
+
37
+ type academyRepository struct {
38
+ db *gorm.DB
39
+ }
40
+
41
+ func NewAcademyRepository(db *gorm.DB) AcademyRepository {
42
+ return &academyRepository{
43
+ db: db,
44
+ }
45
+ }
46
+
47
+ func (r *academyRepository) AdminSaveAcademy(ctx context.Context, req *models.Academy) (*models.AcademyResponse, error) {
48
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
49
+ return nil, err
50
+ }
51
+
52
+ var academy models.AcademyResponse
53
+ var totalMaterial int64
54
+ if err := r.db.WithContext(ctx).Model(&models.AcademyMaterial{}).Where("academy_id = ?", req.ID).Count(&totalMaterial).Error; err != nil {
55
+ return nil, err
56
+ }
57
+
58
+ academy.Academy = *req
59
+ academy.TotalMaterial = totalMaterial
60
+
61
+ return &academy, nil
62
+ }
63
+
64
+ func (r *academyRepository) AdminGetAcademyByID(ctx context.Context, id int64) (*models.AcademyResponse, error) {
65
+ var academy models.AcademyResponse
66
+
67
+ subQuery := r.db.Model(&models.AcademyMaterial{}).
68
+ Select("COUNT(*)").
69
+ Where("academy_id = academy.id")
70
+
71
+ if err := r.db.WithContext(ctx).
72
+ Model(&models.Academy{}).
73
+ Select("academy.*, (?) AS total_material", subQuery).
74
+ Where("academy.id = ?", id).
75
+ Scan(&academy).Error; err != nil {
76
+ return nil, err
77
+ }
78
+
79
+ if academy.ID == 0 {
80
+ return nil, gorm.ErrRecordNotFound
81
+ }
82
+
83
+ return &academy, nil
84
+ }
85
+
86
+ func (r *academyRepository) AdminListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.AcademyResponse, *models.Paging, error) {
87
+ academies := make([]models.AcademyResponse, 0)
88
+
89
+ offset := req.Filter.GetOffset()
90
+ limit := req.Filter.GetLimit()
91
+
92
+ // Subquery untuk count materials
93
+ subQuery := r.db.Model(&models.AcademyMaterial{}).
94
+ Select("COUNT(*)").
95
+ Where("academy_id = academy.id")
96
+
97
+ q := r.db.WithContext(ctx).
98
+ Model(&models.Academy{}).
99
+ Select("academy.*, (?) AS total_material", subQuery)
100
+
101
+ if req.Filter.HasKeyword() {
102
+ q = q.Where("title ILIKE ? OR description ILIKE ?", "%"+req.Filter.Keyword+"%", "%"+req.Filter.Keyword+"%")
103
+ }
104
+
105
+ if req.Filter.HasSort() {
106
+ q = q.Order(clause.OrderByColumn{
107
+ Column: clause.Column{Name: req.Filter.SortBy},
108
+ Desc: req.Filter.IsDesc(),
109
+ })
110
+ } else {
111
+ q = q.Order(`"order" ASC`)
112
+ }
113
+
114
+ // Menghitung jumlah total data tanpa offset dan limit
115
+ var totalData int64
116
+ if err := q.Count(&totalData).Error; err != nil {
117
+ return nil, nil, err
118
+ }
119
+
120
+ q = q.Offset(int(offset)).Limit(int(limit))
121
+
122
+ if err := q.Scan(&academies).Error; err != nil {
123
+ return nil, nil, err
124
+ }
125
+
126
+ pageInfo, err := models.NewPaging(req.Filter.CurrentPage, limit, offset, int(totalData))
127
+ if err != nil {
128
+ return nil, nil, err
129
+ }
130
+
131
+ return academies, pageInfo, nil
132
+ }
133
+
134
+ func (r *academyRepository) AdminDeleteAcademy(ctx context.Context, id int64) error {
135
+ return r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.Academy{}).Error
136
+ }
137
+
138
+ func (r *academyRepository) AdminReorderAcademy(ctx context.Context, req *models.ReorderAcademyRequest) error {
139
+ return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
140
+ for _, reorder := range req.Reorder {
141
+ if err := tx.Model(&models.Academy{}).Where("id = ?", reorder.ID).Update("order", reorder.Order).Error; err != nil {
142
+ return err
143
+ }
144
+ }
145
+ return nil
146
+ })
147
+ }
148
+
149
+ func (r *academyRepository) AdminGetLastOrderAcademy(ctx context.Context) (uint, error) {
150
+ var lastOrder uint
151
+ if err := r.db.WithContext(ctx).Model(&models.Academy{}).Order(`"order" DESC`).Limit(1).Select("order").Scan(&lastOrder).Error; err != nil {
152
+ return 0, err
153
+ }
154
+ return lastOrder, nil
155
+ }
156
+
157
+ func (r *academyRepository) AdminSaveAcademyMaterial(ctx context.Context, req *models.AcademyMaterial) error {
158
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
159
+ return err
160
+ }
161
+ return nil
162
+ }
163
+
164
+ func (r *academyRepository) AdminGetAcademyMaterialByID(ctx context.Context, id int64) (*models.AcademyMaterial, error) {
165
+ var academyMaterial models.AcademyMaterial
166
+ if err := r.db.WithContext(ctx).Where("id = ?", id).First(&academyMaterial).Error; err != nil {
167
+ return nil, err
168
+ }
169
+ return &academyMaterial, nil
170
+ }
171
+
172
+ func (r *academyRepository) AdminListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.AcademyMaterial, *models.Paging, error) {
173
+ academyMaterials := make([]models.AcademyMaterial, 0)
174
+
175
+ offset := req.Filter.GetOffset()
176
+ limit := req.Filter.GetLimit()
177
+
178
+ q := r.db.WithContext(ctx).Model(&models.AcademyMaterial{}).Where("academy_id = ?", req.AcademyID)
179
+
180
+ if req.Filter.HasKeyword() {
181
+ q = q.Where("title ILIKE ? OR description ILIKE ?", "%"+req.Filter.Keyword+"%", "%"+req.Filter.Keyword+"%")
182
+ }
183
+
184
+ if req.Filter.HasSort() {
185
+ q = q.Order(clause.OrderByColumn{
186
+ Column: clause.Column{Name: req.Filter.SortBy},
187
+ Desc: req.Filter.IsDesc(),
188
+ })
189
+ } else {
190
+ q = q.Order(`"order" ASC`)
191
+ }
192
+
193
+ // Menghitung jumlah total data tanpa offset dan limit
194
+ var totalData int64
195
+ if err := q.Count(&totalData).Error; err != nil {
196
+ return nil, nil, err
197
+ }
198
+
199
+ q = q.Offset(int(offset)).Limit(int(limit))
200
+
201
+ if err := q.Scan(&academyMaterials).Error; err != nil {
202
+ return nil, nil, err
203
+ }
204
+
205
+ pageInfo, err := models.NewPaging(req.Filter.CurrentPage, limit, offset, int(totalData))
206
+ if err != nil {
207
+ return nil, nil, err
208
+ }
209
+
210
+ return academyMaterials, pageInfo, nil
211
+ }
212
+
213
+ func (r *academyRepository) AdminDeleteAcademyMaterial(ctx context.Context, id int64) error {
214
+ return r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.AcademyMaterial{}).Error
215
+ }
216
+
217
+ func (r *academyRepository) AdminGetLastOrderAcademyMaterial(ctx context.Context) (uint, error) {
218
+ var lastOrder uint
219
+ if err := r.db.WithContext(ctx).Model(&models.AcademyMaterial{}).Order(`"order" DESC`).Limit(1).Select("order").Scan(&lastOrder).Error; err != nil {
220
+ return 0, err
221
+ }
222
+ return lastOrder, nil
223
+ }
224
+
225
+ func (r *academyRepository) AdminReorderAcademyMaterial(ctx context.Context, req *models.ReorderAcademyMaterialRequest) error {
226
+ return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
227
+ for _, reorder := range req.Reorder {
228
+ if err := tx.Model(&models.AcademyMaterial{}).Where("id = ? AND academy_id = ?", reorder.ID, req.AcademyID).Update("order", reorder.Order).Error; err != nil {
229
+ return err
230
+ }
231
+ }
232
+ return nil
233
+ })
234
+ }
235
+
236
+ // === USER ===
237
+
238
+ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error) {
239
+ academies := make([]models.UserAcademyResponse, 0)
240
+ offset := req.Filter.GetOffset()
241
+ limit := req.Filter.GetLimit()
242
+
243
+ // Base query
244
+ q := r.db.WithContext(ctx).
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")
252
+
253
+ if req.Filter.HasKeyword() {
254
+ q = q.Where("academy.title ILIKE ? OR academy.description ILIKE ?", "%"+req.Filter.Keyword+"%", "%"+req.Filter.Keyword+"%")
255
+ }
256
+
257
+ // Default ordering
258
+ q = q.Order(`academy."order" ASC`)
259
+
260
+ // Count total records (without limit/offset)
261
+ var totalData int64
262
+ countQuery := q.Session(&gorm.Session{NewDB: true}).Table("academy")
263
+ if err := countQuery.Count(&totalData).Error; err != nil {
264
+ return nil, nil, err
265
+ }
266
+
267
+ // Apply pagination
268
+ q = q.Offset(int(offset)).Limit(int(limit))
269
+
270
+ if err := q.Scan(&academies).Error; err != nil {
271
+ return nil, nil, err
272
+ }
273
+
274
+ // Calculate percentages
275
+ for i := range academies {
276
+ if academies[i].TotalMaterial > 0 {
277
+ academies[i].PercentageReadMaterial = float64(academies[i].TotalReadMaterial) / float64(academies[i].TotalMaterial) * 100
278
+ }
279
+ }
280
+
281
+ pageInfo, err := models.NewPaging(req.Filter.CurrentPage, limit, offset, int(totalData))
282
+ if err != nil {
283
+ return nil, nil, err
284
+ }
285
+
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()
292
+ limit := req.Filter.GetLimit()
293
+
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() {
308
+ q = q.Where("academy_materials.title ILIKE ?", "%"+req.Filter.Keyword+"%")
309
+ }
310
+
311
+ if req.Filter.HasSort() {
312
+ q = q.Order(clause.OrderByColumn{
313
+ Column: clause.Column{Name: req.Filter.SortBy},
314
+ Desc: req.Filter.IsDesc(),
315
+ })
316
+ } else {
317
+ q = q.Order("academy_materials.order ASC")
318
+ }
319
+
320
+ // Count total records (without limit/offset)
321
+ var total int64
322
+ if err := q.Count(&total).Error; err != nil {
323
+ return nil, nil, err
324
+ }
325
+
326
+ q = q.Offset(int(offset)).Limit(int(limit))
327
+
328
+ if err := q.Scan(&materials).Error; err != nil {
329
+ return nil, nil, err
330
+ }
331
+
332
+ paging, err := models.NewPaging(req.Filter.CurrentPage, limit, offset, int(total))
333
+ if err != nil {
334
+ return nil, nil, err
335
+ }
336
+
337
+ return materials, paging, nil
338
+ }
339
+
340
+ func (r *academyRepository) UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error) {
341
+ var academy models.AcademyResponse
342
+
343
+ subQuery := r.db.Model(&models.AcademyMaterial{}).
344
+ Select("COUNT(*)").
345
+ Where("academy_id = academy.id")
346
+
347
+ if err := r.db.WithContext(ctx).
348
+ Model(&models.Academy{}).
349
+ Select("academy.*, (?) AS total_material", subQuery).
350
+ Where("academy.slug = ?", slug).
351
+ Scan(&academy).Error; err != nil {
352
+ return nil, err
353
+ }
354
+
355
+ if academy.ID == 0 {
356
+ return nil, gorm.ErrRecordNotFound
357
+ }
358
+
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{}).
367
+ Select(`
368
+ academy_materials.*,
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
+
391
+ func (r *academyRepository) UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error {
392
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
393
+ return err
394
+ }
395
+ return nil
396
+ }
397
+
398
+ func (r *academyRepository) UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error) {
399
+ var academyMaterialProgress models.AcademyMaterialProgress
400
+ if err := r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).First(&academyMaterialProgress).Error; err != nil {
401
+ return nil, err
402
+ }
403
+ return &academyMaterialProgress, nil
404
+ }
405
+
406
+ func (r *academyRepository) UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error {
407
+ return r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).Delete(&models.AcademyMaterialProgress{}).Error
408
+ }
space/space/space/space/repositories/question_repository.go CHANGED
@@ -34,3 +34,14 @@ func GetAnswerByQuestionId(questionId uint) Repository[models.Answer, []models.A
34
  )
35
  return *repo
36
  }
 
 
 
 
 
 
 
 
 
 
 
 
34
  )
35
  return *repo
36
  }
37
+
38
+ func GetAcademyByID(id uint) Repository[models.Academy, models.Academy] {
39
+ repo := Construct[models.Academy, models.Academy](
40
+ models.Academy{ID: int64(id)},
41
+ )
42
+ repo.Transactions(
43
+ WhereGivenConstructor[models.Academy, models.Academy],
44
+ Find[models.Academy, models.Academy],
45
+ )
46
+ return *repo
47
+ }
space/space/space/space/response/api_response_v2.go CHANGED
@@ -1,11 +1,13 @@
1
  package response
2
 
3
  import (
 
 
 
 
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/pkg/validation"
6
  "api.qobiltu.id/utils"
7
- "errors"
8
- "net/http"
9
 
10
  "github.com/gin-gonic/gin"
11
  )
@@ -40,17 +42,44 @@ func HandleError(c *gin.Context, err error) {
40
  case exception.AttemptNotFound:
41
  responseError(c, http.StatusNotFound, exception)
42
  case exception.ValidationError:
43
- responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields) // Gunakan fungsi khusus untuk validasi
44
  default:
45
  responseError(c, http.StatusInternalServerError, exception)
46
  }
47
- } else {
48
- utils.LogError(err)
49
- responseError(c, http.StatusInternalServerError, models.Exception{
50
- InternalServerError: true,
51
- Message: "Internal Server Error",
 
 
 
 
 
 
 
 
 
52
  })
 
 
53
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
56
  func HandleSuccess(c *gin.Context, status int, msg string, data any, metaData any) {
@@ -62,7 +91,6 @@ func HandleSuccess(c *gin.Context, status int, msg string, data any, metaData an
62
  }
63
 
64
  c.JSON(status, res)
65
- return
66
  }
67
 
68
  func responseError(c *gin.Context, status int, exception models.Exception) {
@@ -76,7 +104,6 @@ func responseError(c *gin.Context, status int, exception models.Exception) {
76
  }
77
 
78
  c.AbortWithStatusJSON(status, res)
79
- return
80
  }
81
 
82
  func responseValidationError(c *gin.Context, status int, validationErrors []validation.ErrorMessage) {
@@ -90,5 +117,4 @@ func responseValidationError(c *gin.Context, status int, validationErrors []vali
90
  }
91
 
92
  c.AbortWithStatusJSON(status, res)
93
- return
94
  }
 
1
  package response
2
 
3
  import (
4
+ "encoding/json"
5
+ "errors"
6
+ "net/http"
7
+
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/pkg/validation"
10
  "api.qobiltu.id/utils"
 
 
11
 
12
  "github.com/gin-gonic/gin"
13
  )
 
42
  case exception.AttemptNotFound:
43
  responseError(c, http.StatusNotFound, exception)
44
  case exception.ValidationError:
45
+ responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
46
  default:
47
  responseError(c, http.StatusInternalServerError, exception)
48
  }
49
+
50
+ return
51
+ }
52
+
53
+ var (
54
+ jsonSyntaxError *json.SyntaxError
55
+ jsonUnmarshalTypeError *json.UnmarshalTypeError
56
+ )
57
+
58
+ if errors.As(err, &jsonSyntaxError) {
59
+ responseError(c, http.StatusBadRequest, models.Exception{
60
+ BadRequest: true,
61
+ Message: "Invalid JSON syntax",
62
+ Err: err,
63
  })
64
+
65
+ return
66
  }
67
+
68
+ if errors.As(err, &jsonUnmarshalTypeError) {
69
+ responseError(c, http.StatusBadRequest, models.Exception{
70
+ BadRequest: true,
71
+ Message: "Invalid body request",
72
+ Err: err,
73
+ })
74
+
75
+ return
76
+ }
77
+
78
+ utils.LogError(err)
79
+ responseError(c, http.StatusInternalServerError, models.Exception{
80
+ InternalServerError: true,
81
+ Message: "Internal Server Error",
82
+ })
83
  }
84
 
85
  func HandleSuccess(c *gin.Context, status int, msg string, data any, metaData any) {
 
91
  }
92
 
93
  c.JSON(status, res)
 
94
  }
95
 
96
  func responseError(c *gin.Context, status int, exception models.Exception) {
 
104
  }
105
 
106
  c.AbortWithStatusJSON(status, res)
 
107
  }
108
 
109
  func responseValidationError(c *gin.Context, status int, validationErrors []validation.ErrorMessage) {
 
117
  }
118
 
119
  c.AbortWithStatusJSON(status, res)
 
120
  }
space/space/space/space/router/academy_route.go CHANGED
@@ -1,17 +1,34 @@
1
  package router
2
 
3
  import (
4
- AcademyController "api.qobiltu.id/controller/academy"
5
  "api.qobiltu.id/middleware"
6
- "github.com/gin-gonic/gin"
7
  )
8
 
9
- func AcademyRoute(router *gin.Engine) {
10
- routerGroup := router.Group("/api/v1/academy")
11
  {
12
- routerGroup.GET("/:slug", middleware.AuthUser, AcademyController.MaterialsList)
13
- routerGroup.GET("/:slug/:slug_material", middleware.AuthUser, AcademyController.ContentList)
14
- routerGroup.GET("/list", middleware.AuthUser, AcademyController.List)
15
- routerGroup.POST("/create", middleware.AuthUser, AcademyController.Create)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
  }
 
1
  package router
2
 
3
  import (
 
4
  "api.qobiltu.id/middleware"
 
5
  )
6
 
7
+ func (s *Server) AcademyRoute() {
8
+ adminRouterGroup := s.router.Group("/api/v1/admin/academy")
9
  {
10
+ // TODO: add role checker middleware
11
+
12
+ adminRouterGroup.POST("", middleware.AuthUser, s.academyController.AdminCreateAcademy)
13
+ adminRouterGroup.GET("", middleware.AuthUser, s.academyController.AdminListAcademy)
14
+ adminRouterGroup.PUT("/reorder", middleware.AuthUser, s.academyController.AdminReorderAcademy)
15
+ adminRouterGroup.GET("/:id", middleware.AuthUser, s.academyController.AdminGetAcademy)
16
+ adminRouterGroup.PUT("/:id", middleware.AuthUser, s.academyController.AdminUpdateAcademy)
17
+ adminRouterGroup.DELETE("/:id", middleware.AuthUser, s.academyController.AdminDeleteAcademy)
18
+
19
+ adminRouterGroup.POST("/:id/materials", middleware.AuthUser, s.academyController.AdminCreateAcademyMaterial)
20
+ adminRouterGroup.GET("/:id/materials", middleware.AuthUser, s.academyController.AdminListAcademyMaterial)
21
+ adminRouterGroup.PUT("/:id/materials/reorder", middleware.AuthUser, s.academyController.AdminReorderAcademyMaterial)
22
+ adminRouterGroup.GET("/:id/materials/:materialId", middleware.AuthUser, s.academyController.AdminGetAcademyMaterial)
23
+ adminRouterGroup.PUT("/:id/materials/:materialId", middleware.AuthUser, s.academyController.AdminUpdateAcademyMaterial)
24
+ adminRouterGroup.DELETE("/:id/materials/:materialId", middleware.AuthUser, s.academyController.AdminDeleteAcademyMaterial)
25
+ }
26
+
27
+ userRouterGroup := s.router.Group("/api/v1/academy")
28
+ {
29
+ userRouterGroup.GET("", middleware.AuthUser, s.academyController.UserListAcademy)
30
+ userRouterGroup.GET("/:slug/materials", middleware.AuthUser, s.academyController.UserListAcademyMaterial)
31
+ userRouterGroup.GET("/:slug/materials/:materialSlug", middleware.AuthUser, s.academyController.UserGetAcademyMaterialBySlug)
32
+ userRouterGroup.POST("/:slug/materials/:materialSlug/toggle-read", middleware.AuthUser, s.academyController.UserToggleAcademyMaterialProgress)
33
  }
34
  }
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
- AcademyRoute(s.router)
14
  QuizRoute(s.router)
15
 
16
  s.OptionsRoute()
17
  s.EmailRoute()
 
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
 
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()
space/space/space/space/router/server.go CHANGED
@@ -1,6 +1,7 @@
1
  package router
2
 
3
  import (
 
4
  cv_controller "api.qobiltu.id/controller/cv"
5
  email_controller "api.qobiltu.id/controller/email"
6
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
@@ -15,6 +16,7 @@ type Server struct {
15
  regionController region_controller.RegionController
16
  optionsController options_controller.OptionsController
17
  emailController email_controller.EmailController
 
18
  cvController cv_controller.CVController
19
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
20
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
@@ -24,6 +26,7 @@ func NewServer(
24
  regionController region_controller.RegionController,
25
  optionsController options_controller.OptionsController,
26
  emailController email_controller.EmailController,
 
27
  cvController cv_controller.CVController,
28
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
29
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
@@ -36,6 +39,7 @@ func NewServer(
36
  regionController: regionController,
37
  optionsController: optionsController,
38
  emailController: emailController,
 
39
  cvController: cvController,
40
  marriageReadinessProfileController: marriageReadinessProfileController,
41
  partnerCriteriaController: partnerCriteriaController,
 
1
  package router
2
 
3
  import (
4
+ academy_controller "api.qobiltu.id/controller/academy"
5
  cv_controller "api.qobiltu.id/controller/cv"
6
  email_controller "api.qobiltu.id/controller/email"
7
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
 
16
  regionController region_controller.RegionController
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
 
26
  regionController region_controller.RegionController,
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,
 
39
  regionController: regionController,
40
  optionsController: optionsController,
41
  emailController: emailController,
42
+ academyController: academyController,
43
  cvController: cvController,
44
  marriageReadinessProfileController: marriageReadinessProfileController,
45
  partnerCriteriaController: partnerCriteriaController,
space/space/space/space/services/academy_quiz_service.go CHANGED
@@ -32,7 +32,7 @@ func AuthorizeQuizwithAcademy(s *AttemptQuizService, next func()) {
32
  s.Exception.Message = "There is no academy with given slug!"
33
  return
34
  }
35
- quizRepo := repositories.GetQuizbyAcademyId(academyRepo.Result.ID)
36
  s.Error = quizRepo.RowsError
37
  if quizRepo.NoRecord {
38
  s.Exception.DataNotFound = true
@@ -173,7 +173,7 @@ func (s *SubmitQuizService) Create() {
173
  }
174
 
175
  func (s *QuizListService) Retrieve() {
176
- quizRepo := repositories.GetQuizbyAcademyId(s.Constructor.ID)
177
  s.Error = quizRepo.RowsError
178
  if quizRepo.NoRecord {
179
  s.Exception.DataNotFound = true
 
32
  s.Exception.Message = "There is no academy with given slug!"
33
  return
34
  }
35
+ quizRepo := repositories.GetQuizbyAcademyId(uint(academyRepo.Result.ID))
36
  s.Error = quizRepo.RowsError
37
  if quizRepo.NoRecord {
38
  s.Exception.DataNotFound = true
 
173
  }
174
 
175
  func (s *QuizListService) Retrieve() {
176
+ quizRepo := repositories.GetQuizbyAcademyId(uint(s.Constructor.ID))
177
  s.Error = quizRepo.RowsError
178
  if quizRepo.NoRecord {
179
  s.Exception.DataNotFound = true
space/space/space/space/services/academy_service.go CHANGED
@@ -1,150 +1,316 @@
1
- package services
2
-
3
- import (
4
- "errors"
5
-
6
- "api.qobiltu.id/models"
7
- "api.qobiltu.id/repositories"
8
- "github.com/gosimple/slug"
9
- uuid "github.com/satori/go.uuid"
10
- )
11
-
12
- type AcademyService struct {
13
- Service[models.Academy, []models.Academy]
14
- }
15
-
16
- type CreateAcademyService struct {
17
- Service[models.AllAcademyResponse, models.AllAcademyResponse]
18
- }
19
-
20
- type AcademyMaterialService struct {
21
- Service[models.Academy, []models.AcademyMaterial]
22
- }
23
-
24
- type AcademyContentService struct {
25
- Service[models.AcademyMaterial, models.AcademyContent]
26
- }
27
-
28
- type AcademyMarkReadService struct {
29
- Service[models.AcademyMaterial, models.Academy]
30
- }
31
-
32
- func castAcademyMaterials(academyId uint) []models.AcademyMaterialResponse {
33
- var ArrMaterials []models.AcademyMaterialResponse
34
- academyMaterialsRepo := repositories.GetAllAcademyMaterialsByAcademyID(academyId)
35
- for _, academyMaterial := range academyMaterialsRepo.Result {
36
- ArrMaterials = append(ArrMaterials, models.AcademyMaterialResponse{
37
- Materials: academyMaterial,
38
- Contents: repositories.GetAllAcademyContentsByMaterialID(academyMaterial.ID).Result,
39
- })
40
- }
41
- return ArrMaterials
42
- }
43
- func (s *AcademyService) Retrieve() {
44
- if s.Constructor.Slug != "" {
45
- AcademyRepo := repositories.GetAcademyDataBySlug(s.Constructor.Slug)
46
- s.Error = AcademyRepo.RowsError
47
- if AcademyRepo.NoRecord {
48
- s.Exception.Message = "Academy not found"
49
- s.Exception.DataNotFound = true
50
- return
51
- }
52
-
53
- s.Result = []models.Academy{
54
- AcademyRepo.Result,
55
- }
56
- } else {
57
- AcademyRepo := repositories.GetAllAcademy()
58
- s.Error = AcademyRepo.RowsError
59
- s.Result = AcademyRepo.Result
60
- }
61
-
62
- }
63
-
64
- func (s *CreateAcademyService) Create() {
65
- var ArrAcademy []models.AcademyResponse
66
- for _, academy := range s.Constructor.Academies {
67
- academy.Academy.UUID = uuid.NewV4()
68
- if academy.Academy.Slug == "" {
69
- academy.Academy.Slug = slug.Make(academy.Academy.Title)
70
- }
71
-
72
- createdAcademy := repositories.CreateAcademy(academy.Academy)
73
- if createdAcademy.RowsError != nil {
74
- s.Error = createdAcademy.RowsError
75
- return
76
- }
77
- for _, material := range academy.Materials {
78
- material.Materials.AcademyID = createdAcademy.Result.ID
79
- material.Materials.UUID = uuid.NewV4()
80
- if material.Materials.Slug == "" {
81
- material.Materials.Slug = slug.Make(material.Materials.Title)
82
- }
83
- createdMaterial := repositories.CreateAcademyMaterial(material.Materials)
84
- if createdMaterial.RowsError != nil {
85
- s.Error = createdMaterial.RowsError
86
- return
87
- }
88
- for _, content := range []models.AcademyContent{material.Contents} {
89
- content.UUID = uuid.NewV4()
90
- content.AcademyMaterialID = createdMaterial.Result.ID
91
- createdContent := repositories.CreateAcademyContent(content)
92
- if createdContent.RowsError != nil {
93
- s.Error = createdContent.RowsError
94
- return
95
- }
96
- ArrAcademy = append(ArrAcademy, models.AcademyResponse{
97
- Academy: createdAcademy.Result,
98
- Materials: castAcademyMaterials(createdAcademy.Result.ID),
99
- })
100
- }
101
- }
102
- }
103
- s.Result = models.AllAcademyResponse{Academies: ArrAcademy}
104
-
105
- }
106
-
107
- func (s *AcademyMaterialService) Retrieve() {
108
- academyRepo := repositories.GetAcademyDataBySlug(s.Constructor.Slug)
109
- if academyRepo.NoRecord {
110
- s.Exception.DataNotFound = true
111
- s.Exception.Message = "There is no Academy found with given Slug!"
112
- return
113
- }
114
- s.Error = academyRepo.RowsError
115
- academyMaterialRepo := repositories.GetAllAcademyMaterialsByAcademyID(academyRepo.Result.ID)
116
- s.Error = errors.Join(s.Error, academyMaterialRepo.RowsError)
117
- if academyMaterialRepo.NoRecord {
118
- s.Exception.DataNotFound = true
119
- s.Exception.Message = "There is no Academy Material with given ID"
120
- return
121
- }
122
- s.Result = academyMaterialRepo.Result
123
- }
124
-
125
- func (s *AcademyContentService) Retrieve() {
126
- academyMaterialRepo := repositories.GetAcademyMaterialBySlug(s.Constructor.Slug)
127
- s.Error = academyMaterialRepo.RowsError
128
- if academyMaterialRepo.NoRecord {
129
- s.Exception.DataNotFound = true
130
- s.Exception.Message = "There is no Academy Material with given Slug"
131
- return
132
- }
133
- academyContentRepo := repositories.GetAllAcademyContentsByMaterialID(academyMaterialRepo.Result.ID)
134
- s.Error = errors.Join(s.Error, academyContentRepo.RowsError)
135
- if academyContentRepo.NoRecord {
136
- s.Exception.DataNotFound = true
137
- s.Exception.Message = "There is no Academy Contents with given Material ID"
138
- return
139
- }
140
- s.Result = academyContentRepo.Result
141
- }
142
-
143
- func (s *AcademyMarkReadService) Update() {
144
- markReadRepo := repositories.UpdateAcademyMaterialCompletedById(s.Constructor.ID)
145
- if markReadRepo.NoRecord {
146
- s.Exception.DataNotFound = true
147
- s.Error = markReadRepo.RowsError
148
- return
149
- }
150
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
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
+ "github.com/gosimple/slug"
13
+ "gorm.io/gorm"
14
+ )
15
+
16
+ type AcademyService interface {
17
+ // === ADMIN ===
18
+ AdminCreateAcademy(ctx context.Context, req *models.CreateAcademyRequest) (*models.AcademyResponse, error)
19
+ AdminUpdateAcademy(ctx context.Context, req *models.UpdateAcademyRequest) (*models.AcademyResponse, error)
20
+ AdminGetAcademyByID(ctx context.Context, id int64) (*models.AcademyResponse, error)
21
+ AdminListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.AcademyResponse, *models.Paging, error)
22
+ AdminDeleteAcademy(ctx context.Context, id int64) error
23
+ AdminReorderAcademy(ctx context.Context, req *models.ReorderAcademyRequest) error
24
+
25
+ AdminCreateAcademyMaterial(ctx context.Context, req *models.CreateAcademyMaterialRequest) (*models.AcademyMaterial, error)
26
+ AdminUpdateAcademyMaterial(ctx context.Context, req *models.UpdateAcademyMaterialRequest) (*models.AcademyMaterial, error)
27
+ AdminGetAcademyMaterialByID(ctx context.Context, id int64) (*models.AcademyMaterial, error)
28
+ AdminListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.AcademyMaterial, *models.Paging, error)
29
+ AdminDeleteAcademyMaterial(ctx context.Context, id int64) error
30
+ AdminReorderAcademyMaterial(ctx context.Context, req *models.ReorderAcademyMaterialRequest) error
31
+
32
+ // === USER ===
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
+
40
+ type academyService struct {
41
+ academyRepository repositories.AcademyRepository
42
+ validator *validation.Validator
43
+ }
44
+
45
+ func NewAcademyService(academyRepository repositories.AcademyRepository, validator *validation.Validator) AcademyService {
46
+ return &academyService{
47
+ academyRepository: academyRepository,
48
+ validator: validator,
49
+ }
50
+ }
51
+
52
+ func (s *academyService) AdminCreateAcademy(ctx context.Context, req *models.CreateAcademyRequest) (*models.AcademyResponse, error) {
53
+ if err := s.validator.Validate(req); err != nil {
54
+ return nil, response.HandleValidationError(err)
55
+ }
56
+
57
+ academy := &models.Academy{
58
+ Title: req.Title,
59
+ Slug: slug.Make(req.Title),
60
+ Description: req.Description,
61
+ }
62
+
63
+ if req.Order == nil {
64
+ lastOrder, err := s.academyRepository.AdminGetLastOrderAcademy(ctx)
65
+ if err != nil {
66
+ return nil, err
67
+ }
68
+
69
+ academy.Order = lastOrder + 1
70
+ } else {
71
+ academy.Order = *req.Order
72
+ }
73
+
74
+ res, err := s.academyRepository.AdminSaveAcademy(ctx, academy)
75
+ if err != nil {
76
+ return nil, response.HandleGormError(err, "Internal Server Error")
77
+ }
78
+
79
+ return res, nil
80
+ }
81
+
82
+ func (s *academyService) AdminUpdateAcademy(ctx context.Context, req *models.UpdateAcademyRequest) (*models.AcademyResponse, error) {
83
+ if err := s.validator.Validate(req); err != nil {
84
+ return nil, response.HandleValidationError(err)
85
+ }
86
+
87
+ academy, err := s.academyRepository.AdminGetAcademyByID(ctx, req.ID)
88
+ if err != nil {
89
+ return nil, response.HandleGormError(err, "Internal Server Error")
90
+ }
91
+
92
+ if req.Title != nil {
93
+ academy.Title = *req.Title
94
+ academy.Slug = slug.Make(*req.Title)
95
+ }
96
+
97
+ if req.Description != nil {
98
+ academy.Description = *req.Description
99
+ }
100
+
101
+ if req.Order != nil {
102
+ academy.Order = *req.Order
103
+ }
104
+
105
+ fmt.Println("lolos")
106
+ res, err := s.academyRepository.AdminSaveAcademy(ctx, &academy.Academy)
107
+ if err != nil {
108
+ return nil, err
109
+ }
110
+
111
+ return res, nil
112
+ }
113
+
114
+ func (s *academyService) AdminGetAcademyByID(ctx context.Context, id int64) (*models.AcademyResponse, error) {
115
+ academy, err := s.academyRepository.AdminGetAcademyByID(ctx, id)
116
+ if err != nil {
117
+ return nil, response.HandleGormError(err, "Internal Server Error")
118
+ }
119
+
120
+ return academy, nil
121
+ }
122
+
123
+ func (s *academyService) AdminListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.AcademyResponse, *models.Paging, error) {
124
+ academies, paging, err := s.academyRepository.AdminListAcademy(ctx, req)
125
+ if err != nil {
126
+ return nil, nil, response.HandleGormError(err, "Internal Server Error")
127
+ }
128
+
129
+ return academies, paging, nil
130
+ }
131
+
132
+ func (s *academyService) AdminDeleteAcademy(ctx context.Context, id int64) error {
133
+ err := s.academyRepository.AdminDeleteAcademy(ctx, id)
134
+ if err != nil {
135
+ return response.HandleGormError(err, "Internal Server Error")
136
+ }
137
+
138
+ return nil
139
+ }
140
+
141
+ func (s *academyService) AdminReorderAcademy(ctx context.Context, req *models.ReorderAcademyRequest) error {
142
+ err := s.academyRepository.AdminReorderAcademy(ctx, req)
143
+ if err != nil {
144
+ return response.HandleGormError(err, "Internal Server Error")
145
+ }
146
+
147
+ return nil
148
+ }
149
+
150
+ func (s *academyService) AdminCreateAcademyMaterial(ctx context.Context, req *models.CreateAcademyMaterialRequest) (*models.AcademyMaterial, error) {
151
+ if err := s.validator.Validate(req); err != nil {
152
+ return nil, response.HandleValidationError(err)
153
+ }
154
+
155
+ academy, err := s.academyRepository.AdminGetAcademyByID(ctx, req.AcademyID)
156
+ if err != nil {
157
+ return nil, response.HandleGormError(gorm.ErrRecordNotFound, "Internal Server Error")
158
+ }
159
+
160
+ academyMaterial := &models.AcademyMaterial{
161
+ AcademyID: academy.ID,
162
+ Title: req.Title,
163
+ Slug: slug.Make(req.Title),
164
+ Content: req.Content,
165
+ }
166
+
167
+ if req.Order == nil {
168
+ lastOrder, err := s.academyRepository.AdminGetLastOrderAcademyMaterial(ctx)
169
+ if err != nil {
170
+ return nil, err
171
+ }
172
+
173
+ academyMaterial.Order = lastOrder + 1
174
+ } else {
175
+ academyMaterial.Order = *req.Order
176
+ }
177
+
178
+ if err := s.academyRepository.AdminSaveAcademyMaterial(ctx, academyMaterial); err != nil {
179
+ return nil, response.HandleGormError(err, "Internal Server Error")
180
+ }
181
+
182
+ return academyMaterial, nil
183
+ }
184
+
185
+ func (s *academyService) AdminUpdateAcademyMaterial(ctx context.Context, req *models.UpdateAcademyMaterialRequest) (*models.AcademyMaterial, error) {
186
+ if err := s.validator.Validate(req); err != nil {
187
+ return nil, response.HandleValidationError(err)
188
+ }
189
+
190
+ academyMaterial, err := s.academyRepository.AdminGetAcademyMaterialByID(ctx, req.ID)
191
+ if err != nil {
192
+ return nil, response.HandleGormError(err, "Internal Server Error")
193
+ }
194
+
195
+ if req.Title != nil {
196
+ academyMaterial.Title = *req.Title
197
+ academyMaterial.Slug = slug.Make(*req.Title)
198
+ }
199
+
200
+ if req.Content != nil {
201
+ academyMaterial.Content = *req.Content
202
+ }
203
+
204
+ if req.Order != nil {
205
+ academyMaterial.Order = *req.Order
206
+ }
207
+
208
+ if err := s.academyRepository.AdminSaveAcademyMaterial(ctx, academyMaterial); err != nil {
209
+ return nil, err
210
+ }
211
+
212
+ return academyMaterial, nil
213
+ }
214
+
215
+ func (s *academyService) AdminGetAcademyMaterialByID(ctx context.Context, id int64) (*models.AcademyMaterial, error) {
216
+ academyMaterial, err := s.academyRepository.AdminGetAcademyMaterialByID(ctx, id)
217
+ if err != nil {
218
+ return nil, response.HandleGormError(err, "Internal Server Error")
219
+ }
220
+
221
+ return academyMaterial, nil
222
+ }
223
+
224
+ func (s *academyService) AdminListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.AcademyMaterial, *models.Paging, error) {
225
+ academyMaterials, paging, err := s.academyRepository.AdminListAcademyMaterial(ctx, req)
226
+ if err != nil {
227
+ return nil, nil, response.HandleGormError(err, "Internal Server Error")
228
+ }
229
+
230
+ return academyMaterials, paging, nil
231
+ }
232
+
233
+ func (s *academyService) AdminDeleteAcademyMaterial(ctx context.Context, id int64) error {
234
+ err := s.academyRepository.AdminDeleteAcademyMaterial(ctx, id)
235
+ if err != nil {
236
+ return response.HandleGormError(err, "Internal Server Error")
237
+ }
238
+
239
+ return nil
240
+ }
241
+
242
+ func (s *academyService) AdminReorderAcademyMaterial(ctx context.Context, req *models.ReorderAcademyMaterialRequest) error {
243
+ err := s.academyRepository.AdminReorderAcademyMaterial(ctx, req)
244
+ if err != nil {
245
+ return response.HandleGormError(err, "Internal Server Error")
246
+ }
247
+
248
+ return nil
249
+ }
250
+
251
+ // === USER ===
252
+
253
+ func (s *academyService) UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error) {
254
+ academies, paging, err := s.academyRepository.UserListAcademy(ctx, req)
255
+ if err != nil {
256
+ return nil, nil, response.HandleGormError(err, "Internal Server Error")
257
+ }
258
+
259
+ return academies, paging, nil
260
+ }
261
+
262
+ func (s *academyService) UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error) {
263
+ academyMaterials, paging, err := s.academyRepository.UserListAcademyMaterial(ctx, req)
264
+ if err != nil {
265
+ return nil, nil, response.HandleGormError(err, "Internal Server Error")
266
+ }
267
+
268
+ return academyMaterials, paging, nil
269
+ }
270
+
271
+ func (s *academyService) UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error) {
272
+ academy, err := s.academyRepository.UserGetAcademyBySlug(ctx, slug)
273
+ if err != nil {
274
+ return nil, response.HandleGormError(err, "Internal Server Error")
275
+ }
276
+
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")
284
+ }
285
+
286
+ return academyMaterial, nil
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
316
+ }
space/space/space/space/space/space/assets/efs.go CHANGED
@@ -11,3 +11,11 @@ const (
11
  EmailConfirmationTemplatePath = "emails/email-confirmation.tmpl"
12
  EmailForgotPasswordTemplatePath = "emails/email-forgot-password.tmpl"
13
  )
 
 
 
 
 
 
 
 
 
11
  EmailConfirmationTemplatePath = "emails/email-confirmation.tmpl"
12
  EmailForgotPasswordTemplatePath = "emails/email-forgot-password.tmpl"
13
  )
14
+
15
+ var (
16
+ //go:embed "seeds/province.json"
17
+ EmbeddedProvinceJSON []byte
18
+
19
+ //go:embed "seeds/city.json"
20
+ EmbeddedCityJSON []byte
21
+ )
space/space/space/space/space/space/assets/seeds/city.json ADDED
@@ -0,0 +1,4114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": 1,
4
+ "type": "Kabupaten",
5
+ "name": "Aceh Barat",
6
+ "code": "05",
7
+ "full_code": "1105",
8
+ "province_id": 1
9
+ },
10
+ {
11
+ "id": 2,
12
+ "type": "Kabupaten",
13
+ "name": "Aceh Barat Daya",
14
+ "code": "12",
15
+ "full_code": "1112",
16
+ "province_id": 1
17
+ },
18
+ {
19
+ "id": 3,
20
+ "type": "Kabupaten",
21
+ "name": "Sabu Raijua",
22
+ "code": "20",
23
+ "full_code": "5320",
24
+ "province_id": 23
25
+ },
26
+ {
27
+ "id": 4,
28
+ "type": "Kota",
29
+ "name": "Salatiga",
30
+ "code": "73",
31
+ "full_code": "3373",
32
+ "province_id": 10
33
+ },
34
+ {
35
+ "id": 5,
36
+ "type": "Kabupaten",
37
+ "name": "Aceh Besar",
38
+ "code": "06",
39
+ "full_code": "1106",
40
+ "province_id": 1
41
+ },
42
+ {
43
+ "id": 6,
44
+ "type": "Kabupaten",
45
+ "name": "Aceh Jaya",
46
+ "code": "14",
47
+ "full_code": "1114",
48
+ "province_id": 1
49
+ },
50
+ {
51
+ "id": 7,
52
+ "type": "Kabupaten",
53
+ "name": "Aceh Selatan",
54
+ "code": "01",
55
+ "full_code": "1101",
56
+ "province_id": 1
57
+ },
58
+ {
59
+ "id": 8,
60
+ "type": "Kabupaten",
61
+ "name": "Aceh Singkil",
62
+ "code": "10",
63
+ "full_code": "1110",
64
+ "province_id": 1
65
+ },
66
+ {
67
+ "id": 9,
68
+ "type": "Kabupaten",
69
+ "name": "Aceh Tamiang",
70
+ "code": "16",
71
+ "full_code": "1116",
72
+ "province_id": 1
73
+ },
74
+ {
75
+ "id": 10,
76
+ "type": "Kabupaten",
77
+ "name": "Aceh Tengah",
78
+ "code": "04",
79
+ "full_code": "1104",
80
+ "province_id": 1
81
+ },
82
+ {
83
+ "id": 11,
84
+ "type": "Kabupaten",
85
+ "name": "Aceh Tenggara",
86
+ "code": "02",
87
+ "full_code": "1102",
88
+ "province_id": 1
89
+ },
90
+ {
91
+ "id": 12,
92
+ "type": "Kabupaten",
93
+ "name": "Aceh Timur",
94
+ "code": "03",
95
+ "full_code": "1103",
96
+ "province_id": 1
97
+ },
98
+ {
99
+ "id": 13,
100
+ "type": "Kabupaten",
101
+ "name": "Gorontalo Utara",
102
+ "code": "05",
103
+ "full_code": "7505",
104
+ "province_id": 7
105
+ },
106
+ {
107
+ "id": 14,
108
+ "type": "Kabupaten",
109
+ "name": "Aceh Utara",
110
+ "code": "08",
111
+ "full_code": "1108",
112
+ "province_id": 1
113
+ },
114
+ {
115
+ "id": 15,
116
+ "type": "Kabupaten",
117
+ "name": "Agam",
118
+ "code": "06",
119
+ "full_code": "1306",
120
+ "province_id": 36
121
+ },
122
+ {
123
+ "id": 16,
124
+ "type": "Kabupaten",
125
+ "name": "Alor",
126
+ "code": "05",
127
+ "full_code": "5305",
128
+ "province_id": 23
129
+ },
130
+ {
131
+ "id": 17,
132
+ "type": "Kota",
133
+ "name": "Ambon",
134
+ "code": "71",
135
+ "full_code": "8171",
136
+ "province_id": 20
137
+ },
138
+ {
139
+ "id": 18,
140
+ "type": "Kabupaten",
141
+ "name": "Gowa",
142
+ "code": "06",
143
+ "full_code": "7306",
144
+ "province_id": 32
145
+ },
146
+ {
147
+ "id": 19,
148
+ "type": "Kota",
149
+ "name": "Samarinda",
150
+ "code": "72",
151
+ "full_code": "6472",
152
+ "province_id": 15
153
+ },
154
+ {
155
+ "id": 20,
156
+ "type": "Kabupaten",
157
+ "name": "Asahan",
158
+ "code": "09",
159
+ "full_code": "1209",
160
+ "province_id": 38
161
+ },
162
+ {
163
+ "id": 21,
164
+ "type": "Kabupaten",
165
+ "name": "Sambas",
166
+ "code": "01",
167
+ "full_code": "6101",
168
+ "province_id": 12
169
+ },
170
+ {
171
+ "id": 22,
172
+ "type": "Kabupaten",
173
+ "name": "Asmat",
174
+ "code": "04",
175
+ "full_code": "9304",
176
+ "province_id": 28
177
+ },
178
+ {
179
+ "id": 23,
180
+ "type": "Kabupaten",
181
+ "name": "Samosir",
182
+ "code": "17",
183
+ "full_code": "1217",
184
+ "province_id": 38
185
+ },
186
+ {
187
+ "id": 24,
188
+ "type": "Kabupaten",
189
+ "name": "Badung",
190
+ "code": "03",
191
+ "full_code": "5103",
192
+ "province_id": 2
193
+ },
194
+ {
195
+ "id": 25,
196
+ "type": "Kabupaten",
197
+ "name": "Sampang",
198
+ "code": "27",
199
+ "full_code": "3527",
200
+ "province_id": 11
201
+ },
202
+ {
203
+ "id": 26,
204
+ "type": "Kabupaten",
205
+ "name": "Balangan",
206
+ "code": "11",
207
+ "full_code": "6311",
208
+ "province_id": 13
209
+ },
210
+ {
211
+ "id": 27,
212
+ "type": "Kabupaten",
213
+ "name": "Sanggau",
214
+ "code": "03",
215
+ "full_code": "6103",
216
+ "province_id": 12
217
+ },
218
+ {
219
+ "id": 28,
220
+ "type": "Kabupaten",
221
+ "name": "Sarmi",
222
+ "code": "10",
223
+ "full_code": "9110",
224
+ "province_id": 24
225
+ },
226
+ {
227
+ "id": 29,
228
+ "type": "Kabupaten",
229
+ "name": "Gresik",
230
+ "code": "25",
231
+ "full_code": "3525",
232
+ "province_id": 11
233
+ },
234
+ {
235
+ "id": 30,
236
+ "type": "Kabupaten",
237
+ "name": "Grobogan",
238
+ "code": "15",
239
+ "full_code": "3315",
240
+ "province_id": 10
241
+ },
242
+ {
243
+ "id": 31,
244
+ "type": "Kabupaten",
245
+ "name": "Gunung Mas",
246
+ "code": "10",
247
+ "full_code": "6210",
248
+ "province_id": 14
249
+ },
250
+ {
251
+ "id": 32,
252
+ "type": "Kabupaten",
253
+ "name": "Gunungkidul",
254
+ "code": "03",
255
+ "full_code": "3403",
256
+ "province_id": 5
257
+ },
258
+ {
259
+ "id": 33,
260
+ "type": "Kabupaten",
261
+ "name": "Sarolangun",
262
+ "code": "03",
263
+ "full_code": "1503",
264
+ "province_id": 8
265
+ },
266
+ {
267
+ "id": 34,
268
+ "type": "Kota",
269
+ "name": "Gunungsitoli",
270
+ "code": "78",
271
+ "full_code": "1278",
272
+ "province_id": 38
273
+ },
274
+ {
275
+ "id": 35,
276
+ "type": "Kota",
277
+ "name": "Sawahlunto",
278
+ "code": "73",
279
+ "full_code": "1373",
280
+ "province_id": 36
281
+ },
282
+ {
283
+ "id": 36,
284
+ "type": "Kabupaten",
285
+ "name": "Halmahera Barat",
286
+ "code": "01",
287
+ "full_code": "8201",
288
+ "province_id": 21
289
+ },
290
+ {
291
+ "id": 37,
292
+ "type": "Kabupaten",
293
+ "name": "Sekadau",
294
+ "code": "09",
295
+ "full_code": "6109",
296
+ "province_id": 12
297
+ },
298
+ {
299
+ "id": 38,
300
+ "type": "Kabupaten",
301
+ "name": "Halmahera Selatan",
302
+ "code": "04",
303
+ "full_code": "8204",
304
+ "province_id": 21
305
+ },
306
+ {
307
+ "id": 39,
308
+ "type": "Kabupaten",
309
+ "name": "Seluma",
310
+ "code": "05",
311
+ "full_code": "1705",
312
+ "province_id": 4
313
+ },
314
+ {
315
+ "id": 40,
316
+ "type": "Kabupaten",
317
+ "name": "Halmahera Tengah",
318
+ "code": "02",
319
+ "full_code": "8202",
320
+ "province_id": 21
321
+ },
322
+ {
323
+ "id": 41,
324
+ "type": "Kabupaten",
325
+ "name": "Semarang",
326
+ "code": "22",
327
+ "full_code": "3322",
328
+ "province_id": 10
329
+ },
330
+ {
331
+ "id": 42,
332
+ "type": "Kota",
333
+ "name": "Semarang",
334
+ "code": "74",
335
+ "full_code": "3374",
336
+ "province_id": 10
337
+ },
338
+ {
339
+ "id": 43,
340
+ "type": "Kabupaten",
341
+ "name": "Halmahera Timur",
342
+ "code": "06",
343
+ "full_code": "8206",
344
+ "province_id": 21
345
+ },
346
+ {
347
+ "id": 44,
348
+ "type": "Kabupaten",
349
+ "name": "Seram Bagian Barat",
350
+ "code": "06",
351
+ "full_code": "8106",
352
+ "province_id": 20
353
+ },
354
+ {
355
+ "id": 45,
356
+ "type": "Kabupaten",
357
+ "name": "Halmahera Utara",
358
+ "code": "03",
359
+ "full_code": "8203",
360
+ "province_id": 21
361
+ },
362
+ {
363
+ "id": 46,
364
+ "type": "Kabupaten",
365
+ "name": "Seram Bagian Timur",
366
+ "code": "05",
367
+ "full_code": "8105",
368
+ "province_id": 20
369
+ },
370
+ {
371
+ "id": 47,
372
+ "type": "Kabupaten",
373
+ "name": "Hulu Sungai Selatan",
374
+ "code": "06",
375
+ "full_code": "6306",
376
+ "province_id": 13
377
+ },
378
+ {
379
+ "id": 48,
380
+ "type": "Kabupaten",
381
+ "name": "Hulu Sungai Tengah",
382
+ "code": "07",
383
+ "full_code": "6307",
384
+ "province_id": 13
385
+ },
386
+ {
387
+ "id": 49,
388
+ "type": "Kabupaten",
389
+ "name": "Hulu Sungai Utara",
390
+ "code": "08",
391
+ "full_code": "6308",
392
+ "province_id": 13
393
+ },
394
+ {
395
+ "id": 50,
396
+ "type": "Kota",
397
+ "name": "Serang",
398
+ "code": "73",
399
+ "full_code": "3673",
400
+ "province_id": 3
401
+ },
402
+ {
403
+ "id": 51,
404
+ "type": "Kabupaten",
405
+ "name": "Serang",
406
+ "code": "04",
407
+ "full_code": "3604",
408
+ "province_id": 3
409
+ },
410
+ {
411
+ "id": 52,
412
+ "type": "Kabupaten",
413
+ "name": "Humbang Hasundutan",
414
+ "code": "16",
415
+ "full_code": "1216",
416
+ "province_id": 38
417
+ },
418
+ {
419
+ "id": 53,
420
+ "type": "Kota",
421
+ "name": "Balikpapan",
422
+ "code": "71",
423
+ "full_code": "6471",
424
+ "province_id": 15
425
+ },
426
+ {
427
+ "id": 54,
428
+ "type": "Kabupaten",
429
+ "name": "Indragiri Hilir",
430
+ "code": "04",
431
+ "full_code": "1404",
432
+ "province_id": 30
433
+ },
434
+ {
435
+ "id": 55,
436
+ "type": "Kabupaten",
437
+ "name": "Indragiri Hulu",
438
+ "code": "02",
439
+ "full_code": "1402",
440
+ "province_id": 30
441
+ },
442
+ {
443
+ "id": 56,
444
+ "type": "Kota",
445
+ "name": "Banda Aceh",
446
+ "code": "71",
447
+ "full_code": "1171",
448
+ "province_id": 1
449
+ },
450
+ {
451
+ "id": 57,
452
+ "type": "Kota",
453
+ "name": "Bandar Lampung",
454
+ "code": "71",
455
+ "full_code": "1871",
456
+ "province_id": 19
457
+ },
458
+ {
459
+ "id": 58,
460
+ "type": "Kota",
461
+ "name": "Bandung",
462
+ "code": "73",
463
+ "full_code": "3273",
464
+ "province_id": 9
465
+ },
466
+ {
467
+ "id": 59,
468
+ "type": "Kabupaten",
469
+ "name": "Bandung",
470
+ "code": "04",
471
+ "full_code": "3204",
472
+ "province_id": 9
473
+ },
474
+ {
475
+ "id": 60,
476
+ "type": "Kabupaten",
477
+ "name": "Bandung Barat",
478
+ "code": "17",
479
+ "full_code": "3217",
480
+ "province_id": 9
481
+ },
482
+ {
483
+ "id": 61,
484
+ "type": "Kabupaten",
485
+ "name": "Banggai",
486
+ "code": "01",
487
+ "full_code": "7201",
488
+ "province_id": 33
489
+ },
490
+ {
491
+ "id": 62,
492
+ "type": "Kabupaten",
493
+ "name": "Banggai Kepulauan",
494
+ "code": "07",
495
+ "full_code": "7207",
496
+ "province_id": 33
497
+ },
498
+ {
499
+ "id": 63,
500
+ "type": "Kabupaten",
501
+ "name": "Banggai Laut",
502
+ "code": "11",
503
+ "full_code": "7211",
504
+ "province_id": 33
505
+ },
506
+ {
507
+ "id": 64,
508
+ "type": "Kabupaten",
509
+ "name": "Bangka",
510
+ "code": "01",
511
+ "full_code": "1901",
512
+ "province_id": 17
513
+ },
514
+ {
515
+ "id": 65,
516
+ "type": "Kabupaten",
517
+ "name": "Bangka Barat",
518
+ "code": "05",
519
+ "full_code": "1905",
520
+ "province_id": 17
521
+ },
522
+ {
523
+ "id": 66,
524
+ "type": "Kabupaten",
525
+ "name": "Bangka Selatan",
526
+ "code": "03",
527
+ "full_code": "1903",
528
+ "province_id": 17
529
+ },
530
+ {
531
+ "id": 67,
532
+ "type": "Kabupaten",
533
+ "name": "Bangka Tengah",
534
+ "code": "04",
535
+ "full_code": "1904",
536
+ "province_id": 17
537
+ },
538
+ {
539
+ "id": 68,
540
+ "type": "Kabupaten",
541
+ "name": "Bangkalan",
542
+ "code": "26",
543
+ "full_code": "3526",
544
+ "province_id": 11
545
+ },
546
+ {
547
+ "id": 69,
548
+ "type": "Kabupaten",
549
+ "name": "Bangli",
550
+ "code": "06",
551
+ "full_code": "5106",
552
+ "province_id": 2
553
+ },
554
+ {
555
+ "id": 70,
556
+ "type": "Kota",
557
+ "name": "Banjar",
558
+ "code": "79",
559
+ "full_code": "3279",
560
+ "province_id": 9
561
+ },
562
+ {
563
+ "id": 71,
564
+ "type": "Kabupaten",
565
+ "name": "Banjar",
566
+ "code": "03",
567
+ "full_code": "6303",
568
+ "province_id": 13
569
+ },
570
+ {
571
+ "id": 72,
572
+ "type": "Kota",
573
+ "name": "Banjarbaru",
574
+ "code": "72",
575
+ "full_code": "6372",
576
+ "province_id": 13
577
+ },
578
+ {
579
+ "id": 73,
580
+ "type": "Kota",
581
+ "name": "Banjarmasin",
582
+ "code": "71",
583
+ "full_code": "6371",
584
+ "province_id": 13
585
+ },
586
+ {
587
+ "id": 74,
588
+ "type": "Kabupaten",
589
+ "name": "Banjarnegara",
590
+ "code": "04",
591
+ "full_code": "3304",
592
+ "province_id": 10
593
+ },
594
+ {
595
+ "id": 75,
596
+ "type": "Kabupaten",
597
+ "name": "Bantaeng",
598
+ "code": "03",
599
+ "full_code": "7303",
600
+ "province_id": 32
601
+ },
602
+ {
603
+ "id": 76,
604
+ "type": "Kabupaten",
605
+ "name": "Bantul",
606
+ "code": "02",
607
+ "full_code": "3402",
608
+ "province_id": 5
609
+ },
610
+ {
611
+ "id": 77,
612
+ "type": "Kabupaten",
613
+ "name": "Banyuasin",
614
+ "code": "07",
615
+ "full_code": "1607",
616
+ "province_id": 37
617
+ },
618
+ {
619
+ "id": 78,
620
+ "type": "Kabupaten",
621
+ "name": "Banyumas",
622
+ "code": "02",
623
+ "full_code": "3302",
624
+ "province_id": 10
625
+ },
626
+ {
627
+ "id": 79,
628
+ "type": "Kabupaten",
629
+ "name": "Banyuwangi",
630
+ "code": "10",
631
+ "full_code": "3510",
632
+ "province_id": 11
633
+ },
634
+ {
635
+ "id": 80,
636
+ "type": "Kabupaten",
637
+ "name": "Barito Kuala",
638
+ "code": "04",
639
+ "full_code": "6304",
640
+ "province_id": 13
641
+ },
642
+ {
643
+ "id": 81,
644
+ "type": "Kabupaten",
645
+ "name": "Barito Selatan",
646
+ "code": "04",
647
+ "full_code": "6204",
648
+ "province_id": 14
649
+ },
650
+ {
651
+ "id": 82,
652
+ "type": "Kabupaten",
653
+ "name": "Barito Timur",
654
+ "code": "13",
655
+ "full_code": "6213",
656
+ "province_id": 14
657
+ },
658
+ {
659
+ "id": 83,
660
+ "type": "Kabupaten",
661
+ "name": "Barito Utara",
662
+ "code": "05",
663
+ "full_code": "6205",
664
+ "province_id": 14
665
+ },
666
+ {
667
+ "id": 84,
668
+ "type": "Kabupaten",
669
+ "name": "Barru",
670
+ "code": "11",
671
+ "full_code": "7311",
672
+ "province_id": 32
673
+ },
674
+ {
675
+ "id": 85,
676
+ "type": "Kota",
677
+ "name": "Batam",
678
+ "code": "71",
679
+ "full_code": "2171",
680
+ "province_id": 18
681
+ },
682
+ {
683
+ "id": 86,
684
+ "type": "Kabupaten",
685
+ "name": "Batang",
686
+ "code": "25",
687
+ "full_code": "3325",
688
+ "province_id": 10
689
+ },
690
+ {
691
+ "id": 87,
692
+ "type": "Kabupaten",
693
+ "name": "Batanghari",
694
+ "code": "04",
695
+ "full_code": "1504",
696
+ "province_id": 8
697
+ },
698
+ {
699
+ "id": 88,
700
+ "type": "Kota",
701
+ "name": "Batu",
702
+ "code": "79",
703
+ "full_code": "3579",
704
+ "province_id": 11
705
+ },
706
+ {
707
+ "id": 89,
708
+ "type": "Kabupaten",
709
+ "name": "Batu Bara",
710
+ "code": "19",
711
+ "full_code": "1219",
712
+ "province_id": 38
713
+ },
714
+ {
715
+ "id": 90,
716
+ "type": "Kota",
717
+ "name": "Bau Bau",
718
+ "code": "72",
719
+ "full_code": "7472",
720
+ "province_id": 34
721
+ },
722
+ {
723
+ "id": 91,
724
+ "type": "Kota",
725
+ "name": "Bekasi",
726
+ "code": "75",
727
+ "full_code": "3275",
728
+ "province_id": 9
729
+ },
730
+ {
731
+ "id": 92,
732
+ "type": "Kabupaten",
733
+ "name": "Bekasi",
734
+ "code": "16",
735
+ "full_code": "3216",
736
+ "province_id": 9
737
+ },
738
+ {
739
+ "id": 93,
740
+ "type": "Kabupaten",
741
+ "name": "Belitung",
742
+ "code": "02",
743
+ "full_code": "1902",
744
+ "province_id": 17
745
+ },
746
+ {
747
+ "id": 94,
748
+ "type": "Kabupaten",
749
+ "name": "Belitung Timur",
750
+ "code": "06",
751
+ "full_code": "1906",
752
+ "province_id": 17
753
+ },
754
+ {
755
+ "id": 95,
756
+ "type": "Kabupaten",
757
+ "name": "Belu",
758
+ "code": "04",
759
+ "full_code": "5304",
760
+ "province_id": 23
761
+ },
762
+ {
763
+ "id": 96,
764
+ "type": "Kabupaten",
765
+ "name": "Bener Meriah",
766
+ "code": "17",
767
+ "full_code": "1117",
768
+ "province_id": 1
769
+ },
770
+ {
771
+ "id": 97,
772
+ "type": "Kabupaten",
773
+ "name": "Bengkalis",
774
+ "code": "03",
775
+ "full_code": "1403",
776
+ "province_id": 30
777
+ },
778
+ {
779
+ "id": 98,
780
+ "type": "Kabupaten",
781
+ "name": "Bengkayang",
782
+ "code": "07",
783
+ "full_code": "6107",
784
+ "province_id": 12
785
+ },
786
+ {
787
+ "id": 99,
788
+ "type": "Kabupaten",
789
+ "name": "Serdang Bedagai",
790
+ "code": "18",
791
+ "full_code": "1218",
792
+ "province_id": 38
793
+ },
794
+ {
795
+ "id": 100,
796
+ "type": "Kota",
797
+ "name": "Bengkulu",
798
+ "code": "71",
799
+ "full_code": "1771",
800
+ "province_id": 4
801
+ },
802
+ {
803
+ "id": 101,
804
+ "type": "Kabupaten",
805
+ "name": "Bengkulu Selatan",
806
+ "code": "01",
807
+ "full_code": "1701",
808
+ "province_id": 4
809
+ },
810
+ {
811
+ "id": 102,
812
+ "type": "Kabupaten",
813
+ "name": "Seruyan",
814
+ "code": "07",
815
+ "full_code": "6207",
816
+ "province_id": 14
817
+ },
818
+ {
819
+ "id": 103,
820
+ "type": "Kabupaten",
821
+ "name": "Indramayu",
822
+ "code": "12",
823
+ "full_code": "3212",
824
+ "province_id": 9
825
+ },
826
+ {
827
+ "id": 104,
828
+ "type": "Kabupaten",
829
+ "name": "Siak",
830
+ "code": "08",
831
+ "full_code": "1408",
832
+ "province_id": 30
833
+ },
834
+ {
835
+ "id": 105,
836
+ "type": "Kabupaten",
837
+ "name": "Intan Jaya",
838
+ "code": "07",
839
+ "full_code": "9407",
840
+ "province_id": 29
841
+ },
842
+ {
843
+ "id": 106,
844
+ "type": "Kota",
845
+ "name": "Jakarta Barat",
846
+ "code": "73",
847
+ "full_code": "3173",
848
+ "province_id": 6
849
+ },
850
+ {
851
+ "id": 107,
852
+ "type": "Kota",
853
+ "name": "Sibolga",
854
+ "code": "73",
855
+ "full_code": "1273",
856
+ "province_id": 38
857
+ },
858
+ {
859
+ "id": 108,
860
+ "type": "Kabupaten",
861
+ "name": "Bengkulu Tengah",
862
+ "code": "09",
863
+ "full_code": "1709",
864
+ "province_id": 4
865
+ },
866
+ {
867
+ "id": 109,
868
+ "type": "Kota",
869
+ "name": "Jakarta Pusat",
870
+ "code": "71",
871
+ "full_code": "3171",
872
+ "province_id": 6
873
+ },
874
+ {
875
+ "id": 110,
876
+ "type": "Kabupaten",
877
+ "name": "Sidenreng Rappang",
878
+ "code": "14",
879
+ "full_code": "7314",
880
+ "province_id": 32
881
+ },
882
+ {
883
+ "id": 111,
884
+ "type": "Kabupaten",
885
+ "name": "Bengkulu Utara",
886
+ "code": "03",
887
+ "full_code": "1703",
888
+ "province_id": 4
889
+ },
890
+ {
891
+ "id": 112,
892
+ "type": "Kota",
893
+ "name": "Jakarta Selatan",
894
+ "code": "74",
895
+ "full_code": "3174",
896
+ "province_id": 6
897
+ },
898
+ {
899
+ "id": 113,
900
+ "type": "Kabupaten",
901
+ "name": "Sidoarjo",
902
+ "code": "15",
903
+ "full_code": "3515",
904
+ "province_id": 11
905
+ },
906
+ {
907
+ "id": 114,
908
+ "type": "Kota",
909
+ "name": "Jakarta Timur",
910
+ "code": "75",
911
+ "full_code": "3175",
912
+ "province_id": 6
913
+ },
914
+ {
915
+ "id": 115,
916
+ "type": "Kabupaten",
917
+ "name": "Sigi",
918
+ "code": "10",
919
+ "full_code": "7210",
920
+ "province_id": 33
921
+ },
922
+ {
923
+ "id": 116,
924
+ "type": "Kabupaten",
925
+ "name": "Berau",
926
+ "code": "03",
927
+ "full_code": "6403",
928
+ "province_id": 15
929
+ },
930
+ {
931
+ "id": 117,
932
+ "type": "Kabupaten",
933
+ "name": "Biak Numfor",
934
+ "code": "06",
935
+ "full_code": "9106",
936
+ "province_id": 24
937
+ },
938
+ {
939
+ "id": 118,
940
+ "type": "Kabupaten",
941
+ "name": "Bima",
942
+ "code": "06",
943
+ "full_code": "5206",
944
+ "province_id": 22
945
+ },
946
+ {
947
+ "id": 119,
948
+ "type": "Kota",
949
+ "name": "Jakarta Utara",
950
+ "code": "72",
951
+ "full_code": "3172",
952
+ "province_id": 6
953
+ },
954
+ {
955
+ "id": 120,
956
+ "type": "Kota",
957
+ "name": "Bima",
958
+ "code": "72",
959
+ "full_code": "5272",
960
+ "province_id": 22
961
+ },
962
+ {
963
+ "id": 121,
964
+ "type": "Kota",
965
+ "name": "Jambi",
966
+ "code": "71",
967
+ "full_code": "1571",
968
+ "province_id": 8
969
+ },
970
+ {
971
+ "id": 122,
972
+ "type": "Kabupaten",
973
+ "name": "Sijunjung",
974
+ "code": "03",
975
+ "full_code": "1303",
976
+ "province_id": 36
977
+ },
978
+ {
979
+ "id": 123,
980
+ "type": "Kota",
981
+ "name": "Binjai",
982
+ "code": "75",
983
+ "full_code": "1275",
984
+ "province_id": 38
985
+ },
986
+ {
987
+ "id": 124,
988
+ "type": "Kabupaten",
989
+ "name": "Jayapura",
990
+ "code": "03",
991
+ "full_code": "9103",
992
+ "province_id": 24
993
+ },
994
+ {
995
+ "id": 125,
996
+ "type": "Kabupaten",
997
+ "name": "Bintan",
998
+ "code": "01",
999
+ "full_code": "2101",
1000
+ "province_id": 18
1001
+ },
1002
+ {
1003
+ "id": 126,
1004
+ "type": "Kabupaten",
1005
+ "name": "Sikka",
1006
+ "code": "07",
1007
+ "full_code": "5307",
1008
+ "province_id": 23
1009
+ },
1010
+ {
1011
+ "id": 127,
1012
+ "type": "Kabupaten",
1013
+ "name": "Bireuen",
1014
+ "code": "11",
1015
+ "full_code": "1111",
1016
+ "province_id": 1
1017
+ },
1018
+ {
1019
+ "id": 128,
1020
+ "type": "Kabupaten",
1021
+ "name": "Simalungun",
1022
+ "code": "08",
1023
+ "full_code": "1208",
1024
+ "province_id": 38
1025
+ },
1026
+ {
1027
+ "id": 129,
1028
+ "type": "Kota",
1029
+ "name": "Bitung",
1030
+ "code": "72",
1031
+ "full_code": "7172",
1032
+ "province_id": 35
1033
+ },
1034
+ {
1035
+ "id": 130,
1036
+ "type": "Kota",
1037
+ "name": "Jayapura",
1038
+ "code": "71",
1039
+ "full_code": "9171",
1040
+ "province_id": 24
1041
+ },
1042
+ {
1043
+ "id": 131,
1044
+ "type": "Kabupaten",
1045
+ "name": "Simeulue",
1046
+ "code": "09",
1047
+ "full_code": "1109",
1048
+ "province_id": 1
1049
+ },
1050
+ {
1051
+ "id": 132,
1052
+ "type": "Kabupaten",
1053
+ "name": "Jayawijaya",
1054
+ "code": "01",
1055
+ "full_code": "9501",
1056
+ "province_id": 27
1057
+ },
1058
+ {
1059
+ "id": 133,
1060
+ "type": "Kabupaten",
1061
+ "name": "Jember",
1062
+ "code": "09",
1063
+ "full_code": "3509",
1064
+ "province_id": 11
1065
+ },
1066
+ {
1067
+ "id": 134,
1068
+ "type": "Kota",
1069
+ "name": "Singkawang",
1070
+ "code": "72",
1071
+ "full_code": "6172",
1072
+ "province_id": 12
1073
+ },
1074
+ {
1075
+ "id": 135,
1076
+ "type": "Kabupaten",
1077
+ "name": "Blitar",
1078
+ "code": "05",
1079
+ "full_code": "3505",
1080
+ "province_id": 11
1081
+ },
1082
+ {
1083
+ "id": 136,
1084
+ "type": "Kabupaten",
1085
+ "name": "Jembrana",
1086
+ "code": "01",
1087
+ "full_code": "5101",
1088
+ "province_id": 2
1089
+ },
1090
+ {
1091
+ "id": 137,
1092
+ "type": "Kabupaten",
1093
+ "name": "Sinjai",
1094
+ "code": "07",
1095
+ "full_code": "7307",
1096
+ "province_id": 32
1097
+ },
1098
+ {
1099
+ "id": 138,
1100
+ "type": "Kota",
1101
+ "name": "Blitar",
1102
+ "code": "72",
1103
+ "full_code": "3572",
1104
+ "province_id": 11
1105
+ },
1106
+ {
1107
+ "id": 139,
1108
+ "type": "Kabupaten",
1109
+ "name": "Jeneponto",
1110
+ "code": "04",
1111
+ "full_code": "7304",
1112
+ "province_id": 32
1113
+ },
1114
+ {
1115
+ "id": 140,
1116
+ "type": "Kabupaten",
1117
+ "name": "Blora",
1118
+ "code": "16",
1119
+ "full_code": "3316",
1120
+ "province_id": 10
1121
+ },
1122
+ {
1123
+ "id": 141,
1124
+ "type": "Kabupaten",
1125
+ "name": "Jepara",
1126
+ "code": "20",
1127
+ "full_code": "3320",
1128
+ "province_id": 10
1129
+ },
1130
+ {
1131
+ "id": 142,
1132
+ "type": "Kabupaten",
1133
+ "name": "Sintang",
1134
+ "code": "05",
1135
+ "full_code": "6105",
1136
+ "province_id": 12
1137
+ },
1138
+ {
1139
+ "id": 143,
1140
+ "type": "Kabupaten",
1141
+ "name": "Boalemo",
1142
+ "code": "02",
1143
+ "full_code": "7502",
1144
+ "province_id": 7
1145
+ },
1146
+ {
1147
+ "id": 144,
1148
+ "type": "Kabupaten",
1149
+ "name": "Jombang",
1150
+ "code": "17",
1151
+ "full_code": "3517",
1152
+ "province_id": 11
1153
+ },
1154
+ {
1155
+ "id": 145,
1156
+ "type": "Kabupaten",
1157
+ "name": "Bogor",
1158
+ "code": "01",
1159
+ "full_code": "3201",
1160
+ "province_id": 9
1161
+ },
1162
+ {
1163
+ "id": 146,
1164
+ "type": "Kabupaten",
1165
+ "name": "Situbondo",
1166
+ "code": "12",
1167
+ "full_code": "3512",
1168
+ "province_id": 11
1169
+ },
1170
+ {
1171
+ "id": 147,
1172
+ "type": "Kabupaten",
1173
+ "name": "Kaimana",
1174
+ "code": "08",
1175
+ "full_code": "9208",
1176
+ "province_id": 25
1177
+ },
1178
+ {
1179
+ "id": 148,
1180
+ "type": "Kota",
1181
+ "name": "Bogor",
1182
+ "code": "71",
1183
+ "full_code": "3271",
1184
+ "province_id": 9
1185
+ },
1186
+ {
1187
+ "id": 149,
1188
+ "type": "Kabupaten",
1189
+ "name": "Sleman",
1190
+ "code": "04",
1191
+ "full_code": "3404",
1192
+ "province_id": 5
1193
+ },
1194
+ {
1195
+ "id": 150,
1196
+ "type": "Kabupaten",
1197
+ "name": "Kampar",
1198
+ "code": "01",
1199
+ "full_code": "1401",
1200
+ "province_id": 30
1201
+ },
1202
+ {
1203
+ "id": 151,
1204
+ "type": "Kabupaten",
1205
+ "name": "Bojonegoro",
1206
+ "code": "22",
1207
+ "full_code": "3522",
1208
+ "province_id": 11
1209
+ },
1210
+ {
1211
+ "id": 152,
1212
+ "type": "Kabupaten",
1213
+ "name": "Solok",
1214
+ "code": "02",
1215
+ "full_code": "1302",
1216
+ "province_id": 36
1217
+ },
1218
+ {
1219
+ "id": 153,
1220
+ "type": "Kabupaten",
1221
+ "name": "Bolaang Mongondow",
1222
+ "code": "01",
1223
+ "full_code": "7101",
1224
+ "province_id": 35
1225
+ },
1226
+ {
1227
+ "id": 154,
1228
+ "type": "Kabupaten",
1229
+ "name": "Bolaang Mongondow Selatan",
1230
+ "code": "11",
1231
+ "full_code": "7111",
1232
+ "province_id": 35
1233
+ },
1234
+ {
1235
+ "id": 155,
1236
+ "type": "Kota",
1237
+ "name": "Solok",
1238
+ "code": "72",
1239
+ "full_code": "1372",
1240
+ "province_id": 36
1241
+ },
1242
+ {
1243
+ "id": 156,
1244
+ "type": "Kabupaten",
1245
+ "name": "Kapuas",
1246
+ "code": "03",
1247
+ "full_code": "6203",
1248
+ "province_id": 14
1249
+ },
1250
+ {
1251
+ "id": 157,
1252
+ "type": "Kabupaten",
1253
+ "name": "Solok Selatan",
1254
+ "code": "11",
1255
+ "full_code": "1311",
1256
+ "province_id": 36
1257
+ },
1258
+ {
1259
+ "id": 158,
1260
+ "type": "Kabupaten",
1261
+ "name": "Bolaang Mongondow Timur",
1262
+ "code": "10",
1263
+ "full_code": "7110",
1264
+ "province_id": 35
1265
+ },
1266
+ {
1267
+ "id": 159,
1268
+ "type": "Kabupaten",
1269
+ "name": "Kapuas Hulu",
1270
+ "code": "06",
1271
+ "full_code": "6106",
1272
+ "province_id": 12
1273
+ },
1274
+ {
1275
+ "id": 160,
1276
+ "type": "Kabupaten",
1277
+ "name": "Karanganyar",
1278
+ "code": "13",
1279
+ "full_code": "3313",
1280
+ "province_id": 10
1281
+ },
1282
+ {
1283
+ "id": 161,
1284
+ "type": "Kabupaten",
1285
+ "name": "Karangasem",
1286
+ "code": "07",
1287
+ "full_code": "5107",
1288
+ "province_id": 2
1289
+ },
1290
+ {
1291
+ "id": 162,
1292
+ "type": "Kabupaten",
1293
+ "name": "Bolaang Mongondow Utara",
1294
+ "code": "08",
1295
+ "full_code": "7108",
1296
+ "province_id": 35
1297
+ },
1298
+ {
1299
+ "id": 163,
1300
+ "type": "Kabupaten",
1301
+ "name": "Karawang",
1302
+ "code": "15",
1303
+ "full_code": "3215",
1304
+ "province_id": 9
1305
+ },
1306
+ {
1307
+ "id": 164,
1308
+ "type": "Kabupaten",
1309
+ "name": "Bombana",
1310
+ "code": "06",
1311
+ "full_code": "7406",
1312
+ "province_id": 34
1313
+ },
1314
+ {
1315
+ "id": 165,
1316
+ "type": "Kabupaten",
1317
+ "name": "Karimun",
1318
+ "code": "02",
1319
+ "full_code": "2102",
1320
+ "province_id": 18
1321
+ },
1322
+ {
1323
+ "id": 166,
1324
+ "type": "Kabupaten",
1325
+ "name": "Bondowoso",
1326
+ "code": "11",
1327
+ "full_code": "3511",
1328
+ "province_id": 11
1329
+ },
1330
+ {
1331
+ "id": 167,
1332
+ "type": "Kabupaten",
1333
+ "name": "Karo",
1334
+ "code": "06",
1335
+ "full_code": "1206",
1336
+ "province_id": 38
1337
+ },
1338
+ {
1339
+ "id": 168,
1340
+ "type": "Kabupaten",
1341
+ "name": "Soppeng",
1342
+ "code": "12",
1343
+ "full_code": "7312",
1344
+ "province_id": 32
1345
+ },
1346
+ {
1347
+ "id": 169,
1348
+ "type": "Kabupaten",
1349
+ "name": "Katingan",
1350
+ "code": "06",
1351
+ "full_code": "6206",
1352
+ "province_id": 14
1353
+ },
1354
+ {
1355
+ "id": 170,
1356
+ "type": "Kabupaten",
1357
+ "name": "Kaur",
1358
+ "code": "04",
1359
+ "full_code": "1704",
1360
+ "province_id": 4
1361
+ },
1362
+ {
1363
+ "id": 171,
1364
+ "type": "Kabupaten",
1365
+ "name": "Sorong",
1366
+ "code": "01",
1367
+ "full_code": "9201",
1368
+ "province_id": 26
1369
+ },
1370
+ {
1371
+ "id": 172,
1372
+ "type": "Kabupaten",
1373
+ "name": "Kayong Utara",
1374
+ "code": "11",
1375
+ "full_code": "6111",
1376
+ "province_id": 12
1377
+ },
1378
+ {
1379
+ "id": 173,
1380
+ "type": "Kota",
1381
+ "name": "Sorong",
1382
+ "code": "71",
1383
+ "full_code": "9271",
1384
+ "province_id": 26
1385
+ },
1386
+ {
1387
+ "id": 174,
1388
+ "type": "Kabupaten",
1389
+ "name": "Sorong Selatan",
1390
+ "code": "04",
1391
+ "full_code": "9204",
1392
+ "province_id": 26
1393
+ },
1394
+ {
1395
+ "id": 175,
1396
+ "type": "Kabupaten",
1397
+ "name": "Kebumen",
1398
+ "code": "05",
1399
+ "full_code": "3305",
1400
+ "province_id": 10
1401
+ },
1402
+ {
1403
+ "id": 176,
1404
+ "type": "Kabupaten",
1405
+ "name": "Sragen",
1406
+ "code": "14",
1407
+ "full_code": "3314",
1408
+ "province_id": 10
1409
+ },
1410
+ {
1411
+ "id": 177,
1412
+ "type": "Kabupaten",
1413
+ "name": "Kediri",
1414
+ "code": "06",
1415
+ "full_code": "3506",
1416
+ "province_id": 11
1417
+ },
1418
+ {
1419
+ "id": 178,
1420
+ "type": "Kabupaten",
1421
+ "name": "Subang",
1422
+ "code": "13",
1423
+ "full_code": "3213",
1424
+ "province_id": 9
1425
+ },
1426
+ {
1427
+ "id": 179,
1428
+ "type": "Kota",
1429
+ "name": "Subulussalam",
1430
+ "code": "75",
1431
+ "full_code": "1175",
1432
+ "province_id": 1
1433
+ },
1434
+ {
1435
+ "id": 180,
1436
+ "type": "Kota",
1437
+ "name": "Sukabumi",
1438
+ "code": "72",
1439
+ "full_code": "3272",
1440
+ "province_id": 9
1441
+ },
1442
+ {
1443
+ "id": 181,
1444
+ "type": "Kota",
1445
+ "name": "Kediri",
1446
+ "code": "71",
1447
+ "full_code": "3571",
1448
+ "province_id": 11
1449
+ },
1450
+ {
1451
+ "id": 182,
1452
+ "type": "Kabupaten",
1453
+ "name": "Bone",
1454
+ "code": "08",
1455
+ "full_code": "7308",
1456
+ "province_id": 32
1457
+ },
1458
+ {
1459
+ "id": 183,
1460
+ "type": "Kabupaten",
1461
+ "name": "Keerom",
1462
+ "code": "11",
1463
+ "full_code": "9111",
1464
+ "province_id": 24
1465
+ },
1466
+ {
1467
+ "id": 184,
1468
+ "type": "Kabupaten",
1469
+ "name": "Sukabumi",
1470
+ "code": "02",
1471
+ "full_code": "3202",
1472
+ "province_id": 9
1473
+ },
1474
+ {
1475
+ "id": 185,
1476
+ "type": "Kabupaten",
1477
+ "name": "Bone Bolango",
1478
+ "code": "03",
1479
+ "full_code": "7503",
1480
+ "province_id": 7
1481
+ },
1482
+ {
1483
+ "id": 186,
1484
+ "type": "Kabupaten",
1485
+ "name": "Kendal",
1486
+ "code": "24",
1487
+ "full_code": "3324",
1488
+ "province_id": 10
1489
+ },
1490
+ {
1491
+ "id": 187,
1492
+ "type": "Kota",
1493
+ "name": "Bontang",
1494
+ "code": "74",
1495
+ "full_code": "6474",
1496
+ "province_id": 15
1497
+ },
1498
+ {
1499
+ "id": 188,
1500
+ "type": "Kota",
1501
+ "name": "Kendari",
1502
+ "code": "71",
1503
+ "full_code": "7471",
1504
+ "province_id": 34
1505
+ },
1506
+ {
1507
+ "id": 189,
1508
+ "type": "Kabupaten",
1509
+ "name": "Boven Digoel",
1510
+ "code": "02",
1511
+ "full_code": "9302",
1512
+ "province_id": 28
1513
+ },
1514
+ {
1515
+ "id": 190,
1516
+ "type": "Kabupaten",
1517
+ "name": "Kepahiang",
1518
+ "code": "08",
1519
+ "full_code": "1708",
1520
+ "province_id": 4
1521
+ },
1522
+ {
1523
+ "id": 191,
1524
+ "type": "Kabupaten",
1525
+ "name": "Boyolali",
1526
+ "code": "09",
1527
+ "full_code": "3309",
1528
+ "province_id": 10
1529
+ },
1530
+ {
1531
+ "id": 192,
1532
+ "type": "Kabupaten",
1533
+ "name": "Sukamara",
1534
+ "code": "08",
1535
+ "full_code": "6208",
1536
+ "province_id": 14
1537
+ },
1538
+ {
1539
+ "id": 193,
1540
+ "type": "Kabupaten",
1541
+ "name": "Kepulauan Anambas",
1542
+ "code": "05",
1543
+ "full_code": "2105",
1544
+ "province_id": 18
1545
+ },
1546
+ {
1547
+ "id": 194,
1548
+ "type": "Kabupaten",
1549
+ "name": "Sukoharjo",
1550
+ "code": "11",
1551
+ "full_code": "3311",
1552
+ "province_id": 10
1553
+ },
1554
+ {
1555
+ "id": 195,
1556
+ "type": "Kabupaten",
1557
+ "name": "Sumba Barat",
1558
+ "code": "12",
1559
+ "full_code": "5312",
1560
+ "province_id": 23
1561
+ },
1562
+ {
1563
+ "id": 196,
1564
+ "type": "Kabupaten",
1565
+ "name": "Brebes",
1566
+ "code": "29",
1567
+ "full_code": "3329",
1568
+ "province_id": 10
1569
+ },
1570
+ {
1571
+ "id": 197,
1572
+ "type": "Kota",
1573
+ "name": "Bukittinggi",
1574
+ "code": "75",
1575
+ "full_code": "1375",
1576
+ "province_id": 36
1577
+ },
1578
+ {
1579
+ "id": 198,
1580
+ "type": "Kabupaten",
1581
+ "name": "Buleleng",
1582
+ "code": "08",
1583
+ "full_code": "5108",
1584
+ "province_id": 2
1585
+ },
1586
+ {
1587
+ "id": 199,
1588
+ "type": "Kabupaten",
1589
+ "name": "Bulukumba",
1590
+ "code": "02",
1591
+ "full_code": "7302",
1592
+ "province_id": 32
1593
+ },
1594
+ {
1595
+ "id": 200,
1596
+ "type": "Kabupaten",
1597
+ "name": "Sumba Barat Daya",
1598
+ "code": "18",
1599
+ "full_code": "5318",
1600
+ "province_id": 23
1601
+ },
1602
+ {
1603
+ "id": 201,
1604
+ "type": "Kabupaten",
1605
+ "name": "Sumba Tengah",
1606
+ "code": "17",
1607
+ "full_code": "5317",
1608
+ "province_id": 23
1609
+ },
1610
+ {
1611
+ "id": 202,
1612
+ "type": "Kabupaten",
1613
+ "name": "Sumba Timur",
1614
+ "code": "11",
1615
+ "full_code": "5311",
1616
+ "province_id": 23
1617
+ },
1618
+ {
1619
+ "id": 203,
1620
+ "type": "Kabupaten",
1621
+ "name": "Bulungan",
1622
+ "code": "01",
1623
+ "full_code": "6501",
1624
+ "province_id": 16
1625
+ },
1626
+ {
1627
+ "id": 204,
1628
+ "type": "Kabupaten",
1629
+ "name": "Sumbawa",
1630
+ "code": "04",
1631
+ "full_code": "5204",
1632
+ "province_id": 22
1633
+ },
1634
+ {
1635
+ "id": 205,
1636
+ "type": "Kabupaten",
1637
+ "name": "Bungo",
1638
+ "code": "08",
1639
+ "full_code": "1508",
1640
+ "province_id": 8
1641
+ },
1642
+ {
1643
+ "id": 206,
1644
+ "type": "Kabupaten",
1645
+ "name": "Sumbawa Barat",
1646
+ "code": "07",
1647
+ "full_code": "5207",
1648
+ "province_id": 22
1649
+ },
1650
+ {
1651
+ "id": 207,
1652
+ "type": "Kabupaten",
1653
+ "name": "Buol",
1654
+ "code": "05",
1655
+ "full_code": "7205",
1656
+ "province_id": 33
1657
+ },
1658
+ {
1659
+ "id": 208,
1660
+ "type": "Kabupaten",
1661
+ "name": "Sumedang",
1662
+ "code": "11",
1663
+ "full_code": "3211",
1664
+ "province_id": 9
1665
+ },
1666
+ {
1667
+ "id": 209,
1668
+ "type": "Kabupaten",
1669
+ "name": "Buru",
1670
+ "code": "04",
1671
+ "full_code": "8104",
1672
+ "province_id": 20
1673
+ },
1674
+ {
1675
+ "id": 210,
1676
+ "type": "Kabupaten",
1677
+ "name": "Buru Selatan",
1678
+ "code": "09",
1679
+ "full_code": "8109",
1680
+ "province_id": 20
1681
+ },
1682
+ {
1683
+ "id": 211,
1684
+ "type": "Kabupaten",
1685
+ "name": "Kepulauan Aru",
1686
+ "code": "07",
1687
+ "full_code": "8107",
1688
+ "province_id": 20
1689
+ },
1690
+ {
1691
+ "id": 212,
1692
+ "type": "Kabupaten",
1693
+ "name": "Sumenep",
1694
+ "code": "29",
1695
+ "full_code": "3529",
1696
+ "province_id": 11
1697
+ },
1698
+ {
1699
+ "id": 213,
1700
+ "type": "Kabupaten",
1701
+ "name": "Buton",
1702
+ "code": "04",
1703
+ "full_code": "7404",
1704
+ "province_id": 34
1705
+ },
1706
+ {
1707
+ "id": 214,
1708
+ "type": "Kota",
1709
+ "name": "Sungai Penuh",
1710
+ "code": "72",
1711
+ "full_code": "1572",
1712
+ "province_id": 8
1713
+ },
1714
+ {
1715
+ "id": 215,
1716
+ "type": "Kabupaten",
1717
+ "name": "Buton Selatan",
1718
+ "code": "15",
1719
+ "full_code": "7415",
1720
+ "province_id": 34
1721
+ },
1722
+ {
1723
+ "id": 216,
1724
+ "type": "Kabupaten",
1725
+ "name": "Supiori",
1726
+ "code": "19",
1727
+ "full_code": "9119",
1728
+ "province_id": 24
1729
+ },
1730
+ {
1731
+ "id": 217,
1732
+ "type": "Kabupaten",
1733
+ "name": "Buton Tengah",
1734
+ "code": "14",
1735
+ "full_code": "7414",
1736
+ "province_id": 34
1737
+ },
1738
+ {
1739
+ "id": 218,
1740
+ "type": "Kabupaten",
1741
+ "name": "Buton Utara",
1742
+ "code": "10",
1743
+ "full_code": "7410",
1744
+ "province_id": 34
1745
+ },
1746
+ {
1747
+ "id": 219,
1748
+ "type": "Kota",
1749
+ "name": "Surabaya",
1750
+ "code": "78",
1751
+ "full_code": "3578",
1752
+ "province_id": 11
1753
+ },
1754
+ {
1755
+ "id": 220,
1756
+ "type": "Kabupaten",
1757
+ "name": "Kepulauan Mentawai",
1758
+ "code": "09",
1759
+ "full_code": "1309",
1760
+ "province_id": 36
1761
+ },
1762
+ {
1763
+ "id": 221,
1764
+ "type": "Kabupaten",
1765
+ "name": "Ciamis",
1766
+ "code": "07",
1767
+ "full_code": "3207",
1768
+ "province_id": 9
1769
+ },
1770
+ {
1771
+ "id": 222,
1772
+ "type": "Kota",
1773
+ "name": "Surakarta",
1774
+ "code": "72",
1775
+ "full_code": "3372",
1776
+ "province_id": 10
1777
+ },
1778
+ {
1779
+ "id": 223,
1780
+ "type": "Kabupaten",
1781
+ "name": "Kepulauan Meranti",
1782
+ "code": "10",
1783
+ "full_code": "1410",
1784
+ "province_id": 30
1785
+ },
1786
+ {
1787
+ "id": 224,
1788
+ "type": "Kabupaten",
1789
+ "name": "Cianjur",
1790
+ "code": "03",
1791
+ "full_code": "3203",
1792
+ "province_id": 9
1793
+ },
1794
+ {
1795
+ "id": 225,
1796
+ "type": "Kabupaten",
1797
+ "name": "Tabalong",
1798
+ "code": "09",
1799
+ "full_code": "6309",
1800
+ "province_id": 13
1801
+ },
1802
+ {
1803
+ "id": 226,
1804
+ "type": "Kabupaten",
1805
+ "name": "Kepulauan Sangihe",
1806
+ "code": "03",
1807
+ "full_code": "7103",
1808
+ "province_id": 35
1809
+ },
1810
+ {
1811
+ "id": 227,
1812
+ "type": "Kabupaten",
1813
+ "name": "Kepulauan Selayar",
1814
+ "code": "01",
1815
+ "full_code": "7301",
1816
+ "province_id": 32
1817
+ },
1818
+ {
1819
+ "id": 228,
1820
+ "type": "Kabupaten",
1821
+ "name": "Cilacap",
1822
+ "code": "01",
1823
+ "full_code": "3301",
1824
+ "province_id": 10
1825
+ },
1826
+ {
1827
+ "id": 229,
1828
+ "type": "Kota",
1829
+ "name": "Cilegon",
1830
+ "code": "72",
1831
+ "full_code": "3672",
1832
+ "province_id": 3
1833
+ },
1834
+ {
1835
+ "id": 230,
1836
+ "type": "Kabupaten",
1837
+ "name": "Tabanan",
1838
+ "code": "02",
1839
+ "full_code": "5102",
1840
+ "province_id": 2
1841
+ },
1842
+ {
1843
+ "id": 231,
1844
+ "type": "Kota",
1845
+ "name": "Cimahi",
1846
+ "code": "77",
1847
+ "full_code": "3277",
1848
+ "province_id": 9
1849
+ },
1850
+ {
1851
+ "id": 232,
1852
+ "type": "Kabupaten",
1853
+ "name": "Kepulauan Seribu",
1854
+ "code": "01",
1855
+ "full_code": "3101",
1856
+ "province_id": 6
1857
+ },
1858
+ {
1859
+ "id": 233,
1860
+ "type": "Kabupaten",
1861
+ "name": "Takalar",
1862
+ "code": "05",
1863
+ "full_code": "7305",
1864
+ "province_id": 32
1865
+ },
1866
+ {
1867
+ "id": 234,
1868
+ "type": "Kota",
1869
+ "name": "Cirebon",
1870
+ "code": "74",
1871
+ "full_code": "3274",
1872
+ "province_id": 9
1873
+ },
1874
+ {
1875
+ "id": 235,
1876
+ "type": "Kabupaten",
1877
+ "name": "Kepulauan Siau Tagulandang Biaro (Sitaro)",
1878
+ "code": "09",
1879
+ "full_code": "7109",
1880
+ "province_id": 35
1881
+ },
1882
+ {
1883
+ "id": 236,
1884
+ "type": "Kabupaten",
1885
+ "name": "Kepulauan Sula",
1886
+ "code": "05",
1887
+ "full_code": "8205",
1888
+ "province_id": 21
1889
+ },
1890
+ {
1891
+ "id": 237,
1892
+ "type": "Kabupaten",
1893
+ "name": "Tambrauw",
1894
+ "code": "09",
1895
+ "full_code": "9209",
1896
+ "province_id": 26
1897
+ },
1898
+ {
1899
+ "id": 238,
1900
+ "type": "Kabupaten",
1901
+ "name": "Kepulauan Talaud",
1902
+ "code": "04",
1903
+ "full_code": "7104",
1904
+ "province_id": 35
1905
+ },
1906
+ {
1907
+ "id": 239,
1908
+ "type": "Kabupaten",
1909
+ "name": "Kepulauan Tanimbar (Maluku Tenggara Barat)",
1910
+ "code": "03",
1911
+ "full_code": "8103",
1912
+ "province_id": 20
1913
+ },
1914
+ {
1915
+ "id": 240,
1916
+ "type": "Kabupaten",
1917
+ "name": "Cirebon",
1918
+ "code": "09",
1919
+ "full_code": "3209",
1920
+ "province_id": 9
1921
+ },
1922
+ {
1923
+ "id": 241,
1924
+ "type": "Kabupaten",
1925
+ "name": "Dairi",
1926
+ "code": "11",
1927
+ "full_code": "1211",
1928
+ "province_id": 38
1929
+ },
1930
+ {
1931
+ "id": 242,
1932
+ "type": "Kabupaten",
1933
+ "name": "Tana Tidung",
1934
+ "code": "04",
1935
+ "full_code": "6504",
1936
+ "province_id": 16
1937
+ },
1938
+ {
1939
+ "id": 243,
1940
+ "type": "Kabupaten",
1941
+ "name": "Deiyai",
1942
+ "code": "08",
1943
+ "full_code": "9408",
1944
+ "province_id": 29
1945
+ },
1946
+ {
1947
+ "id": 244,
1948
+ "type": "Kabupaten",
1949
+ "name": "Kepulauan Yapen",
1950
+ "code": "05",
1951
+ "full_code": "9105",
1952
+ "province_id": 24
1953
+ },
1954
+ {
1955
+ "id": 245,
1956
+ "type": "Kabupaten",
1957
+ "name": "Deli Serdang",
1958
+ "code": "07",
1959
+ "full_code": "1207",
1960
+ "province_id": 38
1961
+ },
1962
+ {
1963
+ "id": 246,
1964
+ "type": "Kabupaten",
1965
+ "name": "Tana Toraja",
1966
+ "code": "18",
1967
+ "full_code": "7318",
1968
+ "province_id": 32
1969
+ },
1970
+ {
1971
+ "id": 247,
1972
+ "type": "Kabupaten",
1973
+ "name": "Demak",
1974
+ "code": "21",
1975
+ "full_code": "3321",
1976
+ "province_id": 10
1977
+ },
1978
+ {
1979
+ "id": 248,
1980
+ "type": "Kabupaten",
1981
+ "name": "Kerinci",
1982
+ "code": "01",
1983
+ "full_code": "1501",
1984
+ "province_id": 8
1985
+ },
1986
+ {
1987
+ "id": 249,
1988
+ "type": "Kabupaten",
1989
+ "name": "Tanah Bumbu",
1990
+ "code": "10",
1991
+ "full_code": "6310",
1992
+ "province_id": 13
1993
+ },
1994
+ {
1995
+ "id": 250,
1996
+ "type": "Kota",
1997
+ "name": "Denpasar",
1998
+ "code": "71",
1999
+ "full_code": "5171",
2000
+ "province_id": 2
2001
+ },
2002
+ {
2003
+ "id": 251,
2004
+ "type": "Kabupaten",
2005
+ "name": "Ketapang",
2006
+ "code": "04",
2007
+ "full_code": "6104",
2008
+ "province_id": 12
2009
+ },
2010
+ {
2011
+ "id": 252,
2012
+ "type": "Kota",
2013
+ "name": "Depok",
2014
+ "code": "76",
2015
+ "full_code": "3276",
2016
+ "province_id": 9
2017
+ },
2018
+ {
2019
+ "id": 253,
2020
+ "type": "Kabupaten",
2021
+ "name": "Tanah Datar",
2022
+ "code": "04",
2023
+ "full_code": "1304",
2024
+ "province_id": 36
2025
+ },
2026
+ {
2027
+ "id": 254,
2028
+ "type": "Kabupaten",
2029
+ "name": "Klaten",
2030
+ "code": "10",
2031
+ "full_code": "3310",
2032
+ "province_id": 10
2033
+ },
2034
+ {
2035
+ "id": 255,
2036
+ "type": "Kabupaten",
2037
+ "name": "Dharmasraya",
2038
+ "code": "10",
2039
+ "full_code": "1310",
2040
+ "province_id": 36
2041
+ },
2042
+ {
2043
+ "id": 256,
2044
+ "type": "Kabupaten",
2045
+ "name": "Tanah Laut",
2046
+ "code": "01",
2047
+ "full_code": "6301",
2048
+ "province_id": 13
2049
+ },
2050
+ {
2051
+ "id": 257,
2052
+ "type": "Kabupaten",
2053
+ "name": "Dogiyai",
2054
+ "code": "06",
2055
+ "full_code": "9406",
2056
+ "province_id": 29
2057
+ },
2058
+ {
2059
+ "id": 258,
2060
+ "type": "Kabupaten",
2061
+ "name": "Klungkung",
2062
+ "code": "05",
2063
+ "full_code": "5105",
2064
+ "province_id": 2
2065
+ },
2066
+ {
2067
+ "id": 259,
2068
+ "type": "Kabupaten",
2069
+ "name": "Kolaka",
2070
+ "code": "01",
2071
+ "full_code": "7401",
2072
+ "province_id": 34
2073
+ },
2074
+ {
2075
+ "id": 260,
2076
+ "type": "Kota",
2077
+ "name": "Tangerang",
2078
+ "code": "71",
2079
+ "full_code": "3671",
2080
+ "province_id": 3
2081
+ },
2082
+ {
2083
+ "id": 261,
2084
+ "type": "Kabupaten",
2085
+ "name": "Kolaka Timur",
2086
+ "code": "11",
2087
+ "full_code": "7411",
2088
+ "province_id": 34
2089
+ },
2090
+ {
2091
+ "id": 262,
2092
+ "type": "Kabupaten",
2093
+ "name": "Tangerang",
2094
+ "code": "03",
2095
+ "full_code": "3603",
2096
+ "province_id": 3
2097
+ },
2098
+ {
2099
+ "id": 263,
2100
+ "type": "Kota",
2101
+ "name": "Tangerang Selatan",
2102
+ "code": "74",
2103
+ "full_code": "3674",
2104
+ "province_id": 3
2105
+ },
2106
+ {
2107
+ "id": 264,
2108
+ "type": "Kabupaten",
2109
+ "name": "Tanggamus",
2110
+ "code": "06",
2111
+ "full_code": "1806",
2112
+ "province_id": 19
2113
+ },
2114
+ {
2115
+ "id": 265,
2116
+ "type": "Kabupaten",
2117
+ "name": "Kolaka Utara",
2118
+ "code": "08",
2119
+ "full_code": "7408",
2120
+ "province_id": 34
2121
+ },
2122
+ {
2123
+ "id": 266,
2124
+ "type": "Kabupaten",
2125
+ "name": "Konawe",
2126
+ "code": "02",
2127
+ "full_code": "7402",
2128
+ "province_id": 34
2129
+ },
2130
+ {
2131
+ "id": 267,
2132
+ "type": "Kabupaten",
2133
+ "name": "Konawe Kepulauan",
2134
+ "code": "12",
2135
+ "full_code": "7412",
2136
+ "province_id": 34
2137
+ },
2138
+ {
2139
+ "id": 268,
2140
+ "type": "Kota",
2141
+ "name": "Tanjung Balai",
2142
+ "code": "74",
2143
+ "full_code": "1274",
2144
+ "province_id": 38
2145
+ },
2146
+ {
2147
+ "id": 269,
2148
+ "type": "Kabupaten",
2149
+ "name": "Konawe Selatan",
2150
+ "code": "05",
2151
+ "full_code": "7405",
2152
+ "province_id": 34
2153
+ },
2154
+ {
2155
+ "id": 270,
2156
+ "type": "Kabupaten",
2157
+ "name": "Konawe Utara",
2158
+ "code": "09",
2159
+ "full_code": "7409",
2160
+ "province_id": 34
2161
+ },
2162
+ {
2163
+ "id": 271,
2164
+ "type": "Kabupaten",
2165
+ "name": "Kotabaru",
2166
+ "code": "02",
2167
+ "full_code": "6302",
2168
+ "province_id": 13
2169
+ },
2170
+ {
2171
+ "id": 272,
2172
+ "type": "Kabupaten",
2173
+ "name": "Tanjung Jabung Barat",
2174
+ "code": "06",
2175
+ "full_code": "1506",
2176
+ "province_id": 8
2177
+ },
2178
+ {
2179
+ "id": 273,
2180
+ "type": "Kota",
2181
+ "name": "Kotamobagu",
2182
+ "code": "74",
2183
+ "full_code": "7174",
2184
+ "province_id": 35
2185
+ },
2186
+ {
2187
+ "id": 274,
2188
+ "type": "Kabupaten",
2189
+ "name": "Tanjung Jabung Timur",
2190
+ "code": "07",
2191
+ "full_code": "1507",
2192
+ "province_id": 8
2193
+ },
2194
+ {
2195
+ "id": 275,
2196
+ "type": "Kabupaten",
2197
+ "name": "Kotawaringin Barat",
2198
+ "code": "01",
2199
+ "full_code": "6201",
2200
+ "province_id": 14
2201
+ },
2202
+ {
2203
+ "id": 276,
2204
+ "type": "Kabupaten",
2205
+ "name": "Kotawaringin Timur",
2206
+ "code": "02",
2207
+ "full_code": "6202",
2208
+ "province_id": 14
2209
+ },
2210
+ {
2211
+ "id": 277,
2212
+ "type": "Kota",
2213
+ "name": "Tanjung Pinang",
2214
+ "code": "72",
2215
+ "full_code": "2172",
2216
+ "province_id": 18
2217
+ },
2218
+ {
2219
+ "id": 278,
2220
+ "type": "Kabupaten",
2221
+ "name": "Kuantan Singingi",
2222
+ "code": "09",
2223
+ "full_code": "1409",
2224
+ "province_id": 30
2225
+ },
2226
+ {
2227
+ "id": 279,
2228
+ "type": "Kabupaten",
2229
+ "name": "Tapanuli Selatan",
2230
+ "code": "03",
2231
+ "full_code": "1203",
2232
+ "province_id": 38
2233
+ },
2234
+ {
2235
+ "id": 280,
2236
+ "type": "Kabupaten",
2237
+ "name": "Kubu Raya",
2238
+ "code": "12",
2239
+ "full_code": "6112",
2240
+ "province_id": 12
2241
+ },
2242
+ {
2243
+ "id": 281,
2244
+ "type": "Kabupaten",
2245
+ "name": "Kudus",
2246
+ "code": "19",
2247
+ "full_code": "3319",
2248
+ "province_id": 10
2249
+ },
2250
+ {
2251
+ "id": 282,
2252
+ "type": "Kabupaten",
2253
+ "name": "Tapanuli Tengah",
2254
+ "code": "01",
2255
+ "full_code": "1201",
2256
+ "province_id": 38
2257
+ },
2258
+ {
2259
+ "id": 283,
2260
+ "type": "Kabupaten",
2261
+ "name": "Tapanuli Utara",
2262
+ "code": "02",
2263
+ "full_code": "1202",
2264
+ "province_id": 38
2265
+ },
2266
+ {
2267
+ "id": 284,
2268
+ "type": "Kabupaten",
2269
+ "name": "Tapin",
2270
+ "code": "05",
2271
+ "full_code": "6305",
2272
+ "province_id": 13
2273
+ },
2274
+ {
2275
+ "id": 285,
2276
+ "type": "Kota",
2277
+ "name": "Tarakan",
2278
+ "code": "71",
2279
+ "full_code": "6571",
2280
+ "province_id": 16
2281
+ },
2282
+ {
2283
+ "id": 286,
2284
+ "type": "Kota",
2285
+ "name": "Tasikmalaya",
2286
+ "code": "78",
2287
+ "full_code": "3278",
2288
+ "province_id": 9
2289
+ },
2290
+ {
2291
+ "id": 287,
2292
+ "type": "Kabupaten",
2293
+ "name": "Kulon Progo",
2294
+ "code": "01",
2295
+ "full_code": "3401",
2296
+ "province_id": 5
2297
+ },
2298
+ {
2299
+ "id": 288,
2300
+ "type": "Kabupaten",
2301
+ "name": "Kuningan",
2302
+ "code": "08",
2303
+ "full_code": "3208",
2304
+ "province_id": 9
2305
+ },
2306
+ {
2307
+ "id": 289,
2308
+ "type": "Kabupaten",
2309
+ "name": "Kupang",
2310
+ "code": "01",
2311
+ "full_code": "5301",
2312
+ "province_id": 23
2313
+ },
2314
+ {
2315
+ "id": 290,
2316
+ "type": "Kota",
2317
+ "name": "Kupang",
2318
+ "code": "71",
2319
+ "full_code": "5371",
2320
+ "province_id": 23
2321
+ },
2322
+ {
2323
+ "id": 291,
2324
+ "type": "Kabupaten",
2325
+ "name": "Tasikmalaya",
2326
+ "code": "06",
2327
+ "full_code": "3206",
2328
+ "province_id": 9
2329
+ },
2330
+ {
2331
+ "id": 292,
2332
+ "type": "Kabupaten",
2333
+ "name": "Kutai Barat",
2334
+ "code": "07",
2335
+ "full_code": "6407",
2336
+ "province_id": 15
2337
+ },
2338
+ {
2339
+ "id": 293,
2340
+ "type": "Kabupaten",
2341
+ "name": "Kutai Kartanegara",
2342
+ "code": "02",
2343
+ "full_code": "6402",
2344
+ "province_id": 15
2345
+ },
2346
+ {
2347
+ "id": 294,
2348
+ "type": "Kabupaten",
2349
+ "name": "Kutai Timur",
2350
+ "code": "08",
2351
+ "full_code": "6408",
2352
+ "province_id": 15
2353
+ },
2354
+ {
2355
+ "id": 295,
2356
+ "type": "Kabupaten",
2357
+ "name": "Labuhanbatu",
2358
+ "code": "10",
2359
+ "full_code": "1210",
2360
+ "province_id": 38
2361
+ },
2362
+ {
2363
+ "id": 296,
2364
+ "type": "Kota",
2365
+ "name": "Tebing Tinggi",
2366
+ "code": "76",
2367
+ "full_code": "1276",
2368
+ "province_id": 38
2369
+ },
2370
+ {
2371
+ "id": 297,
2372
+ "type": "Kabupaten",
2373
+ "name": "Labuhanbatu Selatan",
2374
+ "code": "22",
2375
+ "full_code": "1222",
2376
+ "province_id": 38
2377
+ },
2378
+ {
2379
+ "id": 298,
2380
+ "type": "Kabupaten",
2381
+ "name": "Tebo",
2382
+ "code": "09",
2383
+ "full_code": "1509",
2384
+ "province_id": 8
2385
+ },
2386
+ {
2387
+ "id": 299,
2388
+ "type": "Kabupaten",
2389
+ "name": "Tegal",
2390
+ "code": "28",
2391
+ "full_code": "3328",
2392
+ "province_id": 10
2393
+ },
2394
+ {
2395
+ "id": 300,
2396
+ "type": "Kota",
2397
+ "name": "Tegal",
2398
+ "code": "76",
2399
+ "full_code": "3376",
2400
+ "province_id": 10
2401
+ },
2402
+ {
2403
+ "id": 301,
2404
+ "type": "Kabupaten",
2405
+ "name": "Teluk Bintuni",
2406
+ "code": "06",
2407
+ "full_code": "9206",
2408
+ "province_id": 25
2409
+ },
2410
+ {
2411
+ "id": 302,
2412
+ "type": "Kabupaten",
2413
+ "name": "Teluk Wondama",
2414
+ "code": "07",
2415
+ "full_code": "9207",
2416
+ "province_id": 25
2417
+ },
2418
+ {
2419
+ "id": 303,
2420
+ "type": "Kabupaten",
2421
+ "name": "Labuhanbatu Utara",
2422
+ "code": "23",
2423
+ "full_code": "1223",
2424
+ "province_id": 38
2425
+ },
2426
+ {
2427
+ "id": 304,
2428
+ "type": "Kabupaten",
2429
+ "name": "Temanggung",
2430
+ "code": "23",
2431
+ "full_code": "3323",
2432
+ "province_id": 10
2433
+ },
2434
+ {
2435
+ "id": 305,
2436
+ "type": "Kabupaten",
2437
+ "name": "Lahat",
2438
+ "code": "04",
2439
+ "full_code": "1604",
2440
+ "province_id": 37
2441
+ },
2442
+ {
2443
+ "id": 306,
2444
+ "type": "Kota",
2445
+ "name": "Ternate",
2446
+ "code": "71",
2447
+ "full_code": "8271",
2448
+ "province_id": 21
2449
+ },
2450
+ {
2451
+ "id": 307,
2452
+ "type": "Kabupaten",
2453
+ "name": "Lamandau",
2454
+ "code": "09",
2455
+ "full_code": "6209",
2456
+ "province_id": 14
2457
+ },
2458
+ {
2459
+ "id": 308,
2460
+ "type": "Kabupaten",
2461
+ "name": "Dompu",
2462
+ "code": "05",
2463
+ "full_code": "5205",
2464
+ "province_id": 22
2465
+ },
2466
+ {
2467
+ "id": 309,
2468
+ "type": "Kabupaten",
2469
+ "name": "Donggala",
2470
+ "code": "03",
2471
+ "full_code": "7203",
2472
+ "province_id": 33
2473
+ },
2474
+ {
2475
+ "id": 310,
2476
+ "type": "Kota",
2477
+ "name": "Dumai",
2478
+ "code": "72",
2479
+ "full_code": "1472",
2480
+ "province_id": 30
2481
+ },
2482
+ {
2483
+ "id": 311,
2484
+ "type": "Kabupaten",
2485
+ "name": "Empat Lawang",
2486
+ "code": "11",
2487
+ "full_code": "1611",
2488
+ "province_id": 37
2489
+ },
2490
+ {
2491
+ "id": 312,
2492
+ "type": "Kabupaten",
2493
+ "name": "Lamongan",
2494
+ "code": "24",
2495
+ "full_code": "3524",
2496
+ "province_id": 11
2497
+ },
2498
+ {
2499
+ "id": 313,
2500
+ "type": "Kabupaten",
2501
+ "name": "Ende",
2502
+ "code": "08",
2503
+ "full_code": "5308",
2504
+ "province_id": 23
2505
+ },
2506
+ {
2507
+ "id": 314,
2508
+ "type": "Kabupaten",
2509
+ "name": "Lampung Barat",
2510
+ "code": "04",
2511
+ "full_code": "1804",
2512
+ "province_id": 19
2513
+ },
2514
+ {
2515
+ "id": 315,
2516
+ "type": "Kota",
2517
+ "name": "Tidore Kepulauan",
2518
+ "code": "72",
2519
+ "full_code": "8272",
2520
+ "province_id": 21
2521
+ },
2522
+ {
2523
+ "id": 316,
2524
+ "type": "Kabupaten",
2525
+ "name": "Enrekang",
2526
+ "code": "16",
2527
+ "full_code": "7316",
2528
+ "province_id": 32
2529
+ },
2530
+ {
2531
+ "id": 317,
2532
+ "type": "Kabupaten",
2533
+ "name": "Lampung Selatan",
2534
+ "code": "01",
2535
+ "full_code": "1801",
2536
+ "province_id": 19
2537
+ },
2538
+ {
2539
+ "id": 318,
2540
+ "type": "Kabupaten",
2541
+ "name": "Fak Fak",
2542
+ "code": "03",
2543
+ "full_code": "9203",
2544
+ "province_id": 25
2545
+ },
2546
+ {
2547
+ "id": 319,
2548
+ "type": "Kabupaten",
2549
+ "name": "Timor Tengah Selatan",
2550
+ "code": "02",
2551
+ "full_code": "5302",
2552
+ "province_id": 23
2553
+ },
2554
+ {
2555
+ "id": 320,
2556
+ "type": "Kabupaten",
2557
+ "name": "Lampung Tengah",
2558
+ "code": "02",
2559
+ "full_code": "1802",
2560
+ "province_id": 19
2561
+ },
2562
+ {
2563
+ "id": 321,
2564
+ "type": "Kabupaten",
2565
+ "name": "Flores Timur",
2566
+ "code": "06",
2567
+ "full_code": "5306",
2568
+ "province_id": 23
2569
+ },
2570
+ {
2571
+ "id": 322,
2572
+ "type": "Kabupaten",
2573
+ "name": "Lampung Timur",
2574
+ "code": "07",
2575
+ "full_code": "1807",
2576
+ "province_id": 19
2577
+ },
2578
+ {
2579
+ "id": 323,
2580
+ "type": "Kabupaten",
2581
+ "name": "Garut",
2582
+ "code": "05",
2583
+ "full_code": "3205",
2584
+ "province_id": 9
2585
+ },
2586
+ {
2587
+ "id": 324,
2588
+ "type": "Kabupaten",
2589
+ "name": "Timor Tengah Utara",
2590
+ "code": "03",
2591
+ "full_code": "5303",
2592
+ "province_id": 23
2593
+ },
2594
+ {
2595
+ "id": 325,
2596
+ "type": "Kabupaten",
2597
+ "name": "Lampung Utara",
2598
+ "code": "03",
2599
+ "full_code": "1803",
2600
+ "province_id": 19
2601
+ },
2602
+ {
2603
+ "id": 326,
2604
+ "type": "Kabupaten",
2605
+ "name": "Toba",
2606
+ "code": "12",
2607
+ "full_code": "1212",
2608
+ "province_id": 38
2609
+ },
2610
+ {
2611
+ "id": 327,
2612
+ "type": "Kabupaten",
2613
+ "name": "Tojo Una Una",
2614
+ "code": "09",
2615
+ "full_code": "7209",
2616
+ "province_id": 33
2617
+ },
2618
+ {
2619
+ "id": 328,
2620
+ "type": "Kabupaten",
2621
+ "name": "Gayo Lues",
2622
+ "code": "13",
2623
+ "full_code": "1113",
2624
+ "province_id": 1
2625
+ },
2626
+ {
2627
+ "id": 329,
2628
+ "type": "Kabupaten",
2629
+ "name": "Landak",
2630
+ "code": "08",
2631
+ "full_code": "6108",
2632
+ "province_id": 12
2633
+ },
2634
+ {
2635
+ "id": 330,
2636
+ "type": "Kabupaten",
2637
+ "name": "Gianyar",
2638
+ "code": "04",
2639
+ "full_code": "5104",
2640
+ "province_id": 2
2641
+ },
2642
+ {
2643
+ "id": 331,
2644
+ "type": "Kabupaten",
2645
+ "name": "Langkat",
2646
+ "code": "05",
2647
+ "full_code": "1205",
2648
+ "province_id": 38
2649
+ },
2650
+ {
2651
+ "id": 332,
2652
+ "type": "Kabupaten",
2653
+ "name": "Gorontalo",
2654
+ "code": "01",
2655
+ "full_code": "7501",
2656
+ "province_id": 7
2657
+ },
2658
+ {
2659
+ "id": 333,
2660
+ "type": "Kota",
2661
+ "name": "Langsa",
2662
+ "code": "74",
2663
+ "full_code": "1174",
2664
+ "province_id": 1
2665
+ },
2666
+ {
2667
+ "id": 334,
2668
+ "type": "Kabupaten",
2669
+ "name": "Toli Toli",
2670
+ "code": "04",
2671
+ "full_code": "7204",
2672
+ "province_id": 33
2673
+ },
2674
+ {
2675
+ "id": 335,
2676
+ "type": "Kabupaten",
2677
+ "name": "Lanny Jaya",
2678
+ "code": "07",
2679
+ "full_code": "9507",
2680
+ "province_id": 27
2681
+ },
2682
+ {
2683
+ "id": 336,
2684
+ "type": "Kabupaten",
2685
+ "name": "Lebak",
2686
+ "code": "02",
2687
+ "full_code": "3602",
2688
+ "province_id": 3
2689
+ },
2690
+ {
2691
+ "id": 337,
2692
+ "type": "Kota",
2693
+ "name": "Gorontalo",
2694
+ "code": "71",
2695
+ "full_code": "7571",
2696
+ "province_id": 7
2697
+ },
2698
+ {
2699
+ "id": 338,
2700
+ "type": "Kabupaten",
2701
+ "name": "Tolikara",
2702
+ "code": "04",
2703
+ "full_code": "9504",
2704
+ "province_id": 27
2705
+ },
2706
+ {
2707
+ "id": 339,
2708
+ "type": "Kota",
2709
+ "name": "Tomohon",
2710
+ "code": "73",
2711
+ "full_code": "7173",
2712
+ "province_id": 35
2713
+ },
2714
+ {
2715
+ "id": 340,
2716
+ "type": "Kabupaten",
2717
+ "name": "Lebong",
2718
+ "code": "07",
2719
+ "full_code": "1707",
2720
+ "province_id": 4
2721
+ },
2722
+ {
2723
+ "id": 341,
2724
+ "type": "Kabupaten",
2725
+ "name": "Toraja Utara",
2726
+ "code": "26",
2727
+ "full_code": "7326",
2728
+ "province_id": 32
2729
+ },
2730
+ {
2731
+ "id": 342,
2732
+ "type": "Kabupaten",
2733
+ "name": "Lembata",
2734
+ "code": "13",
2735
+ "full_code": "5313",
2736
+ "province_id": 23
2737
+ },
2738
+ {
2739
+ "id": 343,
2740
+ "type": "Kota",
2741
+ "name": "Lhokseumawe",
2742
+ "code": "73",
2743
+ "full_code": "1173",
2744
+ "province_id": 1
2745
+ },
2746
+ {
2747
+ "id": 344,
2748
+ "type": "Kabupaten",
2749
+ "name": "Lima Puluh Kota",
2750
+ "code": "07",
2751
+ "full_code": "1307",
2752
+ "province_id": 36
2753
+ },
2754
+ {
2755
+ "id": 345,
2756
+ "type": "Kabupaten",
2757
+ "name": "Lingga",
2758
+ "code": "04",
2759
+ "full_code": "2104",
2760
+ "province_id": 18
2761
+ },
2762
+ {
2763
+ "id": 346,
2764
+ "type": "Kabupaten",
2765
+ "name": "Trenggalek",
2766
+ "code": "03",
2767
+ "full_code": "3503",
2768
+ "province_id": 11
2769
+ },
2770
+ {
2771
+ "id": 347,
2772
+ "type": "Kota",
2773
+ "name": "Tual",
2774
+ "code": "72",
2775
+ "full_code": "8172",
2776
+ "province_id": 20
2777
+ },
2778
+ {
2779
+ "id": 348,
2780
+ "type": "Kabupaten",
2781
+ "name": "Tuban",
2782
+ "code": "23",
2783
+ "full_code": "3523",
2784
+ "province_id": 11
2785
+ },
2786
+ {
2787
+ "id": 349,
2788
+ "type": "Kabupaten",
2789
+ "name": "Lombok Barat",
2790
+ "code": "01",
2791
+ "full_code": "5201",
2792
+ "province_id": 22
2793
+ },
2794
+ {
2795
+ "id": 350,
2796
+ "type": "Kabupaten",
2797
+ "name": "Tulang Bawang",
2798
+ "code": "05",
2799
+ "full_code": "1805",
2800
+ "province_id": 19
2801
+ },
2802
+ {
2803
+ "id": 351,
2804
+ "type": "Kabupaten",
2805
+ "name": "Lombok Tengah",
2806
+ "code": "02",
2807
+ "full_code": "5202",
2808
+ "province_id": 22
2809
+ },
2810
+ {
2811
+ "id": 352,
2812
+ "type": "Kabupaten",
2813
+ "name": "Lombok Timur",
2814
+ "code": "03",
2815
+ "full_code": "5203",
2816
+ "province_id": 22
2817
+ },
2818
+ {
2819
+ "id": 353,
2820
+ "type": "Kabupaten",
2821
+ "name": "Tulang Bawang Barat",
2822
+ "code": "12",
2823
+ "full_code": "1812",
2824
+ "province_id": 19
2825
+ },
2826
+ {
2827
+ "id": 354,
2828
+ "type": "Kabupaten",
2829
+ "name": "Lombok Utara",
2830
+ "code": "08",
2831
+ "full_code": "5208",
2832
+ "province_id": 22
2833
+ },
2834
+ {
2835
+ "id": 355,
2836
+ "type": "Kota",
2837
+ "name": "Lubuk Linggau",
2838
+ "code": "73",
2839
+ "full_code": "1673",
2840
+ "province_id": 37
2841
+ },
2842
+ {
2843
+ "id": 356,
2844
+ "type": "Kabupaten",
2845
+ "name": "Tulungagung",
2846
+ "code": "04",
2847
+ "full_code": "3504",
2848
+ "province_id": 11
2849
+ },
2850
+ {
2851
+ "id": 357,
2852
+ "type": "Kabupaten",
2853
+ "name": "Lumajang",
2854
+ "code": "08",
2855
+ "full_code": "3508",
2856
+ "province_id": 11
2857
+ },
2858
+ {
2859
+ "id": 358,
2860
+ "type": "Kabupaten",
2861
+ "name": "Wajo",
2862
+ "code": "13",
2863
+ "full_code": "7313",
2864
+ "province_id": 32
2865
+ },
2866
+ {
2867
+ "id": 359,
2868
+ "type": "Kabupaten",
2869
+ "name": "Luwu",
2870
+ "code": "17",
2871
+ "full_code": "7317",
2872
+ "province_id": 32
2873
+ },
2874
+ {
2875
+ "id": 360,
2876
+ "type": "Kabupaten",
2877
+ "name": "Luwu Timur",
2878
+ "code": "24",
2879
+ "full_code": "7324",
2880
+ "province_id": 32
2881
+ },
2882
+ {
2883
+ "id": 361,
2884
+ "type": "Kabupaten",
2885
+ "name": "Wakatobi",
2886
+ "code": "07",
2887
+ "full_code": "7407",
2888
+ "province_id": 34
2889
+ },
2890
+ {
2891
+ "id": 362,
2892
+ "type": "Kabupaten",
2893
+ "name": "Waropen",
2894
+ "code": "15",
2895
+ "full_code": "9115",
2896
+ "province_id": 24
2897
+ },
2898
+ {
2899
+ "id": 363,
2900
+ "type": "Kabupaten",
2901
+ "name": "Way Kanan",
2902
+ "code": "08",
2903
+ "full_code": "1808",
2904
+ "province_id": 19
2905
+ },
2906
+ {
2907
+ "id": 364,
2908
+ "type": "Kabupaten",
2909
+ "name": "Wonogiri",
2910
+ "code": "12",
2911
+ "full_code": "3312",
2912
+ "province_id": 10
2913
+ },
2914
+ {
2915
+ "id": 365,
2916
+ "type": "Kabupaten",
2917
+ "name": "Wonosobo",
2918
+ "code": "07",
2919
+ "full_code": "3307",
2920
+ "province_id": 10
2921
+ },
2922
+ {
2923
+ "id": 366,
2924
+ "type": "Kabupaten",
2925
+ "name": "Yahukimo",
2926
+ "code": "03",
2927
+ "full_code": "9503",
2928
+ "province_id": 27
2929
+ },
2930
+ {
2931
+ "id": 367,
2932
+ "type": "Kabupaten",
2933
+ "name": "Yalimo",
2934
+ "code": "06",
2935
+ "full_code": "9506",
2936
+ "province_id": 27
2937
+ },
2938
+ {
2939
+ "id": 368,
2940
+ "type": "Kota",
2941
+ "name": "Yogyakarta",
2942
+ "code": "71",
2943
+ "full_code": "3471",
2944
+ "province_id": 5
2945
+ },
2946
+ {
2947
+ "id": 369,
2948
+ "type": "Kabupaten",
2949
+ "name": "Luwu Utara",
2950
+ "code": "22",
2951
+ "full_code": "7322",
2952
+ "province_id": 32
2953
+ },
2954
+ {
2955
+ "id": 370,
2956
+ "type": "Kabupaten",
2957
+ "name": "Madiun",
2958
+ "code": "19",
2959
+ "full_code": "3519",
2960
+ "province_id": 11
2961
+ },
2962
+ {
2963
+ "id": 371,
2964
+ "type": "Kota",
2965
+ "name": "Madiun",
2966
+ "code": "77",
2967
+ "full_code": "3577",
2968
+ "province_id": 11
2969
+ },
2970
+ {
2971
+ "id": 372,
2972
+ "type": "Kabupaten",
2973
+ "name": "Magelang",
2974
+ "code": "08",
2975
+ "full_code": "3308",
2976
+ "province_id": 10
2977
+ },
2978
+ {
2979
+ "id": 373,
2980
+ "type": "Kota",
2981
+ "name": "Magelang",
2982
+ "code": "71",
2983
+ "full_code": "3371",
2984
+ "province_id": 10
2985
+ },
2986
+ {
2987
+ "id": 374,
2988
+ "type": "Kabupaten",
2989
+ "name": "Magetan",
2990
+ "code": "20",
2991
+ "full_code": "3520",
2992
+ "province_id": 11
2993
+ },
2994
+ {
2995
+ "id": 375,
2996
+ "type": "Kabupaten",
2997
+ "name": "Mahakam Ulu",
2998
+ "code": "11",
2999
+ "full_code": "6411",
3000
+ "province_id": 15
3001
+ },
3002
+ {
3003
+ "id": 376,
3004
+ "type": "Kabupaten",
3005
+ "name": "Majalengka",
3006
+ "code": "10",
3007
+ "full_code": "3210",
3008
+ "province_id": 9
3009
+ },
3010
+ {
3011
+ "id": 377,
3012
+ "type": "Kabupaten",
3013
+ "name": "Majene",
3014
+ "code": "05",
3015
+ "full_code": "7605",
3016
+ "province_id": 31
3017
+ },
3018
+ {
3019
+ "id": 378,
3020
+ "type": "Kota",
3021
+ "name": "Makassar",
3022
+ "code": "71",
3023
+ "full_code": "7371",
3024
+ "province_id": 32
3025
+ },
3026
+ {
3027
+ "id": 379,
3028
+ "type": "Kabupaten",
3029
+ "name": "Malaka",
3030
+ "code": "21",
3031
+ "full_code": "5321",
3032
+ "province_id": 23
3033
+ },
3034
+ {
3035
+ "id": 380,
3036
+ "type": "Kabupaten",
3037
+ "name": "Malang",
3038
+ "code": "07",
3039
+ "full_code": "3507",
3040
+ "province_id": 11
3041
+ },
3042
+ {
3043
+ "id": 381,
3044
+ "type": "Kota",
3045
+ "name": "Malang",
3046
+ "code": "73",
3047
+ "full_code": "3573",
3048
+ "province_id": 11
3049
+ },
3050
+ {
3051
+ "id": 382,
3052
+ "type": "Kabupaten",
3053
+ "name": "Malinau",
3054
+ "code": "02",
3055
+ "full_code": "6502",
3056
+ "province_id": 16
3057
+ },
3058
+ {
3059
+ "id": 383,
3060
+ "type": "Kabupaten",
3061
+ "name": "Maluku Barat Daya",
3062
+ "code": "08",
3063
+ "full_code": "8108",
3064
+ "province_id": 20
3065
+ },
3066
+ {
3067
+ "id": 384,
3068
+ "type": "Kabupaten",
3069
+ "name": "Maluku Tengah",
3070
+ "code": "01",
3071
+ "full_code": "8101",
3072
+ "province_id": 20
3073
+ },
3074
+ {
3075
+ "id": 385,
3076
+ "type": "Kabupaten",
3077
+ "name": "Maluku Tenggara",
3078
+ "code": "02",
3079
+ "full_code": "8102",
3080
+ "province_id": 20
3081
+ },
3082
+ {
3083
+ "id": 386,
3084
+ "type": "Kabupaten",
3085
+ "name": "Mamasa",
3086
+ "code": "03",
3087
+ "full_code": "7603",
3088
+ "province_id": 31
3089
+ },
3090
+ {
3091
+ "id": 387,
3092
+ "type": "Kabupaten",
3093
+ "name": "Mamberamo Raya",
3094
+ "code": "20",
3095
+ "full_code": "9120",
3096
+ "province_id": 24
3097
+ },
3098
+ {
3099
+ "id": 388,
3100
+ "type": "Kabupaten",
3101
+ "name": "Mamberamo Tengah",
3102
+ "code": "05",
3103
+ "full_code": "9505",
3104
+ "province_id": 27
3105
+ },
3106
+ {
3107
+ "id": 389,
3108
+ "type": "Kabupaten",
3109
+ "name": "Mamuju",
3110
+ "code": "02",
3111
+ "full_code": "7602",
3112
+ "province_id": 31
3113
+ },
3114
+ {
3115
+ "id": 390,
3116
+ "type": "Kabupaten",
3117
+ "name": "Mamuju Tengah",
3118
+ "code": "06",
3119
+ "full_code": "7606",
3120
+ "province_id": 31
3121
+ },
3122
+ {
3123
+ "id": 391,
3124
+ "type": "Kota",
3125
+ "name": "Manado",
3126
+ "code": "71",
3127
+ "full_code": "7171",
3128
+ "province_id": 35
3129
+ },
3130
+ {
3131
+ "id": 392,
3132
+ "type": "Kabupaten",
3133
+ "name": "Mandailing Natal",
3134
+ "code": "13",
3135
+ "full_code": "1213",
3136
+ "province_id": 38
3137
+ },
3138
+ {
3139
+ "id": 393,
3140
+ "type": "Kabupaten",
3141
+ "name": "Manggarai",
3142
+ "code": "10",
3143
+ "full_code": "5310",
3144
+ "province_id": 23
3145
+ },
3146
+ {
3147
+ "id": 394,
3148
+ "type": "Kabupaten",
3149
+ "name": "Manggarai Barat",
3150
+ "code": "15",
3151
+ "full_code": "5315",
3152
+ "province_id": 23
3153
+ },
3154
+ {
3155
+ "id": 395,
3156
+ "type": "Kabupaten",
3157
+ "name": "Manggarai Timur",
3158
+ "code": "19",
3159
+ "full_code": "5319",
3160
+ "province_id": 23
3161
+ },
3162
+ {
3163
+ "id": 396,
3164
+ "type": "Kabupaten",
3165
+ "name": "Manokwari",
3166
+ "code": "02",
3167
+ "full_code": "9202",
3168
+ "province_id": 25
3169
+ },
3170
+ {
3171
+ "id": 397,
3172
+ "type": "Kabupaten",
3173
+ "name": "Manokwari Selatan",
3174
+ "code": "11",
3175
+ "full_code": "9211",
3176
+ "province_id": 25
3177
+ },
3178
+ {
3179
+ "id": 398,
3180
+ "type": "Kabupaten",
3181
+ "name": "Mappi",
3182
+ "code": "03",
3183
+ "full_code": "9303",
3184
+ "province_id": 28
3185
+ },
3186
+ {
3187
+ "id": 399,
3188
+ "type": "Kabupaten",
3189
+ "name": "Maros",
3190
+ "code": "09",
3191
+ "full_code": "7309",
3192
+ "province_id": 32
3193
+ },
3194
+ {
3195
+ "id": 400,
3196
+ "type": "Kota",
3197
+ "name": "Mataram",
3198
+ "code": "71",
3199
+ "full_code": "5271",
3200
+ "province_id": 22
3201
+ },
3202
+ {
3203
+ "id": 401,
3204
+ "type": "Kabupaten",
3205
+ "name": "Maybrat",
3206
+ "code": "10",
3207
+ "full_code": "9210",
3208
+ "province_id": 26
3209
+ },
3210
+ {
3211
+ "id": 402,
3212
+ "type": "Kota",
3213
+ "name": "Medan",
3214
+ "code": "71",
3215
+ "full_code": "1271",
3216
+ "province_id": 38
3217
+ },
3218
+ {
3219
+ "id": 403,
3220
+ "type": "Kabupaten",
3221
+ "name": "Melawi",
3222
+ "code": "10",
3223
+ "full_code": "6110",
3224
+ "province_id": 12
3225
+ },
3226
+ {
3227
+ "id": 404,
3228
+ "type": "Kabupaten",
3229
+ "name": "Mempawah",
3230
+ "code": "02",
3231
+ "full_code": "6102",
3232
+ "province_id": 12
3233
+ },
3234
+ {
3235
+ "id": 405,
3236
+ "type": "Kabupaten",
3237
+ "name": "Merangin",
3238
+ "code": "02",
3239
+ "full_code": "1502",
3240
+ "province_id": 8
3241
+ },
3242
+ {
3243
+ "id": 406,
3244
+ "type": "Kabupaten",
3245
+ "name": "Merauke",
3246
+ "code": "01",
3247
+ "full_code": "9301",
3248
+ "province_id": 28
3249
+ },
3250
+ {
3251
+ "id": 407,
3252
+ "type": "Kabupaten",
3253
+ "name": "Mesuji",
3254
+ "code": "11",
3255
+ "full_code": "1811",
3256
+ "province_id": 19
3257
+ },
3258
+ {
3259
+ "id": 408,
3260
+ "type": "Kota",
3261
+ "name": "Metro",
3262
+ "code": "72",
3263
+ "full_code": "1872",
3264
+ "province_id": 19
3265
+ },
3266
+ {
3267
+ "id": 409,
3268
+ "type": "Kabupaten",
3269
+ "name": "Mimika",
3270
+ "code": "04",
3271
+ "full_code": "9404",
3272
+ "province_id": 29
3273
+ },
3274
+ {
3275
+ "id": 410,
3276
+ "type": "Kabupaten",
3277
+ "name": "Minahasa",
3278
+ "code": "02",
3279
+ "full_code": "7102",
3280
+ "province_id": 35
3281
+ },
3282
+ {
3283
+ "id": 411,
3284
+ "type": "Kabupaten",
3285
+ "name": "Minahasa Selatan",
3286
+ "code": "05",
3287
+ "full_code": "7105",
3288
+ "province_id": 35
3289
+ },
3290
+ {
3291
+ "id": 412,
3292
+ "type": "Kabupaten",
3293
+ "name": "Minahasa Tenggara",
3294
+ "code": "07",
3295
+ "full_code": "7107",
3296
+ "province_id": 35
3297
+ },
3298
+ {
3299
+ "id": 413,
3300
+ "type": "Kabupaten",
3301
+ "name": "Minahasa Utara",
3302
+ "code": "06",
3303
+ "full_code": "7106",
3304
+ "province_id": 35
3305
+ },
3306
+ {
3307
+ "id": 414,
3308
+ "type": "Kabupaten",
3309
+ "name": "Mojokerto",
3310
+ "code": "16",
3311
+ "full_code": "3516",
3312
+ "province_id": 11
3313
+ },
3314
+ {
3315
+ "id": 415,
3316
+ "type": "Kota",
3317
+ "name": "Mojokerto",
3318
+ "code": "76",
3319
+ "full_code": "3576",
3320
+ "province_id": 11
3321
+ },
3322
+ {
3323
+ "id": 416,
3324
+ "type": "Kabupaten",
3325
+ "name": "Morowali",
3326
+ "code": "06",
3327
+ "full_code": "7206",
3328
+ "province_id": 33
3329
+ },
3330
+ {
3331
+ "id": 417,
3332
+ "type": "Kabupaten",
3333
+ "name": "Morowali Utara",
3334
+ "code": "12",
3335
+ "full_code": "7212",
3336
+ "province_id": 33
3337
+ },
3338
+ {
3339
+ "id": 418,
3340
+ "type": "Kabupaten",
3341
+ "name": "Muara Enim",
3342
+ "code": "03",
3343
+ "full_code": "1603",
3344
+ "province_id": 37
3345
+ },
3346
+ {
3347
+ "id": 419,
3348
+ "type": "Kabupaten",
3349
+ "name": "Muaro Jambi",
3350
+ "code": "05",
3351
+ "full_code": "1505",
3352
+ "province_id": 8
3353
+ },
3354
+ {
3355
+ "id": 420,
3356
+ "type": "Kabupaten",
3357
+ "name": "Muko Muko",
3358
+ "code": "06",
3359
+ "full_code": "1706",
3360
+ "province_id": 4
3361
+ },
3362
+ {
3363
+ "id": 421,
3364
+ "type": "Kabupaten",
3365
+ "name": "Muna",
3366
+ "code": "03",
3367
+ "full_code": "7403",
3368
+ "province_id": 34
3369
+ },
3370
+ {
3371
+ "id": 422,
3372
+ "type": "Kabupaten",
3373
+ "name": "Muna Barat",
3374
+ "code": "13",
3375
+ "full_code": "7413",
3376
+ "province_id": 34
3377
+ },
3378
+ {
3379
+ "id": 423,
3380
+ "type": "Kabupaten",
3381
+ "name": "Murung Raya",
3382
+ "code": "12",
3383
+ "full_code": "6212",
3384
+ "province_id": 14
3385
+ },
3386
+ {
3387
+ "id": 424,
3388
+ "type": "Kabupaten",
3389
+ "name": "Musi Banyuasin",
3390
+ "code": "06",
3391
+ "full_code": "1606",
3392
+ "province_id": 37
3393
+ },
3394
+ {
3395
+ "id": 425,
3396
+ "type": "Kabupaten",
3397
+ "name": "Musi Rawas",
3398
+ "code": "05",
3399
+ "full_code": "1605",
3400
+ "province_id": 37
3401
+ },
3402
+ {
3403
+ "id": 426,
3404
+ "type": "Kabupaten",
3405
+ "name": "Musi Rawas Utara",
3406
+ "code": "13",
3407
+ "full_code": "1613",
3408
+ "province_id": 37
3409
+ },
3410
+ {
3411
+ "id": 427,
3412
+ "type": "Kabupaten",
3413
+ "name": "Nabire",
3414
+ "code": "01",
3415
+ "full_code": "9401",
3416
+ "province_id": 29
3417
+ },
3418
+ {
3419
+ "id": 428,
3420
+ "type": "Kabupaten",
3421
+ "name": "Nagan Raya",
3422
+ "code": "15",
3423
+ "full_code": "1115",
3424
+ "province_id": 1
3425
+ },
3426
+ {
3427
+ "id": 429,
3428
+ "type": "Kabupaten",
3429
+ "name": "Nagekeo",
3430
+ "code": "16",
3431
+ "full_code": "5316",
3432
+ "province_id": 23
3433
+ },
3434
+ {
3435
+ "id": 430,
3436
+ "type": "Kabupaten",
3437
+ "name": "Natuna",
3438
+ "code": "03",
3439
+ "full_code": "2103",
3440
+ "province_id": 18
3441
+ },
3442
+ {
3443
+ "id": 431,
3444
+ "type": "Kabupaten",
3445
+ "name": "Nduga",
3446
+ "code": "08",
3447
+ "full_code": "9508",
3448
+ "province_id": 27
3449
+ },
3450
+ {
3451
+ "id": 432,
3452
+ "type": "Kabupaten",
3453
+ "name": "Ngada",
3454
+ "code": "09",
3455
+ "full_code": "5309",
3456
+ "province_id": 23
3457
+ },
3458
+ {
3459
+ "id": 433,
3460
+ "type": "Kabupaten",
3461
+ "name": "Nganjuk",
3462
+ "code": "18",
3463
+ "full_code": "3518",
3464
+ "province_id": 11
3465
+ },
3466
+ {
3467
+ "id": 434,
3468
+ "type": "Kabupaten",
3469
+ "name": "Ngawi",
3470
+ "code": "21",
3471
+ "full_code": "3521",
3472
+ "province_id": 11
3473
+ },
3474
+ {
3475
+ "id": 435,
3476
+ "type": "Kabupaten",
3477
+ "name": "Nias",
3478
+ "code": "04",
3479
+ "full_code": "1204",
3480
+ "province_id": 38
3481
+ },
3482
+ {
3483
+ "id": 436,
3484
+ "type": "Kabupaten",
3485
+ "name": "Nias Barat",
3486
+ "code": "25",
3487
+ "full_code": "1225",
3488
+ "province_id": 38
3489
+ },
3490
+ {
3491
+ "id": 437,
3492
+ "type": "Kabupaten",
3493
+ "name": "Nias Selatan",
3494
+ "code": "14",
3495
+ "full_code": "1214",
3496
+ "province_id": 38
3497
+ },
3498
+ {
3499
+ "id": 438,
3500
+ "type": "Kabupaten",
3501
+ "name": "Nias Utara",
3502
+ "code": "24",
3503
+ "full_code": "1224",
3504
+ "province_id": 38
3505
+ },
3506
+ {
3507
+ "id": 439,
3508
+ "type": "Kabupaten",
3509
+ "name": "Nunukan",
3510
+ "code": "03",
3511
+ "full_code": "6503",
3512
+ "province_id": 16
3513
+ },
3514
+ {
3515
+ "id": 440,
3516
+ "type": "Kabupaten",
3517
+ "name": "Ogan Ilir",
3518
+ "code": "10",
3519
+ "full_code": "1610",
3520
+ "province_id": 37
3521
+ },
3522
+ {
3523
+ "id": 441,
3524
+ "type": "Kabupaten",
3525
+ "name": "Ogan Komering Ilir",
3526
+ "code": "02",
3527
+ "full_code": "1602",
3528
+ "province_id": 37
3529
+ },
3530
+ {
3531
+ "id": 442,
3532
+ "type": "Kabupaten",
3533
+ "name": "Ogan Komering Ulu",
3534
+ "code": "01",
3535
+ "full_code": "1601",
3536
+ "province_id": 37
3537
+ },
3538
+ {
3539
+ "id": 443,
3540
+ "type": "Kabupaten",
3541
+ "name": "Ogan Komering Ulu Selatan",
3542
+ "code": "09",
3543
+ "full_code": "1609",
3544
+ "province_id": 37
3545
+ },
3546
+ {
3547
+ "id": 444,
3548
+ "type": "Kabupaten",
3549
+ "name": "Ogan Komering Ulu Timur",
3550
+ "code": "08",
3551
+ "full_code": "1608",
3552
+ "province_id": 37
3553
+ },
3554
+ {
3555
+ "id": 445,
3556
+ "type": "Kabupaten",
3557
+ "name": "Pacitan",
3558
+ "code": "01",
3559
+ "full_code": "3501",
3560
+ "province_id": 11
3561
+ },
3562
+ {
3563
+ "id": 446,
3564
+ "type": "Kota",
3565
+ "name": "Padang",
3566
+ "code": "71",
3567
+ "full_code": "1371",
3568
+ "province_id": 36
3569
+ },
3570
+ {
3571
+ "id": 447,
3572
+ "type": "Kabupaten",
3573
+ "name": "Padang Lawas",
3574
+ "code": "21",
3575
+ "full_code": "1221",
3576
+ "province_id": 38
3577
+ },
3578
+ {
3579
+ "id": 448,
3580
+ "type": "Kabupaten",
3581
+ "name": "Padang Lawas Utara",
3582
+ "code": "20",
3583
+ "full_code": "1220",
3584
+ "province_id": 38
3585
+ },
3586
+ {
3587
+ "id": 449,
3588
+ "type": "Kota",
3589
+ "name": "Padang Panjang",
3590
+ "code": "74",
3591
+ "full_code": "1374",
3592
+ "province_id": 36
3593
+ },
3594
+ {
3595
+ "id": 450,
3596
+ "type": "Kabupaten",
3597
+ "name": "Padang Pariaman",
3598
+ "code": "05",
3599
+ "full_code": "1305",
3600
+ "province_id": 36
3601
+ },
3602
+ {
3603
+ "id": 451,
3604
+ "type": "Kota",
3605
+ "name": "Padangsidimpuan",
3606
+ "code": "77",
3607
+ "full_code": "1277",
3608
+ "province_id": 38
3609
+ },
3610
+ {
3611
+ "id": 452,
3612
+ "type": "Kota",
3613
+ "name": "Pagar Alam",
3614
+ "code": "72",
3615
+ "full_code": "1672",
3616
+ "province_id": 37
3617
+ },
3618
+ {
3619
+ "id": 453,
3620
+ "type": "Kabupaten",
3621
+ "name": "Pahuwato",
3622
+ "code": "04",
3623
+ "full_code": "7504",
3624
+ "province_id": 7
3625
+ },
3626
+ {
3627
+ "id": 454,
3628
+ "type": "Kabupaten",
3629
+ "name": "Pakpak Bharat",
3630
+ "code": "15",
3631
+ "full_code": "1215",
3632
+ "province_id": 38
3633
+ },
3634
+ {
3635
+ "id": 455,
3636
+ "type": "Kota",
3637
+ "name": "Palangkaraya",
3638
+ "code": "71",
3639
+ "full_code": "6271",
3640
+ "province_id": 14
3641
+ },
3642
+ {
3643
+ "id": 456,
3644
+ "type": "Kota",
3645
+ "name": "Palembang",
3646
+ "code": "71",
3647
+ "full_code": "1671",
3648
+ "province_id": 37
3649
+ },
3650
+ {
3651
+ "id": 457,
3652
+ "type": "Kota",
3653
+ "name": "Palopo",
3654
+ "code": "73",
3655
+ "full_code": "7373",
3656
+ "province_id": 32
3657
+ },
3658
+ {
3659
+ "id": 458,
3660
+ "type": "Kota",
3661
+ "name": "Palu",
3662
+ "code": "71",
3663
+ "full_code": "7271",
3664
+ "province_id": 33
3665
+ },
3666
+ {
3667
+ "id": 459,
3668
+ "type": "Kabupaten",
3669
+ "name": "Pamekasan",
3670
+ "code": "28",
3671
+ "full_code": "3528",
3672
+ "province_id": 11
3673
+ },
3674
+ {
3675
+ "id": 460,
3676
+ "type": "Kabupaten",
3677
+ "name": "Pandeglang",
3678
+ "code": "01",
3679
+ "full_code": "3601",
3680
+ "province_id": 3
3681
+ },
3682
+ {
3683
+ "id": 461,
3684
+ "type": "Kabupaten",
3685
+ "name": "Pangandaran",
3686
+ "code": "18",
3687
+ "full_code": "3218",
3688
+ "province_id": 9
3689
+ },
3690
+ {
3691
+ "id": 462,
3692
+ "type": "Kabupaten",
3693
+ "name": "Pangkajene Kepulauan",
3694
+ "code": "10",
3695
+ "full_code": "7310",
3696
+ "province_id": 32
3697
+ },
3698
+ {
3699
+ "id": 463,
3700
+ "type": "Kota",
3701
+ "name": "Pangkal Pinang",
3702
+ "code": "71",
3703
+ "full_code": "1971",
3704
+ "province_id": 17
3705
+ },
3706
+ {
3707
+ "id": 464,
3708
+ "type": "Kabupaten",
3709
+ "name": "Paniai",
3710
+ "code": "03",
3711
+ "full_code": "9403",
3712
+ "province_id": 29
3713
+ },
3714
+ {
3715
+ "id": 465,
3716
+ "type": "Kota",
3717
+ "name": "Pare Pare",
3718
+ "code": "72",
3719
+ "full_code": "7372",
3720
+ "province_id": 32
3721
+ },
3722
+ {
3723
+ "id": 466,
3724
+ "type": "Kota",
3725
+ "name": "Pariaman",
3726
+ "code": "77",
3727
+ "full_code": "1377",
3728
+ "province_id": 36
3729
+ },
3730
+ {
3731
+ "id": 467,
3732
+ "type": "Kabupaten",
3733
+ "name": "Parigi Moutong",
3734
+ "code": "08",
3735
+ "full_code": "7208",
3736
+ "province_id": 33
3737
+ },
3738
+ {
3739
+ "id": 468,
3740
+ "type": "Kabupaten",
3741
+ "name": "Pasaman",
3742
+ "code": "08",
3743
+ "full_code": "1308",
3744
+ "province_id": 36
3745
+ },
3746
+ {
3747
+ "id": 469,
3748
+ "type": "Kabupaten",
3749
+ "name": "Pasaman Barat",
3750
+ "code": "12",
3751
+ "full_code": "1312",
3752
+ "province_id": 36
3753
+ },
3754
+ {
3755
+ "id": 470,
3756
+ "type": "Kabupaten",
3757
+ "name": "Pasangkayu (Mamuju Utara)",
3758
+ "code": "01",
3759
+ "full_code": "7601",
3760
+ "province_id": 31
3761
+ },
3762
+ {
3763
+ "id": 471,
3764
+ "type": "Kabupaten",
3765
+ "name": "Paser",
3766
+ "code": "01",
3767
+ "full_code": "6401",
3768
+ "province_id": 15
3769
+ },
3770
+ {
3771
+ "id": 472,
3772
+ "type": "Kabupaten",
3773
+ "name": "Pasuruan",
3774
+ "code": "14",
3775
+ "full_code": "3514",
3776
+ "province_id": 11
3777
+ },
3778
+ {
3779
+ "id": 473,
3780
+ "type": "Kota",
3781
+ "name": "Pasuruan",
3782
+ "code": "75",
3783
+ "full_code": "3575",
3784
+ "province_id": 11
3785
+ },
3786
+ {
3787
+ "id": 474,
3788
+ "type": "Kabupaten",
3789
+ "name": "Pati",
3790
+ "code": "18",
3791
+ "full_code": "3318",
3792
+ "province_id": 10
3793
+ },
3794
+ {
3795
+ "id": 475,
3796
+ "type": "Kota",
3797
+ "name": "Payakumbuh",
3798
+ "code": "76",
3799
+ "full_code": "1376",
3800
+ "province_id": 36
3801
+ },
3802
+ {
3803
+ "id": 476,
3804
+ "type": "Kabupaten",
3805
+ "name": "Pegunungan Arfak",
3806
+ "code": "12",
3807
+ "full_code": "9212",
3808
+ "province_id": 25
3809
+ },
3810
+ {
3811
+ "id": 477,
3812
+ "type": "Kabupaten",
3813
+ "name": "Pegunungan Bintang",
3814
+ "code": "02",
3815
+ "full_code": "9502",
3816
+ "province_id": 27
3817
+ },
3818
+ {
3819
+ "id": 478,
3820
+ "type": "Kabupaten",
3821
+ "name": "Pekalongan",
3822
+ "code": "26",
3823
+ "full_code": "3326",
3824
+ "province_id": 10
3825
+ },
3826
+ {
3827
+ "id": 479,
3828
+ "type": "Kota",
3829
+ "name": "Pekalongan",
3830
+ "code": "75",
3831
+ "full_code": "3375",
3832
+ "province_id": 10
3833
+ },
3834
+ {
3835
+ "id": 480,
3836
+ "type": "Kota",
3837
+ "name": "Pekanbaru",
3838
+ "code": "71",
3839
+ "full_code": "1471",
3840
+ "province_id": 30
3841
+ },
3842
+ {
3843
+ "id": 481,
3844
+ "type": "Kabupaten",
3845
+ "name": "Pelalawan",
3846
+ "code": "05",
3847
+ "full_code": "1405",
3848
+ "province_id": 30
3849
+ },
3850
+ {
3851
+ "id": 482,
3852
+ "type": "Kabupaten",
3853
+ "name": "Pemalang",
3854
+ "code": "27",
3855
+ "full_code": "3327",
3856
+ "province_id": 10
3857
+ },
3858
+ {
3859
+ "id": 483,
3860
+ "type": "Kota",
3861
+ "name": "Pematangsiantar",
3862
+ "code": "72",
3863
+ "full_code": "1272",
3864
+ "province_id": 38
3865
+ },
3866
+ {
3867
+ "id": 484,
3868
+ "type": "Kabupaten",
3869
+ "name": "Penajam Paser Utara",
3870
+ "code": "09",
3871
+ "full_code": "6409",
3872
+ "province_id": 15
3873
+ },
3874
+ {
3875
+ "id": 485,
3876
+ "type": "Kabupaten",
3877
+ "name": "Penukal Abab Lematang Ilir",
3878
+ "code": "12",
3879
+ "full_code": "1612",
3880
+ "province_id": 37
3881
+ },
3882
+ {
3883
+ "id": 486,
3884
+ "type": "Kabupaten",
3885
+ "name": "Pesawaran",
3886
+ "code": "09",
3887
+ "full_code": "1809",
3888
+ "province_id": 19
3889
+ },
3890
+ {
3891
+ "id": 487,
3892
+ "type": "Kabupaten",
3893
+ "name": "Pesisir Barat",
3894
+ "code": "13",
3895
+ "full_code": "1813",
3896
+ "province_id": 19
3897
+ },
3898
+ {
3899
+ "id": 488,
3900
+ "type": "Kabupaten",
3901
+ "name": "Pesisir Selatan",
3902
+ "code": "01",
3903
+ "full_code": "1301",
3904
+ "province_id": 36
3905
+ },
3906
+ {
3907
+ "id": 489,
3908
+ "type": "Kabupaten",
3909
+ "name": "Pidie",
3910
+ "code": "07",
3911
+ "full_code": "1107",
3912
+ "province_id": 1
3913
+ },
3914
+ {
3915
+ "id": 490,
3916
+ "type": "Kabupaten",
3917
+ "name": "Pidie Jaya",
3918
+ "code": "18",
3919
+ "full_code": "1118",
3920
+ "province_id": 1
3921
+ },
3922
+ {
3923
+ "id": 491,
3924
+ "type": "Kabupaten",
3925
+ "name": "Pinrang",
3926
+ "code": "15",
3927
+ "full_code": "7315",
3928
+ "province_id": 32
3929
+ },
3930
+ {
3931
+ "id": 492,
3932
+ "type": "Kabupaten",
3933
+ "name": "Polewali Mandar",
3934
+ "code": "04",
3935
+ "full_code": "7604",
3936
+ "province_id": 31
3937
+ },
3938
+ {
3939
+ "id": 493,
3940
+ "type": "Kabupaten",
3941
+ "name": "Ponorogo",
3942
+ "code": "02",
3943
+ "full_code": "3502",
3944
+ "province_id": 11
3945
+ },
3946
+ {
3947
+ "id": 494,
3948
+ "type": "Kota",
3949
+ "name": "Pontianak",
3950
+ "code": "71",
3951
+ "full_code": "6171",
3952
+ "province_id": 12
3953
+ },
3954
+ {
3955
+ "id": 495,
3956
+ "type": "Kabupaten",
3957
+ "name": "Poso",
3958
+ "code": "02",
3959
+ "full_code": "7202",
3960
+ "province_id": 33
3961
+ },
3962
+ {
3963
+ "id": 496,
3964
+ "type": "Kota",
3965
+ "name": "Prabumulih",
3966
+ "code": "74",
3967
+ "full_code": "1674",
3968
+ "province_id": 37
3969
+ },
3970
+ {
3971
+ "id": 497,
3972
+ "type": "Kabupaten",
3973
+ "name": "Pringsewu",
3974
+ "code": "10",
3975
+ "full_code": "1810",
3976
+ "province_id": 19
3977
+ },
3978
+ {
3979
+ "id": 498,
3980
+ "type": "Kabupaten",
3981
+ "name": "Probolinggo",
3982
+ "code": "13",
3983
+ "full_code": "3513",
3984
+ "province_id": 11
3985
+ },
3986
+ {
3987
+ "id": 499,
3988
+ "type": "Kota",
3989
+ "name": "Probolinggo",
3990
+ "code": "74",
3991
+ "full_code": "3574",
3992
+ "province_id": 11
3993
+ },
3994
+ {
3995
+ "id": 500,
3996
+ "type": "Kabupaten",
3997
+ "name": "Pulang Pisau",
3998
+ "code": "11",
3999
+ "full_code": "6211",
4000
+ "province_id": 14
4001
+ },
4002
+ {
4003
+ "id": 501,
4004
+ "type": "Kabupaten",
4005
+ "name": "Pulau Morotai",
4006
+ "code": "07",
4007
+ "full_code": "8207",
4008
+ "province_id": 21
4009
+ },
4010
+ {
4011
+ "id": 502,
4012
+ "type": "Kabupaten",
4013
+ "name": "Pulau Taliabu",
4014
+ "code": "08",
4015
+ "full_code": "8208",
4016
+ "province_id": 21
4017
+ },
4018
+ {
4019
+ "id": 503,
4020
+ "type": "Kabupaten",
4021
+ "name": "Puncak",
4022
+ "code": "05",
4023
+ "full_code": "9405",
4024
+ "province_id": 29
4025
+ },
4026
+ {
4027
+ "id": 504,
4028
+ "type": "Kabupaten",
4029
+ "name": "Puncak Jaya",
4030
+ "code": "02",
4031
+ "full_code": "9402",
4032
+ "province_id": 29
4033
+ },
4034
+ {
4035
+ "id": 505,
4036
+ "type": "Kabupaten",
4037
+ "name": "Purbalingga",
4038
+ "code": "03",
4039
+ "full_code": "3303",
4040
+ "province_id": 10
4041
+ },
4042
+ {
4043
+ "id": 506,
4044
+ "type": "Kabupaten",
4045
+ "name": "Purwakarta",
4046
+ "code": "14",
4047
+ "full_code": "3214",
4048
+ "province_id": 9
4049
+ },
4050
+ {
4051
+ "id": 507,
4052
+ "type": "Kabupaten",
4053
+ "name": "Purworejo",
4054
+ "code": "06",
4055
+ "full_code": "3306",
4056
+ "province_id": 10
4057
+ },
4058
+ {
4059
+ "id": 508,
4060
+ "type": "Kabupaten",
4061
+ "name": "Raja Ampat",
4062
+ "code": "05",
4063
+ "full_code": "9205",
4064
+ "province_id": 26
4065
+ },
4066
+ {
4067
+ "id": 509,
4068
+ "type": "Kabupaten",
4069
+ "name": "Rejang Lebong",
4070
+ "code": "02",
4071
+ "full_code": "1702",
4072
+ "province_id": 4
4073
+ },
4074
+ {
4075
+ "id": 510,
4076
+ "type": "Kabupaten",
4077
+ "name": "Rembang",
4078
+ "code": "17",
4079
+ "full_code": "3317",
4080
+ "province_id": 10
4081
+ },
4082
+ {
4083
+ "id": 511,
4084
+ "type": "Kabupaten",
4085
+ "name": "Rokan Hilir",
4086
+ "code": "07",
4087
+ "full_code": "1407",
4088
+ "province_id": 30
4089
+ },
4090
+ {
4091
+ "id": 512,
4092
+ "type": "Kabupaten",
4093
+ "name": "Rokan Hulu",
4094
+ "code": "06",
4095
+ "full_code": "1406",
4096
+ "province_id": 30
4097
+ },
4098
+ {
4099
+ "id": 513,
4100
+ "type": "Kabupaten",
4101
+ "name": "Rote Ndao",
4102
+ "code": "14",
4103
+ "full_code": "5314",
4104
+ "province_id": 23
4105
+ },
4106
+ {
4107
+ "id": 514,
4108
+ "type": "Kota",
4109
+ "name": "Sabang",
4110
+ "code": "72",
4111
+ "full_code": "1172",
4112
+ "province_id": 1
4113
+ }
4114
+ ]
space/space/space/space/space/space/assets/seeds/province.json ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": 1,
4
+ "name": "Aceh (NAD)",
5
+ "code": "11"
6
+ },
7
+ {
8
+ "id": 2,
9
+ "name": "Bali",
10
+ "code": "51"
11
+ },
12
+ {
13
+ "id": 3,
14
+ "name": "Banten",
15
+ "code": "36"
16
+ },
17
+ {
18
+ "id": 4,
19
+ "name": "Bengkulu",
20
+ "code": "17"
21
+ },
22
+ {
23
+ "id": 5,
24
+ "name": "DI Yogyakarta",
25
+ "code": "34"
26
+ },
27
+ {
28
+ "id": 6,
29
+ "name": "DKI Jakarta",
30
+ "code": "31"
31
+ },
32
+ {
33
+ "id": 7,
34
+ "name": "Gorontalo",
35
+ "code": "75"
36
+ },
37
+ {
38
+ "id": 8,
39
+ "name": "Jambi",
40
+ "code": "15"
41
+ },
42
+ {
43
+ "id": 9,
44
+ "name": "Jawa Barat",
45
+ "code": "32"
46
+ },
47
+ {
48
+ "id": 10,
49
+ "name": "Jawa Tengah",
50
+ "code": "33"
51
+ },
52
+ {
53
+ "id": 11,
54
+ "name": "Jawa Timur",
55
+ "code": "35"
56
+ },
57
+ {
58
+ "id": 12,
59
+ "name": "Kalimantan Barat",
60
+ "code": "61"
61
+ },
62
+ {
63
+ "id": 13,
64
+ "name": "Kalimantan Selatan",
65
+ "code": "63"
66
+ },
67
+ {
68
+ "id": 14,
69
+ "name": "Kalimantan Tengah",
70
+ "code": "62"
71
+ },
72
+ {
73
+ "id": 15,
74
+ "name": "Kalimantan Timur",
75
+ "code": "64"
76
+ },
77
+ {
78
+ "id": 16,
79
+ "name": "Kalimantan Utara",
80
+ "code": "65"
81
+ },
82
+ {
83
+ "id": 17,
84
+ "name": "Kepulauan Bangka Belitung",
85
+ "code": "19"
86
+ },
87
+ {
88
+ "id": 18,
89
+ "name": "Kepulauan Riau",
90
+ "code": "21"
91
+ },
92
+ {
93
+ "id": 19,
94
+ "name": "Lampung",
95
+ "code": "18"
96
+ },
97
+ {
98
+ "id": 20,
99
+ "name": "Maluku",
100
+ "code": "81"
101
+ },
102
+ {
103
+ "id": 21,
104
+ "name": "Maluku Utara",
105
+ "code": "82"
106
+ },
107
+ {
108
+ "id": 22,
109
+ "name": "Nusa Tenggara Barat (NTB)",
110
+ "code": "52"
111
+ },
112
+ {
113
+ "id": 23,
114
+ "name": "Nusa Tenggara Timur (NTT)",
115
+ "code": "53"
116
+ },
117
+ {
118
+ "id": 24,
119
+ "name": "Papua",
120
+ "code": "91"
121
+ },
122
+ {
123
+ "id": 25,
124
+ "name": "Papua Barat",
125
+ "code": "92"
126
+ },
127
+ {
128
+ "id": 26,
129
+ "name": "Papua Barat Daya",
130
+ "code": "92"
131
+ },
132
+ {
133
+ "id": 27,
134
+ "name": "Papua Pegunungan",
135
+ "code": "95"
136
+ },
137
+ {
138
+ "id": 28,
139
+ "name": "Papua Selatan",
140
+ "code": "93"
141
+ },
142
+ {
143
+ "id": 29,
144
+ "name": "Papua Tengah",
145
+ "code": "94"
146
+ },
147
+ {
148
+ "id": 30,
149
+ "name": "Riau",
150
+ "code": "14"
151
+ },
152
+ {
153
+ "id": 31,
154
+ "name": "Sulawesi Barat",
155
+ "code": "76"
156
+ },
157
+ {
158
+ "id": 32,
159
+ "name": "Sulawesi Selatan",
160
+ "code": "73"
161
+ },
162
+ {
163
+ "id": 33,
164
+ "name": "Sulawesi Tengah",
165
+ "code": "72"
166
+ },
167
+ {
168
+ "id": 34,
169
+ "name": "Sulawesi Tenggara",
170
+ "code": "74"
171
+ },
172
+ {
173
+ "id": 35,
174
+ "name": "Sulawesi Utara",
175
+ "code": "71"
176
+ },
177
+ {
178
+ "id": 36,
179
+ "name": "Sumatera Barat",
180
+ "code": "13"
181
+ },
182
+ {
183
+ "id": 37,
184
+ "name": "Sumatera Selatan",
185
+ "code": "16"
186
+ },
187
+ {
188
+ "id": 38,
189
+ "name": "Sumatera Utara",
190
+ "code": "12"
191
+ }
192
+ ]
space/space/space/space/space/space/controller/options/options_controller.go ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package options_controller
2
+
3
+ import (
4
+ "net/http"
5
+
6
+ "api.qobiltu.id/models"
7
+ "api.qobiltu.id/response"
8
+ "api.qobiltu.id/services"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ type OptionsController interface {
13
+ CreateOptions(ctx *gin.Context)
14
+ ListOptions(ctx *gin.Context)
15
+ ListOptionsBySlug(ctx *gin.Context)
16
+ }
17
+
18
+ type optionsController struct {
19
+ optionService services.OptionsService
20
+ }
21
+
22
+ func NewOptionsController(optionService services.OptionsService) OptionsController {
23
+ return &optionsController{
24
+ optionService: optionService,
25
+ }
26
+ }
27
+
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
+
39
+ res, err := c.optionService.CreateOptions(ctx, &models.CreateOptionsRequest{
40
+ Options: req,
41
+ })
42
+ if err != nil {
43
+ response.HandleError(ctx, err)
44
+ return
45
+ }
46
+
47
+ response.HandleSuccess(ctx, http.StatusOK, "Options created successfully", res, nil)
48
+ }
49
+
50
+ func (c *optionsController) ListOptions(ctx *gin.Context) {
51
+ res, err := c.optionService.ListOptions(ctx)
52
+ if err != nil {
53
+ response.HandleError(ctx, err)
54
+ return
55
+ }
56
+
57
+ response.HandleSuccess(ctx, http.StatusOK, "Options retrieved successfully", res.Options, nil)
58
+ }
59
+
60
+ func (c *optionsController) ListOptionsBySlug(ctx *gin.Context) {
61
+ slug := ctx.Param("slug")
62
+
63
+ res, err := c.optionService.ListOptionsBySlug(ctx, &models.ListOptionsBySlugRequest{
64
+ Slug: slug,
65
+ })
66
+ if err != nil {
67
+ response.HandleError(ctx, err)
68
+ return
69
+ }
70
+
71
+ response.HandleSuccess(ctx, http.StatusOK, "Options retrieved successfully", res, nil)
72
+ }