@hed-hog/lms 0.0.358 → 0.0.364

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +65 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +72 -0
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
  6. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  7. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
  8. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
  9. package/dist/class-group/class-group.controller.d.ts +16 -16
  10. package/dist/class-group/class-group.service.d.ts +12 -12
  11. package/dist/course/course-audio-transcription.service.d.ts +3 -2
  12. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  13. package/dist/course/course-audio-transcription.service.js +49 -8
  14. package/dist/course/course-audio-transcription.service.js.map +1 -1
  15. package/dist/course/course-lesson.controller.d.ts +4 -0
  16. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  17. package/dist/course/course-lesson.controller.js +10 -0
  18. package/dist/course/course-lesson.controller.js.map +1 -1
  19. package/dist/course/course-structure.controller.d.ts +11 -4
  20. package/dist/course/course-structure.controller.d.ts.map +1 -1
  21. package/dist/course/course-structure.controller.js +14 -0
  22. package/dist/course/course-structure.controller.js.map +1 -1
  23. package/dist/course/course-structure.service.d.ts +15 -1
  24. package/dist/course/course-structure.service.d.ts.map +1 -1
  25. package/dist/course/course-structure.service.js +139 -4
  26. package/dist/course/course-structure.service.js.map +1 -1
  27. package/dist/course/course-video-conversion.service.d.ts +8 -0
  28. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  29. package/dist/course/course-video-conversion.service.js +87 -51
  30. package/dist/course/course-video-conversion.service.js.map +1 -1
  31. package/dist/course/course.controller.d.ts +73 -1
  32. package/dist/course/course.controller.d.ts.map +1 -1
  33. package/dist/course/course.controller.js +27 -3
  34. package/dist/course/course.controller.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +15 -3
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +108 -4
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +631 -30
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
  44. package/dist/course/dto/cleanup-course-storage.dto.js +33 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
  48. package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +4 -0
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
  52. package/dist/course/dto/create-course-bulk-job.dto.js +21 -0
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
  54. package/dist/course/lms-bulk-upload-automation.service.d.ts +39 -0
  55. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
  56. package/dist/course/lms-bulk-upload-automation.service.js +443 -0
  57. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
  58. package/dist/course/lms-bulk-upload-infra.service.d.ts +31 -0
  59. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
  60. package/dist/course/lms-bulk-upload-infra.service.js +277 -0
  61. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
  62. package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
  63. package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
  64. package/dist/course/lms-bulk-upload.constants.js +7 -0
  65. package/dist/course/lms-bulk-upload.constants.js.map +1 -0
  66. package/dist/course/lms-bulk-upload.controller.d.ts +153 -0
  67. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -0
  68. package/dist/course/lms-bulk-upload.controller.js +129 -0
  69. package/dist/course/lms-bulk-upload.controller.js.map +1 -0
  70. package/dist/course/lms-bulk-upload.service.d.ts +181 -0
  71. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -0
  72. package/dist/course/lms-bulk-upload.service.js +754 -0
  73. package/dist/course/lms-bulk-upload.service.js.map +1 -0
  74. package/dist/course/lms-setting.controller.d.ts +7 -1
  75. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  76. package/dist/course/lms-setting.controller.js +26 -3
  77. package/dist/course/lms-setting.controller.js.map +1 -1
  78. package/dist/enterprise/enterprise.controller.d.ts +20 -20
  79. package/dist/enterprise/enterprise.service.d.ts +20 -20
  80. package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
  81. package/dist/enterprise/training/training-admin.service.d.ts +11 -11
  82. package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
  83. package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
  84. package/dist/enterprise/training/training-student.controller.d.ts +1 -1
  85. package/dist/enterprise/training/training-student.service.d.ts +1 -1
  86. package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
  87. package/dist/evaluation/evaluation.controller.d.ts +8 -8
  88. package/dist/evaluation/evaluation.service.d.ts +8 -8
  89. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
  90. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
  91. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
  92. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
  93. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
  94. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
  95. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
  96. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
  97. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
  98. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
  99. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
  100. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
  101. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
  102. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
  103. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
  104. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
  105. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
  106. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
  107. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
  108. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +26 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +304 -0
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
  113. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
  114. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
  115. package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
  116. package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
  117. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
  118. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
  119. package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
  120. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
  121. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
  122. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
  123. package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
  124. package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
  126. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
  127. package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
  128. package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
  129. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
  130. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
  131. package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
  132. package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
  133. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +17 -2
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
  138. package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/update-profile.dto.js +87 -0
  140. package/dist/platforma/dto/update-profile.dto.js.map +1 -0
  141. package/dist/platforma/platforma.controller.d.ts +94 -7
  142. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  143. package/dist/platforma/platforma.controller.js +85 -2
  144. package/dist/platforma/platforma.controller.js.map +1 -1
  145. package/dist/platforma/platforma.service.d.ts +27 -0
  146. package/dist/platforma/platforma.service.d.ts.map +1 -0
  147. package/dist/platforma/platforma.service.js +274 -0
  148. package/dist/platforma/platforma.service.js.map +1 -0
  149. package/dist/student-xp/student-xp.controller.d.ts +41 -0
  150. package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
  151. package/dist/student-xp/student-xp.controller.js +114 -0
  152. package/dist/student-xp/student-xp.controller.js.map +1 -0
  153. package/dist/student-xp/student-xp.module.d.ts +3 -0
  154. package/dist/student-xp/student-xp.module.d.ts.map +1 -0
  155. package/dist/student-xp/student-xp.module.js +25 -0
  156. package/dist/student-xp/student-xp.module.js.map +1 -0
  157. package/dist/student-xp/student-xp.service.d.ts +65 -0
  158. package/dist/student-xp/student-xp.service.d.ts.map +1 -0
  159. package/dist/student-xp/student-xp.service.js +197 -0
  160. package/dist/student-xp/student-xp.service.js.map +1 -0
  161. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
  162. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
  163. package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
  164. package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
  165. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
  166. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
  167. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
  168. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
  169. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
  170. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
  171. package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
  172. package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
  173. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
  174. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
  175. package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
  176. package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
  177. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
  178. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
  179. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
  180. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
  181. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
  182. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
  183. package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
  184. package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
  185. package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
  186. package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
  187. package/dist/xp-catalog/xp-area.controller.js +105 -0
  188. package/dist/xp-catalog/xp-area.controller.js.map +1 -0
  189. package/dist/xp-catalog/xp-area.service.d.ts +35 -0
  190. package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
  191. package/dist/xp-catalog/xp-area.service.js +168 -0
  192. package/dist/xp-catalog/xp-area.service.js.map +1 -0
  193. package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
  194. package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
  195. package/dist/xp-catalog/xp-catalog.module.js +29 -0
  196. package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
  197. package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
  198. package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
  199. package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
  200. package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
  201. package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
  202. package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
  203. package/dist/xp-catalog/xp-learning-type.service.js +146 -0
  204. package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
  205. package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
  206. package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
  207. package/dist/xp-catalog/xp-skill.controller.js +113 -0
  208. package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
  209. package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
  210. package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
  211. package/dist/xp-catalog/xp-skill.service.js +174 -0
  212. package/dist/xp-catalog/xp-skill.service.js.map +1 -0
  213. package/hedhog/data/menu.yaml +101 -0
  214. package/hedhog/data/role.yaml +8 -0
  215. package/hedhog/data/route.yaml +547 -0
  216. package/hedhog/data/setting_group.yaml +33 -0
  217. package/hedhog/data/xp_area.yaml +164 -0
  218. package/hedhog/data/xp_learning_type.yaml +131 -0
  219. package/hedhog/data/xp_skill.yaml +1834 -0
  220. package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
  221. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
  222. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1453 -0
  223. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
  224. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
  225. package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
  226. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
  227. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
  228. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +4 -4
  229. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
  230. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1170 -0
  231. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
  232. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
  233. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
  234. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
  235. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +55 -2
  236. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +442 -104
  237. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +296 -49
  238. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
  239. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
  240. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
  241. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +21 -1
  242. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
  243. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
  244. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
  245. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +54 -0
  246. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +52 -0
  247. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
  248. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
  249. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
  250. package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
  251. package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
  252. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
  253. package/hedhog/frontend/app/courses/page.tsx.ejs +400 -230
  254. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
  255. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
  256. package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
  257. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
  258. package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
  259. package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
  260. package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
  261. package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
  262. package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
  263. package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
  264. package/hedhog/frontend/messages/en.json +386 -3
  265. package/hedhog/frontend/messages/pt.json +386 -3
  266. package/hedhog/table/lesson_xp_map.yaml +50 -0
  267. package/hedhog/table/lesson_xp_segment.yaml +40 -0
  268. package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
  269. package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
  270. package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
  271. package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
  272. package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
  273. package/hedhog/table/student_area_xp.yaml +30 -0
  274. package/hedhog/table/student_learning_type_xp.yaml +30 -0
  275. package/hedhog/table/student_skill_xp.yaml +30 -0
  276. package/hedhog/table/student_xp_event.yaml +34 -0
  277. package/hedhog/table/xp_area.yaml +39 -0
  278. package/hedhog/table/xp_learning_type.yaml +34 -0
  279. package/hedhog/table/xp_skill.yaml +39 -0
  280. package/package.json +9 -8
  281. package/src/bitcode-wallet/bitcode-wallet.service.ts +113 -0
  282. package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
  283. package/src/course/course-audio-transcription.service.ts +58 -21
  284. package/src/course/course-lesson.controller.ts +6 -1
  285. package/src/course/course-structure.controller.ts +10 -0
  286. package/src/course/course-structure.service.ts +174 -1
  287. package/src/course/course-video-conversion.service.ts +113 -75
  288. package/src/course/course.controller.ts +22 -3
  289. package/src/course/course.module.ts +15 -3
  290. package/src/course/course.service.ts +847 -30
  291. package/src/course/dto/cleanup-course-storage.dto.ts +22 -0
  292. package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
  293. package/src/course/dto/create-course-bulk-job.dto.ts +6 -0
  294. package/src/course/lms-bulk-upload-automation.service.ts +560 -0
  295. package/src/course/lms-bulk-upload-infra.service.ts +327 -0
  296. package/src/course/lms-bulk-upload.constants.ts +5 -0
  297. package/src/course/lms-bulk-upload.controller.ts +103 -0
  298. package/src/course/lms-bulk-upload.service.ts +1029 -0
  299. package/src/course/lms-setting.controller.ts +28 -1
  300. package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
  301. package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
  302. package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
  303. package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
  304. package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
  305. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +396 -0
  306. package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
  307. package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
  308. package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
  309. package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
  310. package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
  311. package/src/lms.module.ts +17 -2
  312. package/src/platforma/dto/update-profile.dto.ts +59 -0
  313. package/src/platforma/platforma.controller.ts +57 -2
  314. package/src/platforma/platforma.service.ts +268 -0
  315. package/src/student-xp/student-xp.controller.ts +76 -0
  316. package/src/student-xp/student-xp.module.ts +12 -0
  317. package/src/student-xp/student-xp.service.ts +236 -0
  318. package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
  319. package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
  320. package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
  321. package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
  322. package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
  323. package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
  324. package/src/xp-catalog/xp-area.controller.ts +64 -0
  325. package/src/xp-catalog/xp-area.service.ts +196 -0
  326. package/src/xp-catalog/xp-catalog.module.ts +16 -0
  327. package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
  328. package/src/xp-catalog/xp-learning-type.service.ts +170 -0
  329. package/src/xp-catalog/xp-skill.controller.ts +71 -0
  330. package/src/xp-catalog/xp-skill.service.ts +205 -0
