preogress tracker fix
This commit is contained in:
parent
515573d56e
commit
d558739097
|
|
@ -305,38 +305,151 @@ INSERT INTO sub_course_prerequisites (sub_course_id, prerequisite_sub_course_id)
|
|||
ON CONFLICT (sub_course_id, prerequisite_sub_course_id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- User Sub-course Progress
|
||||
-- Simulate realistic student progress for admin panel
|
||||
-- Completion-driven progress seed (auto-aggregate model)
|
||||
-- Seed video/practice completion records, then derive sub-course progress
|
||||
-- ======================================================
|
||||
INSERT INTO user_sub_course_progress (user_id, sub_course_id, status, progress_percentage, started_at, completed_at) VALUES
|
||||
-- Student 10 (Demo Student): working through Python course
|
||||
(10, 1, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '30 days', CURRENT_TIMESTAMP - INTERVAL '20 days'),
|
||||
(10, 2, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '12 days'),
|
||||
(10, 3, 'IN_PROGRESS', 65, CURRENT_TIMESTAMP - INTERVAL '12 days', NULL),
|
||||
|
||||
-- Student 10: started Flutter
|
||||
(10, 18, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP - INTERVAL '8 days'),
|
||||
(10, 19, 'IN_PROGRESS', 40, CURRENT_TIMESTAMP - INTERVAL '8 days', NULL),
|
||||
-- Video completions
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 10, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '20 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id IN (1, 2, 18)
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
-- Student 11 (Abebe): completed Python, started JavaScript
|
||||
(11, 1, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '45 days', CURRENT_TIMESTAMP - INTERVAL '35 days'),
|
||||
(11, 2, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '35 days', CURRENT_TIMESTAMP - INTERVAL '25 days'),
|
||||
(11, 3, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '25 days', CURRENT_TIMESTAMP - INTERVAL '15 days'),
|
||||
(11, 4, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP - INTERVAL '5 days'),
|
||||
(11, 5, 'IN_PROGRESS', 30, CURRENT_TIMESTAMP - INTERVAL '5 days', NULL),
|
||||
(11, 6, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '10 days'),
|
||||
(11, 7, 'IN_PROGRESS', 50, CURRENT_TIMESTAMP - INTERVAL '10 days', NULL),
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 10, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '8 days', CURRENT_TIMESTAMP - INTERVAL '8 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 19
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND v.display_order = 1
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
-- Student 11: Docker course
|
||||
(11, 25, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '10 days', CURRENT_TIMESTAMP - INTERVAL '3 days'),
|
||||
(11, 26, 'IN_PROGRESS', 20, CURRENT_TIMESTAMP - INTERVAL '3 days', NULL),
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 11, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '25 days', CURRENT_TIMESTAMP - INTERVAL '25 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id IN (1, 2, 25)
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
-- Student 12 (Sara): just started
|
||||
(12, 1, 'IN_PROGRESS', 25, CURRENT_TIMESTAMP - INTERVAL '7 days', NULL),
|
||||
(12, 18, 'IN_PROGRESS', 10, CURRENT_TIMESTAMP - INTERVAL '3 days', NULL),
|
||||
(12, 22, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '14 days', CURRENT_TIMESTAMP - INTERVAL '7 days'),
|
||||
(12, 23, 'IN_PROGRESS', 60, CURRENT_TIMESTAMP - INTERVAL '7 days', NULL)
|
||||
ON CONFLICT (user_id, sub_course_id) DO NOTHING;
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 11, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '3 days', CURRENT_TIMESTAMP - INTERVAL '3 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 26
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 12, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '7 days', CURRENT_TIMESTAMP - INTERVAL '7 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 22
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 12, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '3 days', CURRENT_TIMESTAMP - INTERVAL '3 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 18
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND v.display_order = 1
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
-- Practice completions
|
||||
INSERT INTO user_practice_progress (user_id, sub_course_id, question_set_id, completed_at, updated_at)
|
||||
SELECT 10, qs.owner_id::BIGINT, qs.id, CURRENT_TIMESTAMP - INTERVAL '18 days', CURRENT_TIMESTAMP - INTERVAL '18 days'
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND qs.owner_id IN (1, 2, 18)
|
||||
ON CONFLICT (user_id, question_set_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_practice_progress (user_id, sub_course_id, question_set_id, completed_at, updated_at)
|
||||
SELECT 11, qs.owner_id::BIGINT, qs.id, CURRENT_TIMESTAMP - INTERVAL '10 days', CURRENT_TIMESTAMP - INTERVAL '10 days'
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND qs.owner_id IN (1, 2, 25)
|
||||
ON CONFLICT (user_id, question_set_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_practice_progress (user_id, sub_course_id, question_set_id, completed_at, updated_at)
|
||||
SELECT 12, qs.owner_id::BIGINT, qs.id, CURRENT_TIMESTAMP - INTERVAL '7 days', CURRENT_TIMESTAMP - INTERVAL '7 days'
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND qs.owner_id IN (22)
|
||||
ON CONFLICT (user_id, question_set_id) DO NOTHING;
|
||||
|
||||
-- Derive sub-course progress from completion tables (same model as runtime auto-aggregate)
|
||||
WITH target_pairs AS (
|
||||
SELECT DISTINCT user_id, sub_course_id
|
||||
FROM user_sub_course_video_progress
|
||||
WHERE user_id IN (10, 11, 12)
|
||||
UNION
|
||||
SELECT DISTINCT user_id, sub_course_id
|
||||
FROM user_practice_progress
|
||||
WHERE user_id IN (10, 11, 12)
|
||||
),
|
||||
stats AS (
|
||||
SELECT
|
||||
tp.user_id,
|
||||
tp.sub_course_id,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = tp.sub_course_id
|
||||
AND v.status = 'PUBLISHED')
|
||||
+
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = tp.sub_course_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS total_items,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_sub_course_video_progress uv
|
||||
JOIN sub_course_videos v ON v.id = uv.video_id
|
||||
WHERE uv.user_id = tp.user_id
|
||||
AND uv.sub_course_id = tp.sub_course_id
|
||||
AND uv.completed_at IS NOT NULL
|
||||
AND v.status = 'PUBLISHED')
|
||||
+
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_practice_progress up
|
||||
JOIN question_sets qs ON qs.id = up.question_set_id
|
||||
WHERE up.user_id = tp.user_id
|
||||
AND up.sub_course_id = tp.sub_course_id
|
||||
AND up.completed_at IS NOT NULL
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = tp.sub_course_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS completed_items
|
||||
FROM target_pairs tp
|
||||
)
|
||||
INSERT INTO user_sub_course_progress (user_id, sub_course_id, status, progress_percentage, started_at, completed_at, updated_at)
|
||||
SELECT
|
||||
user_id,
|
||||
sub_course_id,
|
||||
CASE
|
||||
WHEN total_items > 0 AND completed_items >= total_items THEN 'COMPLETED'
|
||||
WHEN completed_items > 0 THEN 'IN_PROGRESS'
|
||||
ELSE 'NOT_STARTED'
|
||||
END AS status,
|
||||
CASE
|
||||
WHEN total_items = 0 THEN 0
|
||||
ELSE ROUND((completed_items::NUMERIC * 100.0) / total_items::NUMERIC)::SMALLINT
|
||||
END AS progress_percentage,
|
||||
CASE WHEN completed_items > 0 THEN CURRENT_TIMESTAMP - INTERVAL '10 days' ELSE NULL END AS started_at,
|
||||
CASE WHEN total_items > 0 AND completed_items >= total_items THEN CURRENT_TIMESTAMP - INTERVAL '3 days' ELSE NULL END AS completed_at,
|
||||
CURRENT_TIMESTAMP AS updated_at
|
||||
FROM stats
|
||||
ON CONFLICT (user_id, sub_course_id) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
progress_percentage = EXCLUDED.progress_percentage,
|
||||
started_at = COALESCE(user_sub_course_progress.started_at, EXCLUDED.started_at),
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
|
||||
-- ======================================================
|
||||
-- Reset sequences to avoid ID conflicts after seeding
|
||||
|
|
@ -352,3 +465,5 @@ SELECT setval(pg_get_serial_sequence('question_set_items', 'id'), COALESCE((SELE
|
|||
SELECT setval(pg_get_serial_sequence('question_set_personas', 'id'), COALESCE((SELECT MAX(id) FROM question_set_personas), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('sub_course_prerequisites', 'id'), COALESCE((SELECT MAX(id) FROM sub_course_prerequisites), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('user_sub_course_progress', 'id'), COALESCE((SELECT MAX(id) FROM user_sub_course_progress), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('user_sub_course_video_progress', 'id'), COALESCE((SELECT MAX(id) FROM user_sub_course_video_progress), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('user_practice_progress', 'id'), COALESCE((SELECT MAX(id) FROM user_practice_progress), 1), true);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,31 @@ This guide explains how to integrate learner sub-course progress tracking into t
|
|||
- **Auth:** Bearer token
|
||||
- **Required permission:** `progress.get_any_user`
|
||||
|
||||
## Course-level Summary Endpoint
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/api/v1/admin/users/:userId/progress/courses/:courseId/summary`
|
||||
- **Auth:** Bearer token
|
||||
- **Required permission:** `progress.get_any_user`
|
||||
|
||||
### Success Response (`200`)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Learner course progress summary retrieved successfully",
|
||||
"data": {
|
||||
"course_id": 1,
|
||||
"learner_user_id": 10,
|
||||
"overall_progress_percentage": 40,
|
||||
"total_sub_courses": 5,
|
||||
"completed_sub_courses": 2,
|
||||
"in_progress_sub_courses": 1,
|
||||
"not_started_sub_courses": 2,
|
||||
"locked_sub_courses": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
- `userId` (number): target learner user ID
|
||||
|
|
@ -74,6 +99,31 @@ This guide explains how to integrate learner sub-course progress tracking into t
|
|||
- list is ordered by `display_order`
|
||||
- only active sub-courses are included
|
||||
|
||||
## Progress Calculation Model
|
||||
|
||||
Sub-course progress is automatically aggregated from completion records:
|
||||
|
||||
- completed published videos in the sub-course (`user_sub_course_video_progress`)
|
||||
- completed published practices in the sub-course (`user_practice_progress`)
|
||||
|
||||
Formula:
|
||||
|
||||
- `total_items = published_videos + published_practices`
|
||||
- `completed_items = completed_videos + completed_practices`
|
||||
- `progress_percentage = round((completed_items / total_items) * 100)`
|
||||
- if `total_items = 0`, `progress_percentage = 0`
|
||||
|
||||
Status transitions:
|
||||
|
||||
- `NOT_STARTED` when `completed_items = 0`
|
||||
- `IN_PROGRESS` when `0 < completed_items < total_items`
|
||||
- `COMPLETED` when `completed_items >= total_items` and `total_items > 0`
|
||||
|
||||
Auto-recalculation triggers:
|
||||
|
||||
- `POST /api/v1/progress/videos/:id/complete`
|
||||
- `POST /api/v1/progress/practices/:id/complete`
|
||||
|
||||
## Backend Rollout Steps
|
||||
|
||||
After pulling this backend change:
|
||||
|
|
@ -90,6 +140,7 @@ After pulling this backend change:
|
|||
2. Admin selects learner and course.
|
||||
3. Frontend requests:
|
||||
- `GET /api/v1/admin/users/{userId}/progress/courses/{courseId}`
|
||||
- `GET /api/v1/admin/users/{userId}/progress/courses/{courseId}/summary`
|
||||
4. Render returned `data` as ordered progress items.
|
||||
|
||||
## Recommended UI Sections
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ type ProgressionStore interface {
|
|||
StartSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error)
|
||||
UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error
|
||||
CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error
|
||||
RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error
|
||||
GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error)
|
||||
GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error)
|
||||
GetSubCoursesWithProgressByCourse(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error)
|
||||
|
|
|
|||
|
|
@ -109,6 +109,90 @@ func (s *Store) CompleteSubCourse(ctx context.Context, userID, subCourseID int64
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Store) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error {
|
||||
const query = `
|
||||
WITH totals AS (
|
||||
SELECT
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = $2
|
||||
AND v.status = 'PUBLISHED') AS total_videos,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = $2
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS total_practices
|
||||
),
|
||||
completed AS (
|
||||
SELECT
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_sub_course_video_progress uv
|
||||
JOIN sub_course_videos v ON v.id = uv.video_id
|
||||
WHERE uv.user_id = $1
|
||||
AND uv.sub_course_id = $2
|
||||
AND uv.completed_at IS NOT NULL
|
||||
AND v.status = 'PUBLISHED') AS completed_videos,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_practice_progress up
|
||||
JOIN question_sets qs ON qs.id = up.question_set_id
|
||||
WHERE up.user_id = $1
|
||||
AND up.sub_course_id = $2
|
||||
AND up.completed_at IS NOT NULL
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = $2
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS completed_practices
|
||||
),
|
||||
stats AS (
|
||||
SELECT
|
||||
(total_videos + total_practices) AS total_items,
|
||||
(completed_videos + completed_practices) AS completed_items
|
||||
FROM totals, completed
|
||||
)
|
||||
INSERT INTO user_sub_course_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
status,
|
||||
progress_percentage,
|
||||
started_at,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1,
|
||||
$2,
|
||||
CASE
|
||||
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN 'COMPLETED'
|
||||
WHEN stats.completed_items > 0 THEN 'IN_PROGRESS'
|
||||
ELSE 'NOT_STARTED'
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.total_items = 0 THEN 0
|
||||
ELSE ROUND((stats.completed_items::NUMERIC * 100.0) / stats.total_items::NUMERIC)::SMALLINT
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.completed_items > 0 THEN CURRENT_TIMESTAMP
|
||||
ELSE NULL
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN CURRENT_TIMESTAMP
|
||||
ELSE NULL
|
||||
END,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM stats
|
||||
ON CONFLICT (user_id, sub_course_id) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
progress_percentage = EXCLUDED.progress_percentage,
|
||||
started_at = COALESCE(user_sub_course_progress.started_at, EXCLUDED.started_at),
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
`
|
||||
|
||||
_, err := s.conn.Exec(ctx, query, userID, subCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
row, err := s.queries.GetUserSubCourseProgress(ctx, dbgen.GetUserSubCourseProgressParams{
|
||||
UserID: userID,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ func (s *Service) CompleteSubCourse(ctx context.Context, userID, subCourseID int
|
|||
return s.progressionStore.CompleteSubCourse(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error {
|
||||
return s.progressionStore.RecalculateSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
return s.progressionStore.GetUserSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1311,6 +1311,12 @@ func (h *Handler) CompleteSubCourseVideo(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.courseMgmtSvc.RecalculateSubCourseProgress(c.Context(), userID, video.SubCourseID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update sub-course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Video completed",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package handlers
|
|||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
|
@ -60,6 +61,17 @@ type userProgressRes struct {
|
|||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
type courseProgressSummaryRes struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
LearnerUserID int64 `json:"learner_user_id"`
|
||||
OverallProgressPercentage int16 `json:"overall_progress_percentage"`
|
||||
TotalSubCourses int32 `json:"total_sub_courses"`
|
||||
CompletedSubCourses int32 `json:"completed_sub_courses"`
|
||||
InProgressSubCourses int32 `json:"in_progress_sub_courses"`
|
||||
NotStartedSubCourses int32 `json:"not_started_sub_courses"`
|
||||
LockedSubCourses int32 `json:"locked_sub_courses"`
|
||||
}
|
||||
|
||||
func mapSubCourseProgress(items []domain.SubCourseWithProgress) []subCourseProgressRes {
|
||||
res := make([]subCourseProgressRes, 0, len(items))
|
||||
for _, item := range items {
|
||||
|
|
@ -447,3 +459,83 @@ func (h *Handler) GetUserCourseProgressForAdmin(c *fiber.Ctx) error {
|
|||
Data: mapSubCourseProgress(items),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCourseProgressSummaryForAdmin godoc
|
||||
// @Summary Get learner's course progress summary (admin)
|
||||
// @Description Returns course-level aggregated progress metrics for a target learner
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param userId path int true "Learner User ID"
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/admin/users/{userId}/progress/courses/{courseId}/summary [get]
|
||||
func (h *Handler) GetUserCourseProgressSummaryForAdmin(c *fiber.Ctx) error {
|
||||
targetUserID, err := strconv.ParseInt(c.Params("userId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), targetUserID, courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get learner course progress summary",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
completedCount int32
|
||||
inProgressCount int32
|
||||
notStartedCount int32
|
||||
lockedCount int32
|
||||
sumPercentage int64
|
||||
)
|
||||
|
||||
for _, item := range items {
|
||||
sumPercentage += int64(item.ProgressPercentage)
|
||||
switch item.ProgressStatus {
|
||||
case domain.ProgressStatusCompleted:
|
||||
completedCount++
|
||||
case domain.ProgressStatusInProgress:
|
||||
inProgressCount++
|
||||
default:
|
||||
notStartedCount++
|
||||
}
|
||||
if item.IsLocked {
|
||||
lockedCount++
|
||||
}
|
||||
}
|
||||
|
||||
totalSubCourses := int32(len(items))
|
||||
overall := int16(0)
|
||||
if totalSubCourses > 0 {
|
||||
overall = int16(math.Round(float64(sumPercentage) / float64(totalSubCourses)))
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Learner course progress summary retrieved successfully",
|
||||
Data: courseProgressSummaryRes{
|
||||
CourseID: courseID,
|
||||
LearnerUserID: targetUserID,
|
||||
OverallProgressPercentage: overall,
|
||||
TotalSubCourses: totalSubCourses,
|
||||
CompletedSubCourses: completedCount,
|
||||
InProgressSubCourses: inProgressCount,
|
||||
NotStartedSubCourses: notStartedCount,
|
||||
LockedSubCourses: lockedCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1221,6 +1221,18 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if set.OwnerID == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update sub-course progress",
|
||||
Error: "practice owner is missing",
|
||||
})
|
||||
}
|
||||
if err := h.courseMgmtSvc.RecalculateSubCourseProgress(c.Context(), userID, *set.OwnerID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update sub-course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice completed",
|
||||
|
|
|
|||
|
|
@ -332,6 +332,7 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/progress/sub-courses/:id/access", a.authMiddleware, a.RequirePermission("progress.check_access"), h.CheckSubCourseAccess)
|
||||
groupV1.Get("/progress/courses/:courseId", a.authMiddleware, a.RequirePermission("progress.get_course"), h.GetUserCourseProgress)
|
||||
groupV1.Get("/admin/users/:userId/progress/courses/:courseId", a.authMiddleware, a.RequirePermission("progress.get_any_user"), h.GetUserCourseProgressForAdmin)
|
||||
groupV1.Get("/admin/users/:userId/progress/courses/:courseId/summary", a.authMiddleware, a.RequirePermission("progress.get_any_user"), h.GetUserCourseProgressSummaryForAdmin)
|
||||
|
||||
// Ratings
|
||||
groupV1.Post("/ratings", a.authMiddleware, a.RequirePermission("ratings.submit"), h.SubmitRating)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user