@@ -221,11 +221,6 @@ export class QuestionBanksService {
221221 throw new BadRequestException ( 'File is required' ) ;
222222 }
223223
224- const workbook = xlsx . read ( file . buffer , { type : 'buffer' } ) ;
225- const sheetName = workbook . SheetNames [ 0 ] ;
226- const worksheet = workbook . Sheets [ sheetName ] ;
227- const data = xlsx . utils . sheet_to_json ( worksheet ) ;
228-
229224 // Lấy danh sách questionText hiện có trong course để kiểm tra trùng lặp
230225 const existingQuestions = await this . questionRepository . find ( {
231226 where : { courseId } ,
@@ -235,6 +230,53 @@ export class QuestionBanksService {
235230 existingQuestions . map ( ( q ) => q . questionText . toLowerCase ( ) . trim ( ) ) ,
236231 ) ;
237232
233+ // Parse file (CSV hoặc XLSX)
234+ // xlsx library tự động xử lý cả CSV và XLSX
235+ const workbook = xlsx . read ( file . buffer , {
236+ type : 'buffer' ,
237+ cellDates : false ,
238+ cellNF : false ,
239+ cellText : false ,
240+ } ) ;
241+ const sheetName = workbook . SheetNames [ 0 ] ;
242+ const worksheet = workbook . Sheets [ sheetName ] ;
243+
244+ // Parse với header tự động (sẽ tạo object với keys từ dòng đầu tiên)
245+ // Không dùng header: 1 vì nó sẽ trả về array of arrays thay vì array of objects
246+ let data = xlsx . utils . sheet_to_json ( worksheet , {
247+ defval : '' , // Giá trị mặc định cho cell rỗng
248+ raw : false , // Convert tất cả về string
249+ blankrows : false , // Bỏ qua dòng trống
250+ } ) ;
251+
252+ // Normalize tên cột: xóa khoảng trắng thừa và chuẩn hóa
253+ // Xử lý trường hợp xlsx có thể parse tên cột khác đi (do dấu ngoặc kép trong CSV)
254+ data = data . map ( ( row : any ) => {
255+ const normalizedRow : Record < string , any > = { } ;
256+
257+ // Map các tên cột có thể có (xlsx có thể parse khác do dấu ngoặc kép)
258+ Object . keys ( row ) . forEach ( ( key ) => {
259+ const normalizedKey = key . trim ( ) . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ; // Xóa dấu ngoặc kép nếu có
260+ const value = row [ key ] ;
261+
262+ // Chuẩn hóa tên cột
263+ if ( normalizedKey === 'Question Text' || normalizedKey === 'QuestionText' || normalizedKey === 'Question' ) {
264+ normalizedRow [ 'Question Text' ] = value ;
265+ } else if ( normalizedKey === 'Choices' || normalizedKey === 'Choice' ) {
266+ normalizedRow [ 'Choices' ] = value ;
267+ } else if ( normalizedKey === 'Correct Answer' || normalizedKey === 'CorrectAnswer' || normalizedKey === 'Correct' ) {
268+ normalizedRow [ 'Correct Answer' ] = value ;
269+ } else if ( normalizedKey === 'Difficulty' ) {
270+ normalizedRow [ 'Difficulty' ] = value ;
271+ } else {
272+ // Giữ nguyên nếu không match (có thể là tên cột khác)
273+ normalizedRow [ key ] = value ;
274+ }
275+ } ) ;
276+
277+ return normalizedRow ;
278+ } ) ;
279+
238280 // Mảng để lưu kết quả từng câu hỏi
239281 const questionResults : Array < {
240282 rowNumber : number ;
@@ -252,30 +294,49 @@ export class QuestionBanksService {
252294 for ( const [ index , row ] of data . entries ( ) ) {
253295 const rowNumber = index + 2 ; // 1-based index + header row
254296 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 ;
297+
298+ // Debug: Log row data để kiểm tra
299+ // console.log(`Row ${rowNumber}:`, JSON.stringify(rowData));
300+
301+ // Lấy questionText, xử lý các trường hợp có thể có
302+ let questionText = '' ;
303+ if ( rowData [ 'Question Text' ] ) {
304+ questionText = String ( rowData [ 'Question Text' ] ) . trim ( ) ;
305+ } else if ( rowData [ 'QuestionText' ] ) {
306+ // Fallback nếu không có khoảng trắng
307+ questionText = String ( rowData [ 'QuestionText' ] ) . trim ( ) ;
308+ } else if ( rowData [ 'Question' ] ) {
309+ // Fallback nếu chỉ có "Question"
310+ questionText = String ( rowData [ 'Question' ] ) . trim ( ) ;
267311 }
268- questionTextsInFile . add ( questionTextLower ) ;
269312
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 ;
313+ // Nếu questionText rỗng, bỏ qua kiểm tra trùng lặp và để mapRowToQuestionDto báo lỗi
314+ let questionTextLower = '' ;
315+ if ( questionText ) {
316+ questionTextLower = questionText . toLowerCase ( ) ;
317+
318+ // Kiểm tra trùng lặp trong file
319+ if ( questionTextsInFile . has ( questionTextLower ) ) {
320+ questionResults . push ( {
321+ rowNumber,
322+ questionText,
323+ success : false ,
324+ error : 'Question is duplicated in file' ,
325+ } ) ;
326+ continue ;
327+ }
328+ questionTextsInFile . add ( questionTextLower ) ;
329+
330+ // Kiểm tra trùng lặp với database
331+ if ( existingQuestionTexts . has ( questionTextLower ) ) {
332+ questionResults . push ( {
333+ rowNumber,
334+ questionText,
335+ success : false ,
336+ error : 'Question already exists in question bank' ,
337+ } ) ;
338+ continue ;
339+ }
279340 }
280341
281342 // Validate và map dữ liệu
@@ -327,41 +388,89 @@ export class QuestionBanksService {
327388 }
328389
329390 private mapRowToQuestionDto ( row : any , courseId : string , createdById : string ) : CreateQuestionDto {
330- if ( ! row [ 'Question Text' ] || ! row [ 'Choices' ] || ! row [ 'Correct Answer' ] || ! row [ 'Difficulty' ] ) {
331- throw new Error ( 'Missing required fields' ) ;
391+ // Kiểm tra và convert các trường về string, xử lý null/undefined
392+ const questionText = row [ 'Question Text' ]
393+ ? String ( row [ 'Question Text' ] ) . trim ( )
394+ : null ;
395+ const choicesValue = row [ 'Choices' ]
396+ ? String ( row [ 'Choices' ] ) . trim ( )
397+ : null ;
398+ const correctAnswerValue = row [ 'Correct Answer' ] !== undefined && row [ 'Correct Answer' ] !== null
399+ ? String ( row [ 'Correct Answer' ] ) . trim ( )
400+ : null ;
401+ const difficultyValue = row [ 'Difficulty' ]
402+ ? String ( row [ 'Difficulty' ] ) . trim ( )
403+ : null ;
404+
405+ // Kiểm tra từng trường và báo lỗi cụ thể
406+ const missingFields : string [ ] = [ ] ;
407+ if ( ! questionText ) missingFields . push ( 'Question Text' ) ;
408+ if ( ! choicesValue ) missingFields . push ( 'Choices' ) ;
409+ if ( ! correctAnswerValue ) missingFields . push ( 'Correct Answer' ) ;
410+ if ( ! difficultyValue ) missingFields . push ( 'Difficulty' ) ;
411+
412+ if ( missingFields . length > 0 ) {
413+ throw new Error ( `Missing required fields: ${ missingFields . join ( ', ' ) } ` ) ;
332414 }
333415
416+ // Parse Choices
417+ if ( ! choicesValue ) {
418+ throw new Error ( 'Choices is required' ) ;
419+ }
334420 let choices : string [ ] ;
335421 try {
336422 // Try parsing as JSON array first
337- choices = JSON . parse ( row [ 'Choices' ] ) ;
423+ choices = JSON . parse ( choicesValue ) ;
424+ if ( ! Array . isArray ( choices ) ) {
425+ throw new Error ( 'Choices must be a JSON array' ) ;
426+ }
338427 } catch {
339- // Fallback: split by comma or pipe if it's a string
340- if ( typeof row [ 'Choices' ] === 'string' ) {
341- choices = row [ 'Choices' ] . split ( '|' ) . map ( ( c ) => c . trim ( ) ) ;
428+ // Fallback: split by pipe if it's a string
429+ if ( typeof choicesValue === 'string' ) {
430+ choices = choicesValue . split ( '|' ) . map ( ( c ) => c . trim ( ) ) . filter ( ( c ) => c . length > 0 ) ;
342431 } else {
343- throw new Error ( 'Invalid choices format' ) ;
432+ throw new Error ( 'Invalid choices format: must be a string separated by "|" or a JSON array ' ) ;
344433 }
345434 }
346435
436+ // Validate số lượng choices
347437 if ( ! Array . isArray ( choices ) || choices . length < 2 ) {
348- throw new Error ( 'At least 2 choices are required' ) ;
438+ throw new Error ( `At least 2 choices are required, but found ${ choices . length } choice(s)` ) ;
439+ }
440+
441+ // Kiểm tra choices rỗng
442+ const emptyChoices = choices . filter ( ( c ) => ! c || c . trim ( ) . length === 0 ) ;
443+ if ( emptyChoices . length > 0 ) {
444+ throw new Error ( `Found ${ emptyChoices . length } empty choice(s). All choices must have content.` ) ;
349445 }
350446
351- const correctAnswer = Number ( row [ 'Correct Answer' ] ) ;
447+ // Validate Correct Answer
448+ const correctAnswer = Number ( correctAnswerValue ) ;
449+ if ( isNaN ( correctAnswer ) ) {
450+ throw new Error ( `Correct Answer must be a number, but got: ${ correctAnswerValue } ` ) ;
451+ }
452+
352453 if ( correctAnswer < 0 || correctAnswer >= choices . length ) {
353- throw new Error ( `correctAnswer must be between 0 and ${ choices . length - 1 } ` ) ;
454+ throw new Error ( `Correct Answer must be between 0 and ${ choices . length - 1 } , but got: ${ correctAnswer } ` ) ;
354455 }
355456
356- const difficulty = row [ 'Difficulty' ] . toLowerCase ( ) ;
457+ // Validate Difficulty
458+ if ( ! difficultyValue ) {
459+ throw new Error ( 'Difficulty is required' ) ;
460+ }
461+ const difficulty = difficultyValue . toLowerCase ( ) ;
357462 if ( ! Object . values ( QuestionDifficulty ) . includes ( difficulty as QuestionDifficulty ) ) {
358- throw new Error ( `Invalid difficulty: ${ difficulty } ` ) ;
463+ throw new Error ( `Invalid difficulty: "${ difficultyValue } ". Must be one of: ${ Object . values ( QuestionDifficulty ) . join ( ', ' ) } ` ) ;
464+ }
465+
466+ if ( ! questionText ) {
467+ throw new Error ( 'Question Text is required' ) ;
359468 }
360469
361470 return {
362471 courseId,
363472 createdById,
364- questionText : row [ 'Question Text' ] ,
473+ questionText,
365474 choices,
366475 correctAnswer,
367476 difficulty : difficulty as QuestionDifficulty ,
0 commit comments