@@ -0,0 +1,811 @@
1
+ 'use client';
2
+
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ SearchBar,
9
+ } from '@/components/entity-list';
10
+ import {
11
+ AlertDialog,
12
+ AlertDialogAction,
13
+ AlertDialogCancel,
14
+ AlertDialogContent,
15
+ AlertDialogDescription,
16
+ AlertDialogHeader,
17
+ AlertDialogTitle,
18
+ } from '@/components/ui/alert-dialog';
19
+ import { Badge } from '@/components/ui/badge';
20
+ import { Button } from '@/components/ui/button';
21
+ import {
22
+ Form,
23
+ FormControl,
24
+ FormField,
25
+ FormItem,
26
+ FormLabel,
27
+ FormMessage,
28
+ } from '@/components/ui/form';
29
+ import { IconActionGroup } from '@/components/ui/icon-action-group';
30
+ import { Input } from '@/components/ui/input';
31
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
32
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
33
+ import {
34
+ Select,
35
+ SelectContent,
36
+ SelectItem,
37
+ SelectTrigger,
38
+ SelectValue,
39
+ } from '@/components/ui/select';
40
+ import { Sheet, SheetHeader, SheetTitle } from '@/components/ui/sheet';
41
+ import { Skeleton } from '@/components/ui/skeleton';
42
+ import {
43
+ Table,
44
+ TableBody,
45
+ TableCell,
46
+ TableHead,
47
+ TableHeader,
48
+ TableRow,
49
+ } from '@/components/ui/table';
50
+ import { Textarea } from '@/components/ui/textarea';
51
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
52
+ import { cn } from '@/lib/utils';
53
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
54
+ import { zodResolver } from '@hookform/resolvers/zod';
55
+ import {
56
+ CheckCircle2,
57
+ Code2,
58
+ Link2Off,
59
+ Pencil,
60
+ Plus,
61
+ Trash2,
62
+ XCircle,
63
+ } from 'lucide-react';
64
+ import { useTranslations } from 'next-intl';
65
+ import { useEffect, useMemo, useState } from 'react';
66
+ import { useForm } from 'react-hook-form';
67
+ import { toast } from 'sonner';
68
+ import * as z from 'zod';
69
+
70
+ // ─── Types ────────────────────────────────────────────────────────────────────
71
+
72
+ type XpAreaOption = {
73
+ id: number;
74
+ slug: string;
75
+ name: string;
76
+ };
77
+
78
+ type XpSkill = {
79
+ id: number;
80
+ slug: string;
81
+ status: 'active' | 'inactive';
82
+ sortOrder: number;
83
+ xpAreaId: number | null;
84
+ name: string;
85
+ };
86
+
87
+ type XpSkillDetail = {
88
+ id: number;
89
+ slug: string;
90
+ status: 'active' | 'inactive';
91
+ sortOrder: number;
92
+ xpAreaId: number | null;
93
+ translations: Record<string, { name: string; description: string | null }>;
94
+ };
95
+
96
+ type PaginatedResult<T> = {
97
+ data: T[];
98
+ total: number;
99
+ page: number;
100
+ pageSize: number;
101
+ lastPage?: number;
102
+ };
103
+
104
+ // ─── Zod schema ───────────────────────────────────────────────────────────────
105
+
106
+ const createSchema = (t: ReturnType<typeof useTranslations>) =>
107
+ z.object({
108
+ slug: z
109
+ .string()
110
+ .min(1, t('form.validation.required'))
111
+ .max(100, t('form.validation.max100'))
112
+ .regex(/^[a-z0-9-]+$/, t('form.validation.slugPattern')),
113
+ namePt: z.string().min(1, t('form.validation.required')),
114
+ nameEn: z.string().min(1, t('form.validation.required')),
115
+ descriptionPt: z.string().optional(),
116
+ descriptionEn: z.string().optional(),
117
+ xpAreaId: z.coerce.number().int().positive().nullable().optional(),
118
+ sortOrder: z.coerce.number().int().min(0).default(0),
119
+ status: z.enum(['active', 'inactive']),
120
+ });
121
+
122
+ type SkillFormValues = z.infer<ReturnType<typeof createSchema>>;
123
+
124
+ // ─── SkillFormSheet ────────────────────────────────────────────────────────────
125
+
126
+ interface SkillFormSheetProps {
127
+ open: boolean;
128
+ onOpenChange: (open: boolean) => void;
129
+ skillToEdit: XpSkill | null;
130
+ areas: XpAreaOption[];
131
+ onSaved: () => void;
132
+ }
133
+
134
+ function SkillFormSheet({
135
+ open,
136
+ onOpenChange,
137
+ skillToEdit,
138
+ areas,
139
+ onSaved,
140
+ }: SkillFormSheetProps) {
141
+ const { request } = useApp();
142
+ const t = useTranslations('lms.XpSkillsPage');
143
+ const [isSaving, setIsSaving] = useState(false);
144
+ const [isLoadingDetail, setIsLoadingDetail] = useState(false);
145
+
146
+ const form = useForm<SkillFormValues>({
147
+ resolver: zodResolver(createSchema(t)),
148
+ defaultValues: {
149
+ slug: '',
150
+ namePt: '',
151
+ nameEn: '',
152
+ descriptionPt: '',
153
+ descriptionEn: '',
154
+ xpAreaId: null,
155
+ sortOrder: 0,
156
+ status: 'active',
157
+ },
158
+ });
159
+
160
+ useEffect(() => {
161
+ if (!open) return;
162
+
163
+ if (skillToEdit) {
164
+ setIsLoadingDetail(true);
165
+ request<{ data: XpSkillDetail }>({
166
+ url: `/lms/xp/skills/${skillToEdit.id}`,
167
+ method: 'GET',
168
+ })
169
+ .then((res) => {
170
+ const d = res.data as unknown as XpSkillDetail;
171
+ form.reset({
172
+ slug: d.slug,
173
+ namePt: d.translations?.pt?.name ?? '',
174
+ nameEn: d.translations?.en?.name ?? '',
175
+ descriptionPt: d.translations?.pt?.description ?? '',
176
+ descriptionEn: d.translations?.en?.description ?? '',
177
+ xpAreaId: d.xpAreaId ?? null,
178
+ sortOrder: d.sortOrder ?? 0,
179
+ status: d.status,
180
+ });
181
+ })
182
+ .catch(() => toast.error(t('messages.saveError')))
183
+ .finally(() => setIsLoadingDetail(false));
184
+ } else {
185
+ form.reset({
186
+ slug: '',
187
+ namePt: '',
188
+ nameEn: '',
189
+ descriptionPt: '',
190
+ descriptionEn: '',
191
+ xpAreaId: null,
192
+ sortOrder: 0,
193
+ status: 'active',
194
+ });
195
+ }
196
+ }, [open, skillToEdit]);
197
+
198
+ const onSubmit = async (values: SkillFormValues) => {
199
+ try {
200
+ setIsSaving(true);
201
+ const payload = {
202
+ slug: values.slug,
203
+ namePt: values.namePt,
204
+ nameEn: values.nameEn,
205
+ descriptionPt: values.descriptionPt || null,
206
+ descriptionEn: values.descriptionEn || null,
207
+ xpAreaId: values.xpAreaId ?? null,
208
+ sortOrder: values.sortOrder,
209
+ status: values.status,
210
+ };
211
+
212
+ if (skillToEdit) {
213
+ await request({
214
+ url: `/lms/xp/skills/${skillToEdit.id}`,
215
+ method: 'PATCH',
216
+ data: payload,
217
+ });
218
+ toast.success(t('messages.updateSuccess'));
219
+ } else {
220
+ await request({
221
+ url: '/lms/xp/skills',
222
+ method: 'POST',
223
+ data: payload,
224
+ });
225
+ toast.success(t('messages.createSuccess'));
226
+ }
227
+
228
+ onSaved();
229
+ onOpenChange(false);
230
+ } catch {
231
+ toast.error(t('messages.saveError'));
232
+ } finally {
233
+ setIsSaving(false);
234
+ }
235
+ };
236
+
237
+ return (
238
+ <Sheet open={open} onOpenChange={onOpenChange}>
239
+ <ResizableSheetContent
240
+ sheetId="lms-xp-skills-form-sheet"
241
+ defaultWidth={620}
242
+ minWidth={420}
243
+ maxWidth={900}
244
+ className="overflow-y-auto"
245
+ >
246
+ <SheetHeader>
247
+ <SheetTitle>
248
+ {skillToEdit ? t('sheet.editTitle') : t('sheet.createTitle')}
249
+ </SheetTitle>
250
+ </SheetHeader>
251
+
252
+ {isLoadingDetail ? (
253
+ <div className="space-y-3 p-4">
254
+ {Array.from({ length: 5 }).map((_, i) => (
255
+ <Skeleton key={i} className="h-10 w-full" />
256
+ ))}
257
+ </div>
258
+ ) : (
259
+ <Form {...form}>
260
+ <form
261
+ onSubmit={form.handleSubmit(onSubmit)}
262
+ className="flex flex-col gap-4 px-4 py-4"
263
+ >
264
+ <div className="grid grid-cols-2 gap-4">
265
+ <FormField
266
+ control={form.control}
267
+ name="slug"
268
+ render={({ field }) => (
269
+ <FormItem>
270
+ <FormLabel>{t('form.slug')}</FormLabel>
271
+ <FormControl>
272
+ <Input
273
+ placeholder={t('form.slugPlaceholder')}
274
+ {...field}
275
+ />
276
+ </FormControl>
277
+ <FormMessage />
278
+ </FormItem>
279
+ )}
280
+ />
281
+
282
+ <FormField
283
+ control={form.control}
284
+ name="status"
285
+ render={({ field }) => (
286
+ <FormItem>
287
+ <FormLabel>{t('form.status')}</FormLabel>
288
+ <Select
289
+ onValueChange={field.onChange}
290
+ value={field.value}
291
+ >
292
+ <FormControl>
293
+ <SelectTrigger className="w-full">
294
+ <SelectValue
295
+ placeholder={t('form.statusPlaceholder')}
296
+ />
297
+ </SelectTrigger>
298
+ </FormControl>
299
+ <SelectContent>
300
+ <SelectItem value="active">
301
+ {t('status.active')}
302
+ </SelectItem>
303
+ <SelectItem value="inactive">
304
+ {t('status.inactive')}
305
+ </SelectItem>
306
+ </SelectContent>
307
+ </Select>
308
+ <FormMessage />
309
+ </FormItem>
310
+ )}
311
+ />
312
+ </div>
313
+
314
+ <div className="grid grid-cols-2 gap-4">
315
+ <FormField
316
+ control={form.control}
317
+ name="namePt"
318
+ render={({ field }) => (
319
+ <FormItem>
320
+ <FormLabel>{t('form.namePt')}</FormLabel>
321
+ <FormControl>
322
+ <Input
323
+ placeholder={t('form.namePtPlaceholder')}
324
+ {...field}
325
+ />
326
+ </FormControl>
327
+ <FormMessage />
328
+ </FormItem>
329
+ )}
330
+ />
331
+
332
+ <FormField
333
+ control={form.control}
334
+ name="nameEn"
335
+ render={({ field }) => (
336
+ <FormItem>
337
+ <FormLabel>{t('form.nameEn')}</FormLabel>
338
+ <FormControl>
339
+ <Input
340
+ placeholder={t('form.nameEnPlaceholder')}
341
+ {...field}
342
+ />
343
+ </FormControl>
344
+ <FormMessage />
345
+ </FormItem>
346
+ )}
347
+ />
348
+ </div>
349
+
350
+ <div className="grid grid-cols-2 gap-4">
351
+ <FormField
352
+ control={form.control}
353
+ name="descriptionPt"
354
+ render={({ field }) => (
355
+ <FormItem>
356
+ <FormLabel>{t('form.descriptionPt')}</FormLabel>
357
+ <FormControl>
358
+ <Textarea
359
+ rows={3}
360
+ placeholder={t('form.descriptionPtPlaceholder')}
361
+ {...field}
362
+ />
363
+ </FormControl>
364
+ <FormMessage />
365
+ </FormItem>
366
+ )}
367
+ />
368
+
369
+ <FormField
370
+ control={form.control}
371
+ name="descriptionEn"
372
+ render={({ field }) => (
373
+ <FormItem>
374
+ <FormLabel>{t('form.descriptionEn')}</FormLabel>
375
+ <FormControl>
376
+ <Textarea
377
+ rows={3}
378
+ placeholder={t('form.descriptionEnPlaceholder')}
379
+ {...field}
380
+ />
381
+ </FormControl>
382
+ <FormMessage />
383
+ </FormItem>
384
+ )}
385
+ />
386
+ </div>
387
+
388
+ <div className="grid grid-cols-2 gap-4">
389
+ <FormField
390
+ control={form.control}
391
+ name="xpAreaId"
392
+ render={({ field }) => (
393
+ <FormItem>
394
+ <FormLabel>{t('form.area')}</FormLabel>
395
+ <Select
396
+ onValueChange={(val) =>
397
+ field.onChange(
398
+ val === '__none__' ? null : Number(val)
399
+ )
400
+ }
401
+ value={
402
+ field.value != null ? String(field.value) : '__none__'
403
+ }
404
+ >
405
+ <FormControl>
406
+ <SelectTrigger className="w-full">
407
+ <SelectValue
408
+ placeholder={t('form.areaPlaceholder')}
409
+ />
410
+ </SelectTrigger>
411
+ </FormControl>
412
+ <SelectContent>
413
+ <SelectItem value="__none__">
414
+ {t('form.areaNone')}
415
+ </SelectItem>
416
+ {areas.map((a) => (
417
+ <SelectItem key={a.id} value={String(a.id)}>
418
+ {a.name}
419
+ </SelectItem>
420
+ ))}
421
+ </SelectContent>
422
+ </Select>
423
+ <FormMessage />
424
+ </FormItem>
425
+ )}
426
+ />
427
+
428
+ <FormField
429
+ control={form.control}
430
+ name="sortOrder"
431
+ render={({ field }) => (
432
+ <FormItem>
433
+ <FormLabel>{t('form.sortOrder')}</FormLabel>
434
+ <FormControl>
435
+ <Input type="number" min={0} {...field} />
436
+ </FormControl>
437
+ <FormMessage />
438
+ </FormItem>
439
+ )}
440
+ />
441
+ </div>
442
+
443
+ <div className="flex justify-end gap-2 pt-2">
444
+ <Button
445
+ type="button"
446
+ variant="outline"
447
+ onClick={() => onOpenChange(false)}
448
+ disabled={isSaving}
449
+ >
450
+ {t('actions.cancel')}
451
+ </Button>
452
+ <Button type="submit" disabled={isSaving}>
453
+ {isSaving ? t('actions.saving') : t('actions.save')}
454
+ </Button>
455
+ </div>
456
+ </form>
457
+ </Form>
458
+ )}
459
+ </ResizableSheetContent>
460
+ </Sheet>
461
+ );
462
+ }
463
+
464
+ // ─── Page ─────────────────────────────────────────────────────────────────────
465
+
466
+ export default function XpSkillsPage() {
467
+ const { request } = useApp();
468
+ const t = useTranslations('lms.XpSkillsPage');
469
+
470
+ const [page, setPage] = useState(1);
471
+ const [pageSize, setPageSize] = usePersistedPageSize({
472
+ storageKey: 'pagination:global:pageSize',
473
+ defaultValue: 12,
474
+ allowedValues: [6, 12, 24, 48],
475
+ });
476
+ const [searchInput, setSearchInput] = useState('');
477
+ const [debouncedSearch, setDebouncedSearch] = useState('');
478
+ const [sheetOpen, setSheetOpen] = useState(false);
479
+ const [skillToEdit, setSkillToEdit] = useState<XpSkill | null>(null);
480
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
481
+ const [skillToDelete, setSkillToDelete] = useState<XpSkill | null>(null);
482
+ const [isDeleting, setIsDeleting] = useState(false);
483
+
484
+ useEffect(() => {
485
+ const timeout = setTimeout(() => {
486
+ setDebouncedSearch(searchInput.trim());
487
+ setPage(1);
488
+ }, 300);
489
+ return () => clearTimeout(timeout);
490
+ }, [searchInput]);
491
+
492
+ const { data: statsData, refetch: refetchStats } = useQuery<{
493
+ total: number;
494
+ active: number;
495
+ inactive: number;
496
+ unassigned: number;
497
+ }>({
498
+ queryKey: ['lms-xp-skills-stats'],
499
+ queryFn: async () => {
500
+ const response = await request<{
501
+ total: number;
502
+ active: number;
503
+ inactive: number;
504
+ unassigned: number;
505
+ }>({
506
+ url: '/lms/xp/skills/stats',
507
+ method: 'GET',
508
+ });
509
+ return response.data;
510
+ },
511
+ });
512
+
513
+ const { data: areas = [] } = useQuery<XpAreaOption[]>({
514
+ queryKey: ['lms-xp-areas-all'],
515
+ queryFn: async () => {
516
+ const res = await request<XpAreaOption[]>({
517
+ url: '/lms/xp/areas/all',
518
+ method: 'GET',
519
+ });
520
+ return res.data;
521
+ },
522
+ placeholderData: [],
523
+ });
524
+
525
+ const areaMap = useMemo(() => {
526
+ const map = new Map<number, string>();
527
+ for (const a of areas) map.set(a.id, a.name);
528
+ return map;
529
+ }, [areas]);
530
+
531
+ const {
532
+ data: paginate = { data: [], total: 0, page: 1, pageSize, lastPage: 1 },
533
+ isLoading,
534
+ refetch: refetchList,
535
+ } = useQuery<PaginatedResult<XpSkill>>({
536
+ queryKey: ['lms-xp-skills', page, pageSize, debouncedSearch],
537
+ queryFn: async () => {
538
+ const params = new URLSearchParams({
539
+ page: String(page),
540
+ pageSize: String(pageSize),
541
+ });
542
+ if (debouncedSearch) params.set('search', debouncedSearch);
543
+ const response = await request<PaginatedResult<XpSkill>>({
544
+ url: `/lms/xp/skills?${params.toString()}`,
545
+ method: 'GET',
546
+ });
547
+ return response.data;
548
+ },
549
+ placeholderData: (prev) =>
550
+ prev ?? { data: [], total: 0, page: 1, pageSize, lastPage: 1 },
551
+ });
552
+
553
+ const totalPages = Math.max(
554
+ 1,
555
+ (paginate.lastPage ?? Math.ceil((paginate.total || 0) / pageSize)) || 1
556
+ );
557
+
558
+ useEffect(() => {
559
+ if (page > totalPages) setPage(totalPages);
560
+ }, [page, totalPages]);
561
+
562
+ const openCreateSheet = () => {
563
+ setSkillToEdit(null);
564
+ setSheetOpen(true);
565
+ };
566
+
567
+ const openEditSheet = (skill: XpSkill) => {
568
+ setSkillToEdit(skill);
569
+ setSheetOpen(true);
570
+ };
571
+
572
+ const handleDeleteConfirm = async () => {
573
+ if (!skillToDelete) return;
574
+ try {
575
+ setIsDeleting(true);
576
+ await request({
577
+ url: `/lms/xp/skills/${skillToDelete.id}`,
578
+ method: 'DELETE',
579
+ });
580
+ toast.success(t('messages.deleteSuccess'));
581
+ setDeleteDialogOpen(false);
582
+ setSkillToDelete(null);
583
+ await Promise.all([refetchList(), refetchStats()]);
584
+ } catch {
585
+ toast.error(t('messages.deleteError'));
586
+ } finally {
587
+ setIsDeleting(false);
588
+ }
589
+ };
590
+
591
+ return (
592
+ <Page>
593
+ <PageHeader
594
+ breadcrumbs={[
595
+ { label: t('breadcrumbs.home'), href: '/' },
596
+ { label: t('breadcrumbs.lms'), href: '/lms' },
597
+ { label: t('breadcrumbs.xp'), href: '/lms/xp' },
598
+ { label: t('breadcrumbs.current') },
599
+ ]}
600
+ title={t('title')}
601
+ description={t('description')}
602
+ actions={[
603
+ {
604
+ label: t('actions.create'),
605
+ onClick: openCreateSheet,
606
+ icon: <Plus className="h-4 w-4" />,
607
+ },
608
+ ]}
609
+ />
610
+
611
+ <KpiCardsGrid
612
+ columns={4}
613
+ items={[
614
+ {
615
+ key: 'total',
616
+ title: t('kpis.total.label'),
617
+ value: statsData?.total ?? 0,
618
+ description: t('kpis.total.description'),
619
+ icon: Code2,
620
+ accentClassName: 'from-sky-500/20 via-blue-400/10 to-transparent',
621
+ iconContainerClassName: 'bg-sky-50 text-sky-700',
622
+ layout: 'compact',
623
+ },
624
+ {
625
+ key: 'active',
626
+ title: t('kpis.active.label'),
627
+ value: statsData?.active ?? 0,
628
+ description: t('kpis.active.description'),
629
+ icon: CheckCircle2,
630
+ accentClassName:
631
+ 'from-emerald-500/20 via-green-400/10 to-transparent',
632
+ iconContainerClassName: 'bg-emerald-50 text-emerald-700',
633
+ layout: 'compact',
634
+ },
635
+ {
636
+ key: 'inactive',
637
+ title: t('kpis.inactive.label'),
638
+ value: statsData?.inactive ?? 0,
639
+ description: t('kpis.inactive.description'),
640
+ icon: XCircle,
641
+ accentClassName:
642
+ 'from-amber-500/20 via-orange-400/10 to-transparent',
643
+ iconContainerClassName: 'bg-amber-50 text-amber-700',
644
+ layout: 'compact',
645
+ },
646
+ {
647
+ key: 'unassigned',
648
+ title: t('kpis.unassigned.label'),
649
+ value: statsData?.unassigned ?? 0,
650
+ description: t('kpis.unassigned.description'),
651
+ icon: Link2Off,
652
+ accentClassName: 'from-rose-500/20 via-red-400/10 to-transparent',
653
+ iconContainerClassName: 'bg-rose-50 text-rose-700',
654
+ layout: 'compact',
655
+ },
656
+ ]}
657
+ />
658
+
659
+ <SearchBar
660
+ searchQuery={searchInput}
661
+ onSearchChange={setSearchInput}
662
+ onSearch={() => setPage(1)}
663
+ placeholder={t('filters.searchPlaceholder')}
664
+ />
665
+
666
+ {isLoading ? (
667
+ <div className="space-y-3 p-4">
668
+ {Array.from({ length: 6 }).map((_, i) => (
669
+ <Skeleton key={i} className="h-14 w-full" />
670
+ ))}
671
+ </div>
672
+ ) : paginate.data.length === 0 ? (
673
+ <EmptyState
674
+ icon={<Code2 className="h-12 w-12" />}
675
+ title={t('empty.title')}
676
+ description={t('empty.description')}
677
+ actionLabel={t('actions.create')}
678
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
679
+ onAction={openCreateSheet}
680
+ />
681
+ ) : (
682
+ <div className="overflow-x-auto">
683
+ <Table>
684
+ <TableHeader>
685
+ <TableRow>
686
+ <TableHead>{t('table.name')}</TableHead>
687
+ <TableHead>{t('table.slug')}</TableHead>
688
+ <TableHead>{t('table.area')}</TableHead>
689
+ <TableHead>{t('table.status')}</TableHead>
690
+ <TableHead className="w-10" />
691
+ </TableRow>
692
+ </TableHeader>
693
+ <TableBody>
694
+ {paginate.data.map((skill) => (
695
+ <TableRow
696
+ key={skill.id}
697
+ className="cursor-pointer"
698
+ onDoubleClick={() => openEditSheet(skill)}
699
+ >
700
+ <TableCell className="font-medium">{skill.name}</TableCell>
701
+ <TableCell>
702
+ <span className="font-mono text-sm text-muted-foreground">
703
+ {skill.slug}
704
+ </span>
705
+ </TableCell>
706
+ <TableCell>
707
+ {skill.xpAreaId ? (
708
+ <span className="text-sm text-muted-foreground">
709
+ {areaMap.get(skill.xpAreaId) ?? `#${skill.xpAreaId}`}
710
+ </span>
711
+ ) : (
712
+ <span className="text-sm text-muted-foreground/40">
713
+
714
+ </span>
715
+ )}
716
+ </TableCell>
717
+ <TableCell>
718
+ <Badge
719
+ variant="outline"
720
+ className={cn(
721
+ 'border px-2.5 py-1 text-xs font-medium',
722
+ skill.status === 'active'
723
+ ? 'border-green-500/20 bg-green-500/10 text-green-600'
724
+ : 'border-gray-500/20 bg-gray-500/10 text-gray-600'
725
+ )}
726
+ >
727
+ {skill.status === 'active'
728
+ ? t('status.active')
729
+ : t('status.inactive')}
730
+ </Badge>
731
+ </TableCell>
732
+ <TableCell>
733
+ <IconActionGroup
734
+ className="ml-auto"
735
+ actions={[
736
+ {
737
+ key: 'edit',
738
+ label: t('actions.edit'),
739
+ icon: <Pencil className="size-4" />,
740
+ onClick: () => openEditSheet(skill),
741
+ },
742
+ {
743
+ key: 'delete',
744
+ label: t('actions.delete'),
745
+ icon: <Trash2 className="size-4" />,
746
+ onClick: () => {
747
+ setSkillToDelete(skill);
748
+ setDeleteDialogOpen(true);
749
+ },
750
+ destructive: true,
751
+ },
752
+ ]}
753
+ />
754
+ </TableCell>
755
+ </TableRow>
756
+ ))}
757
+ </TableBody>
758
+ </Table>
759
+ </div>
760
+ )}
761
+
762
+ <PaginationFooter
763
+ currentPage={page}
764
+ pageSize={pageSize}
765
+ totalItems={paginate.total}
766
+ onPageChange={setPage}
767
+ onPageSizeChange={(next) => {
768
+ setPageSize(next);
769
+ setPage(1);
770
+ }}
771
+ pageSizeOptions={[6, 12, 24, 48]}
772
+ />
773
+
774
+ <SkillFormSheet
775
+ open={sheetOpen}
776
+ onOpenChange={setSheetOpen}
777
+ skillToEdit={skillToEdit}
778
+ areas={areas}
779
+ onSaved={() => {
780
+ refetchList();
781
+ refetchStats();
782
+ }}
783
+ />
784
+
785
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
786
+ <AlertDialogContent>
787
+ <AlertDialogHeader>
788
+ <AlertDialogTitle>{t('deleteDialog.title')}</AlertDialogTitle>
789
+ <AlertDialogDescription>
790
+ {t('deleteDialog.description', {
791
+ slug: skillToDelete?.slug ?? '',
792
+ })}
793
+ </AlertDialogDescription>
794
+ </AlertDialogHeader>
795
+ <div className="flex justify-end gap-2">
796
+ <AlertDialogCancel disabled={isDeleting}>
797
+ {t('actions.cancel')}
798
+ </AlertDialogCancel>
799
+ <AlertDialogAction
800
+ onClick={handleDeleteConfirm}
801
+ disabled={isDeleting}
802
+ className="bg-red-600 hover:bg-red-700"
803
+ >
804
+ {isDeleting ? t('actions.deleting') : t('actions.delete')}
805
+ </AlertDialogAction>
806
+ </div>
807
+ </AlertDialogContent>
808
+ </AlertDialog>
809
+ </Page>
810
+ );
811
+ }