diff --git a/Backend/src/controllers/AuditController.ts b/Backend/src/controllers/AuditController.ts index 5de912fc..a78c8d5a 100644 --- a/Backend/src/controllers/AuditController.ts +++ b/Backend/src/controllers/AuditController.ts @@ -169,8 +169,8 @@ export class AuditController { throw new ValidationError('No token provided'); } - if (days < 30) { - throw new ValidationError('Cannot delete logs newer than 30 days'); + if (days < 6) { + throw new ValidationError('Cannot delete logs newer than 6 days'); } const deleted = await auditService.deleteOldLogs(days); diff --git a/Backend/src/services/CoursesStudent.service.ts b/Backend/src/services/CoursesStudent.service.ts index 0e9e5b86..986695b1 100644 --- a/Backend/src/services/CoursesStudent.service.ts +++ b/Backend/src/services/CoursesStudent.service.ts @@ -340,6 +340,19 @@ export class CoursesStudentService { throw new ForbiddenError('You are not enrolled in this course'); } + // Update last_accessed_at (fire-and-forget — ไม่ block response) + if (enrollment.status === 'ENROLLED') { + prisma.enrollment.update({ + where: { + unique_enrollment: { + user_id: decoded.id, + course_id, + }, + }, + data: { last_accessed_at: new Date() }, + }).catch(err => logger.warn(`Failed to update last_accessed_at: ${err}`)); + } + // Get all lesson progress for this user and course const lessonIds = course.chapters.flatMap(ch => ch.lessons.map(l => l.id)); const lessonProgress = await prisma.lessonProgress.findMany({ @@ -1249,17 +1262,17 @@ export class CoursesStudentService { } catch (error) { logger.error(`Error completing lesson: ${error}`); const decoded = jwt.decode(input.token) as { id: number } | null; - await auditService.logSync({ - userId: decoded?.id || 0, - action: AuditAction.ERROR, - entityType: 'LessonProgress', - entityId: input.lesson_id, - metadata: { - operation: 'complete_lesson', - lesson_id: input.lesson_id, - error: error instanceof Error ? error.message : String(error) - } - }); + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'LessonProgress', + entityId: input.lesson_id, + metadata: { + operation: 'complete_lesson', + lesson_id: input.lesson_id, + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } diff --git a/Backend/tests/k6/login-load-test.js b/Backend/tests/k6/login-load-test.js index 2a0c375d..aee4cb4a 100644 --- a/Backend/tests/k6/login-load-test.js +++ b/Backend/tests/k6/login-load-test.js @@ -31,7 +31,7 @@ export const options = { thresholds: { http_req_duration: ['p(95)<2000'], // 95% of requests < 2s errors: ['rate<0.1'], // Error rate < 10% - login_duration: ['p(95)<2000'], // 95% of logins < 2s + login_duration: ['p(95)<2000'], // 95% pof logins < 2s }, }; diff --git a/Frontend-Learner/app.vue b/Frontend-Learner/app.vue index a1ac35c7..8070d468 100644 --- a/Frontend-Learner/app.vue +++ b/Frontend-Learner/app.vue @@ -1,20 +1,27 @@ - diff --git a/Frontend-Learner/assets/css/main.css b/Frontend-Learner/assets/css/main.css index 960858a7..7f1001f3 100644 --- a/Frontend-Learner/assets/css/main.css +++ b/Frontend-Learner/assets/css/main.css @@ -113,9 +113,9 @@ body { background-attachment: fixed; } -a { +/* a { text-decoration: none; - color: #3b82f6; + color: #2563eb; transition: color 0.2s; } @@ -129,7 +129,7 @@ a:hover { .dark a:hover { color: #93c5fd; -} +} */ ul { list-style: none; @@ -645,9 +645,9 @@ ul { .rounded { border-radius: var(--radius-md); } -.border-b { +/* .border-b { border-bottom: 1px solid var(--border-color); -} +} */ .load-more-wrap { display: flex; justify-content: center; diff --git a/Frontend-Learner/components/classroom/CurriculumSidebar.vue b/Frontend-Learner/components/classroom/CurriculumSidebar.vue index a7ccddab..7bd6526e 100644 --- a/Frontend-Learner/components/classroom/CurriculumSidebar.vue +++ b/Frontend-Learner/components/classroom/CurriculumSidebar.vue @@ -21,15 +21,40 @@ const emit = defineEmits<{ const { locale } = useI18n() +// State for expansion items +const chapterOpenState = ref>({}) + // Helper for localization const getLocalizedText = (text: any) => { if (!text) return '' if (typeof text === 'string') return text - const currentLocale = locale.value as 'th' | 'en' + // Safe locale access + const currentLocale = (locale?.value || 'th') as 'th' | 'en' return text[currentLocale] || text.th || text.en || '' } +// Helper: Check if lesson is completed +const isLessonCompleted = (lesson: any) => { + return lesson.is_completed === true || lesson.progress?.is_completed === true +} + +// Reactive Chapter Completion Status +// Computes a map of chapterId -> boolean (true if all lessons are completed) +const chapterCompletionStatus = computed(() => { + const status: Record = {} + if (!props.courseData || !props.courseData.chapters) return status + + props.courseData.chapters.forEach((chapter: any) => { + if (chapter.lessons && chapter.lessons.length > 0) { + status[chapter.id] = chapter.lessons.every((l: any) => isLessonCompleted(l)) + } else { + status[chapter.id] = false + } + }) + return status +}) + // Local Progress Calculation const progressPercentage = computed(() => { if (!props.courseData || !props.courseData.chapters) return 0 @@ -38,11 +63,34 @@ const progressPercentage = computed(() => { props.courseData.chapters.forEach((c: any) => { c.lessons.forEach((l: any) => { total++ - if (l.is_completed || l.progress?.is_completed) completed++ + if (isLessonCompleted(l)) completed++ }) }) return total > 0 ? Math.round((completed / total) * 100) : 0 }) + +// Auto-expand chapter containing current lesson +watch(() => props.currentLessonId, (newId) => { + if (newId && props.courseData?.chapters) { + props.courseData.chapters.forEach((chapter: any) => { + const hasLesson = chapter.lessons.some((l: any) => l.id === newId) + if (hasLesson) { + chapterOpenState.value[chapter.id] = true + } + }) + } +}, { immediate: true }) + +// Initialize all chapters as open by default on load +watch(() => props.courseData, (newData) => { + if (newData?.chapters) { + newData.chapters.forEach((chapter: any) => { + if (chapterOpenState.value[chapter.id] === undefined) { + chapterOpenState.value[chapter.id] = true + } + }) + } +}, { immediate: true }) diff --git a/Frontend-Learner/components/course/CourseCard.vue b/Frontend-Learner/components/course/CourseCard.vue index b26d9ea1..1cd55e5a 100644 --- a/Frontend-Learner/components/course/CourseCard.vue +++ b/Frontend-Learner/components/course/CourseCard.vue @@ -25,6 +25,8 @@ interface CourseCardProps { showContinue?: boolean showCertificate?: boolean showStudyAgain?: boolean + hideProgress?: boolean + hideActions?: boolean } const props = withDefaults(defineProps(), { @@ -55,7 +57,7 @@ const displayCategory = computed(() => getLocalizedText(props.category))