@@ -17,6 +17,7 @@ import {
1717} from './dto/enrollment-response.dto' ;
1818import { NotificationsService } from '../notifications/notifications.service' ;
1919import { NotificationType } from '../notifications/entities/notification.entity' ;
20+ import { CourseFilterDto } from './dto/course-filter.dto' ;
2021
2122@Injectable ( )
2223export class CoursesService {
@@ -43,24 +44,70 @@ export class CoursesService {
4344 page : number = 1 ,
4445 limit : number = 10 ,
4546 userId ?: string ,
47+ filters ?: CourseFilterDto ,
4648 ) : Promise < PaginatedResult < CourseResponseDto > > {
47- return this . findAllPaginated ( page , limit , userId ) ;
49+ return this . findAllPaginated ( page , limit , userId , filters ) ;
4850 }
4951
5052 async findAllPaginated (
5153 page : number ,
5254 limit : number ,
5355 userId ?: string ,
56+ filters ?: CourseFilterDto ,
5457 ) : Promise < PaginatedResult < CourseResponseDto > > {
55- const result = await this . paginationService . paginate < Course > (
56- this . courseRepository ,
57- { page, limit } ,
58- { where : { published : true } , order : { createdAt : 'DESC' } } ,
59- ) ;
58+ const { search, difficulty, tags, sortBy = 'createdAt' , sortOrder = 'desc' } = filters ?? { } ;
59+
60+ const qb = this . courseRepository
61+ . createQueryBuilder ( 'course' )
62+ . leftJoin ( 'course.registrations' , 'reg' )
63+ . addSelect ( 'COUNT(reg.id)' , 'enrollmentCount' )
64+ . where ( 'course.published = :published' , { published : true } )
65+ . groupBy ( 'course.id' )
66+ . skip ( ( page - 1 ) * limit )
67+ . take ( limit ) ;
68+
69+ if ( search ) {
70+ qb . andWhere (
71+ '(LOWER(course.title) LIKE :search OR LOWER(course.description) LIKE :search)' ,
72+ { search : `%${ search . toLowerCase ( ) } %` } ,
73+ ) ;
74+ }
75+
76+ if ( difficulty ) {
77+ qb . andWhere ( 'LOWER(course.difficulty) = :difficulty' , {
78+ difficulty : difficulty . toLowerCase ( ) ,
79+ } ) ;
80+ }
81+
82+ if ( tags ) {
83+ const tagList = tags . split ( ',' ) . map ( ( t ) => t . trim ( ) ) . filter ( Boolean ) ;
84+ tagList . forEach ( ( tag , i ) => {
85+ qb . andWhere ( `course.tags LIKE :tag${ i } ` , { [ `tag${ i } ` ] : `%${ tag } %` } ) ;
86+ } ) ;
87+ }
88+
89+ const orderCol = [ 'title' , 'difficulty' ] . includes ( sortBy ) ? sortBy : 'createdAt' ;
90+ qb . orderBy ( `course.${ orderCol } ` , sortOrder . toUpperCase ( ) as 'ASC' | 'DESC' ) ;
91+
92+ const [ courses , total ] = await qb . getManyAndCount ( ) ;
93+
94+ // fetch enrollment counts via raw query for accuracy
95+ const courseIds = courses . map ( ( c ) => c . id ) ;
96+ const countMap = new Map < string , number > ( ) ;
97+ if ( courseIds . length > 0 ) {
98+ const counts : { courseId : string ; count : string } [ ] =
99+ await this . courseRegistrationRepository
100+ . createQueryBuilder ( 'reg' )
101+ . select ( 'reg.courseId' , 'courseId' )
102+ . addSelect ( 'COUNT(reg.id)' , 'count' )
103+ . where ( 'reg.courseId IN (:...ids)' , { ids : courseIds } )
104+ . groupBy ( 'reg.courseId' )
105+ . getRawMany ( ) ;
106+ counts . forEach ( ( r ) => countMap . set ( r . courseId , parseInt ( r . count , 10 ) ) ) ;
107+ }
60108
61109 let enrolledIds = new Set < string > ( ) ;
62- if ( userId && result . data . length > 0 ) {
63- const courseIds = result . data . map ( ( c ) => c . id ) ;
110+ if ( userId && courseIds . length > 0 ) {
64111 const regs = await this . courseRegistrationRepository . find ( {
65112 where : { userId, courseId : In ( courseIds ) } ,
66113 select : [ 'courseId' ] ,
@@ -69,17 +116,30 @@ export class CoursesService {
69116 }
70117
71118 return {
72- ...result ,
73- data : result . data . map (
119+ data : courses . map (
74120 ( course ) =>
75121 new CourseResponseDto ( course , {
76- isEnrolled :
77- userId !== undefined ? enrolledIds . has ( course . id ) : undefined ,
122+ isEnrolled : userId !== undefined ? enrolledIds . has ( course . id ) : undefined ,
123+ enrollmentCount : countMap . get ( course . id ) ?? 0 ,
78124 } ) ,
79125 ) ,
126+ total,
127+ page,
128+ limit,
129+ totalPages : Math . ceil ( total / limit ) ,
80130 } ;
81131 }
82132
133+ async getUniqueTags ( ) : Promise < string [ ] > {
134+ const courses = await this . courseRepository . find ( {
135+ where : { published : true } ,
136+ select : [ 'tags' ] ,
137+ } ) ;
138+ const tagSet = new Set < string > ( ) ;
139+ courses . forEach ( ( c ) => ( c . tags ?? [ ] ) . forEach ( ( t ) => tagSet . add ( t ) ) ) ;
140+ return Array . from ( tagSet ) . sort ( ) ;
141+ }
142+
83143 async findOne ( id : string ) : Promise < CourseResponseDto > {
84144 const course = await this . courseRepository . findOne ( { where : { id } } ) ;
85145 if ( ! course ) {
0 commit comments