@@ -51,6 +51,24 @@ export class QuestionBanksService {
5151 throw new BadRequestException ( 'At least 2 choices are required' ) ;
5252 }
5353
54+ // Kiểm tra trùng lặp questionText trong cùng course
55+ const questionTextLower = createQuestionDto . questionText . toLowerCase ( ) . trim ( ) ;
56+ const allQuestionsInCourse = await this . questionRepository . find ( {
57+ where : { courseId : createQuestionDto . courseId } ,
58+ select : [ 'questionText' ] ,
59+ } ) ;
60+
61+ const isDuplicate = allQuestionsInCourse . some (
62+ ( q ) => q . questionText . toLowerCase ( ) . trim ( ) === questionTextLower ,
63+ ) ;
64+
65+ if ( isDuplicate ) {
66+ throw new BadRequestException ( {
67+ message : 'Question already exists in question bank' ,
68+ statusCode : 400 ,
69+ } ) ;
70+ }
71+
5472 // Set createdById from user
5573 const question = this . questionRepository . create ( {
5674 ...createQuestionDto ,
@@ -59,11 +77,21 @@ export class QuestionBanksService {
5977 return this . questionRepository . save ( question ) ;
6078 }
6179
62- async findAll ( user ?: User , courseId ?: string ) : Promise < Question [ ] > {
80+ async findAll (
81+ user ?: User ,
82+ courseId ?: string ,
83+ page : number = 1 ,
84+ limit : number = 50 ,
85+ ) : Promise < { data : Question [ ] ; total : number ; page : number ; limit : number } > {
6386 if ( ! user || user . role !== UserRole . INSTRUCTOR ) {
6487 throw new ForbiddenException ( 'Only instructors can view questions' ) ;
6588 }
6689
90+ // Validate pagination parameters
91+ const pageNum = Math . max ( 1 , page ) ;
92+ const limitNum = Math . min ( 100 , Math . max ( 1 , limit ) ) ;
93+ const skip = ( pageNum - 1 ) * limitNum ;
94+
6795 // Instructor: Xem questions của courses mà họ tạo
6896 const queryBuilder = this . questionRepository
6997 . createQueryBuilder ( 'question' )
@@ -76,7 +104,17 @@ export class QuestionBanksService {
76104 queryBuilder . andWhere ( 'question.courseId = :courseId' , { courseId } ) ;
77105 }
78106
79- return queryBuilder . orderBy ( 'question.createdAt' , 'DESC' ) . getMany ( ) ;
107+ // Get total count
108+ const total = await queryBuilder . getCount ( ) ;
109+
110+ // Get paginated data
111+ const data = await queryBuilder
112+ . orderBy ( 'question.createdAt' , 'DESC' )
113+ . skip ( skip )
114+ . take ( limitNum )
115+ . getMany ( ) ;
116+
117+ return { data, total, page : pageNum , limit : limitNum } ;
80118 }
81119
82120 async findOne ( id : string , user ?: User ) : Promise < Question > {
@@ -188,28 +226,103 @@ export class QuestionBanksService {
188226 const worksheet = workbook . Sheets [ sheetName ] ;
189227 const data = xlsx . utils . sheet_to_json ( worksheet ) ;
190228
191- const questionsToCreate : CreateQuestionDto [ ] = [ ] ;
192- const errors : string [ ] = [ ] ;
193-
229+ // Lấy danh sách questionText hiện có trong course để kiểm tra trùng lặp
230+ const existingQuestions = await this . questionRepository . find ( {
231+ where : { courseId } ,
232+ select : [ 'questionText' ] ,
233+ } ) ;
234+ const existingQuestionTexts = new Set (
235+ existingQuestions . map ( ( q ) => q . questionText . toLowerCase ( ) . trim ( ) ) ,
236+ ) ;
237+
238+ // Mảng để lưu kết quả từng câu hỏi
239+ const questionResults : Array < {
240+ rowNumber : number ;
241+ questionText : string ;
242+ success : boolean ;
243+ questionId ?: string ;
244+ error ?: string ;
245+ } > = [ ] ;
246+
247+ // Mảng để lưu các câu hỏi hợp lệ để tạo (kèm index để map lại)
248+ const questionsToCreate : Array < { dto : CreateQuestionDto ; resultIndex : number } > = [ ] ;
249+ const questionTextsInFile = new Set < string > ( ) ; // Để kiểm tra trùng lặp trong file
250+
251+ // Xử lý từng dòng trong file
194252 for ( const [ index , row ] of data . entries ( ) ) {
195253 const rowNumber = index + 2 ; // 1-based index + header row
254+ const rowData = row as Record < string , any > ;
255+ const questionText = rowData [ 'Question Text' ] ?. toString ( ) . trim ( ) || '' ;
256+
257+ // Kiểm tra trùng lặp trong file
258+ const questionTextLower = questionText . toLowerCase ( ) ;
259+ if ( questionTextsInFile . has ( questionTextLower ) ) {
260+ questionResults . push ( {
261+ rowNumber,
262+ questionText,
263+ success : false ,
264+ error : 'Question is duplicated in file' ,
265+ } ) ;
266+ continue ;
267+ }
268+ questionTextsInFile . add ( questionTextLower ) ;
269+
270+ // Kiểm tra trùng lặp với database
271+ if ( existingQuestionTexts . has ( questionTextLower ) ) {
272+ questionResults . push ( {
273+ rowNumber,
274+ questionText,
275+ success : false ,
276+ error : 'Question already exists in question bank' ,
277+ } ) ;
278+ continue ;
279+ }
280+
281+ // Validate và map dữ liệu
196282 try {
197- const questionDto = this . mapRowToQuestionDto ( row , courseId , user . id ) ;
198- questionsToCreate . push ( questionDto ) ;
283+ const questionDto = this . mapRowToQuestionDto ( rowData , courseId , user . id ) ;
284+ const resultIndex = questionResults . length ;
285+ questionsToCreate . push ( { dto : questionDto , resultIndex } ) ;
286+
287+ // Tạm thời đánh dấu là success, sẽ cập nhật questionId sau khi save
288+ questionResults . push ( {
289+ rowNumber,
290+ questionText,
291+ success : true ,
292+ } ) ;
199293 } catch ( error ) {
200- errors . push ( `Row ${ rowNumber } : ${ error . message } ` ) ;
294+ questionResults . push ( {
295+ rowNumber,
296+ questionText,
297+ success : false ,
298+ error : error . message || 'Unknown error' ,
299+ } ) ;
201300 }
202301 }
203302
303+ // Lưu các câu hỏi hợp lệ vào database
204304 if ( questionsToCreate . length > 0 ) {
205- await this . questionRepository . save ( this . questionRepository . create ( questionsToCreate ) ) ;
305+ const dtosToCreate = questionsToCreate . map ( ( item ) => item . dto ) ;
306+ const savedQuestions = await this . questionRepository . save (
307+ this . questionRepository . create ( dtosToCreate ) ,
308+ ) ;
309+
310+ // Cập nhật questionId cho các câu hỏi đã lưu thành công
311+ for ( let i = 0 ; i < questionsToCreate . length ; i ++ ) {
312+ const { resultIndex } = questionsToCreate [ i ] ;
313+ questionResults [ resultIndex ] . questionId = savedQuestions [ i ] . id ;
314+ }
206315 }
207316
317+ // Tính toán thống kê
318+ const imported = questionResults . filter ( ( r ) => r . success ) . length ;
319+ const failed = questionResults . filter ( ( r ) => ! r . success ) . length ;
320+
208321 return {
209322 total : data . length ,
210- imported : questionsToCreate . length ,
211- failed : errors . length ,
212- errors ,
323+ imported,
324+ failed,
325+ questions : questionResults , // Mảng chi tiết từng câu hỏi
213326 } ;
214327 }
215328
0 commit comments