diff --git a/src/main/webapp/app/scanexam/coursdetail/coursdetails.component.ts b/src/main/webapp/app/scanexam/coursdetail/coursdetails.component.ts index 60f4f6a5..fced4f62 100644 --- a/src/main/webapp/app/scanexam/coursdetail/coursdetails.component.ts +++ b/src/main/webapp/app/scanexam/coursdetail/coursdetails.component.ts @@ -24,6 +24,7 @@ import { firstValueFrom, scan } from 'rxjs'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { PreferenceService } from '../preference-page/preference.service'; import { Title } from '@angular/platform-browser'; +import { ExportResultService, formatDateTime } from '../exportresult.service'; export interface CacheUploadNotification { setMessage(v: string): void; @@ -79,6 +80,7 @@ export class CoursdetailsComponent implements OnInit { private http: HttpClient, private preferenceService: PreferenceService, private titleService: Title, + private exportResultService: ExportResultService, ) {} ngOnInit(): void { @@ -225,6 +227,22 @@ export class CoursdetailsComponent implements OnInit { this.confirmeDelete(); }, }, + { + label: this.translateService.instant('scanexam.exportExcel'), + icon: this.appConfig.getFrontUrl() + 'content/images/exportExcel.svg', + title: this.translateService.instant('scanexam.exportExceldetail'), + command1: () => { + this.exportExcel(); + }, + }, + { + label: this.translateService.instant('scanexam.exportCsv'), + icon: this.appConfig.getFrontUrl() + 'content/images/exportCsv.svg', + title: this.translateService.instant('scanexam.exportCsvdetail'), + command1: () => { + this.exportCsv(); + }, + }, { label: this.translateService.instant('scanexam.export'), icon: this.appConfig.getFrontUrl() + 'content/images/import-export-outline-icon.svg', @@ -285,4 +303,29 @@ export class CoursdetailsComponent implements OnInit { changeStartPreference(): void { this.preferenceService.setFirstCourseMessage(this.firsthelpvalue); } + + async exportExcel(): Promise { + const examResults = new Map(); + const examNames = new Map(); + for (const exam of this.exams) { + const s = await firstValueFrom(this.http.get(this.applicationConfigService.getEndpointFor('api/showResult/' + exam.id))); + examResults.set(exam.id!, s); + examNames.set(exam.id!, exam.name ? exam.name : 'exam' + exam.id!); + } + const filename = this.course?.name ? 'students_export-' + this.course.name + '-' + formatDateTime(new Date()) : 'students'; + this.exportResultService.exportExcelForModule(examNames, examResults, filename); + } + + async exportCsv(): Promise { + const examResults = new Map(); + const examNames = new Map(); + for (const exam of this.exams) { + const s = await firstValueFrom(this.http.get(this.applicationConfigService.getEndpointFor('api/showResult/' + exam.id))); + examResults.set(exam.id!, s); + examNames.set(exam.id!, exam.name ? exam.name : 'exam' + exam.id!); + } + const filename = this.course?.name ? 'students_export-' + this.course.name + '-' + formatDateTime(new Date()) : 'students'; + + this.exportResultService.exportCsvForModule(examNames, examResults, filename); + } } diff --git a/src/main/webapp/app/scanexam/exportresult.service.ts b/src/main/webapp/app/scanexam/exportresult.service.ts new file mode 100644 index 00000000..f142a1b2 --- /dev/null +++ b/src/main/webapp/app/scanexam/exportresult.service.ts @@ -0,0 +1,215 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { download, generateCsv, mkConfig } from 'export-to-csv'; +import FileSaver from 'file-saver'; +import { firstValueFrom } from 'rxjs/internal/firstValueFrom'; +import { WorkSheet } from 'xlsx'; + +const csvConfig = mkConfig({ useKeysAsHeaders: true }); + +@Injectable({ + providedIn: 'root', +}) +export class ExportResultService { + constructor( + protected applicationConfigService: ApplicationConfigService, + private http: HttpClient, + ) {} + + prepareDataToExport(studentsresult: any[], libelles: any, deleteId: boolean): void { + let maxQuestion = 0; + studentsresult.forEach(res => { + // eslint-disable-next-line no-console + for (const key in res.notequestions) { + // eslint-disable-next-line no-prototype-builtins + if (res.notequestions.hasOwnProperty(key)) { + if (+key > maxQuestion) { + maxQuestion = +key; + } + } + } + }); + studentsresult.forEach(res => { + for (let i = 1; i <= maxQuestion; i++) { + if (libelles[i] !== undefined && libelles[i] !== '') { + res['Q' + i + ' (' + libelles[i] + ')'] = undefined; + } else { + res['Q' + i] = undefined; + } + } + }); + + studentsresult.forEach(res => { + if (res['note'] !== undefined && (typeof res['note'] === 'string' || res['note'] instanceof String)) { + res['note'] = parseFloat((res['note'] as any).replaceAll(',', '.')); + } + if (res['abi'] !== undefined) { + if (res['abi'] === 1) { + res['abi'] = 'ABI'; + } else if (res['abi'] === 2) { + res['abi'] = 'ABJ'; + } else { + res['abi'] = false; + } + } + for (const key in res.notequestions) { + // eslint-disable-next-line no-prototype-builtins + if (res.notequestions.hasOwnProperty(key)) { + if (libelles[key] !== undefined && libelles[key] !== '') { + res['Q' + key + ' (' + libelles[key] + ')'] = parseFloat(res.notequestions[key].replaceAll(',', '.')); + } else { + res['Q' + key] = parseFloat(res.notequestions[key].replaceAll(',', '.')); + } + } + } + }); + studentsresult.forEach((e: any) => delete e.notequestions); + if (deleteId) { + studentsresult.forEach((e: any) => delete e.id); + } + } + + async loadLibelle(examId: number): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return (await firstValueFrom(this.http.get(this.applicationConfigService.getEndpointFor('api/getLibelleQuestions/' + examId)))) as any; + } + + async exportExcelForModule(exaname: Map, examresult: Map, fileName: string): Promise { + const xlsx = await import('xlsx'); + const sheets = new Map(); + const sheetsF: any = {}; + const sheetsName: string[] = []; + + for (const examId of examresult.keys()) { + const libelle = await this.loadLibelle(examId); + this.prepareDataToExport(examresult.get(examId)!, libelle, false); + } + + const general = new Map(); + for (const examId of examresult.keys()) { + const studentResults = examresult.get(examId)!; + for (const studentResult of studentResults) { + const res = general.get(studentResult.id) ? general.get(studentResult.id) : ({} as any); + if (!general.has(studentResult.id)) { + general.set(studentResult.id, res); + } + if (res['nom'] === undefined || res['nom'] === '') { + res['nom'] = studentResult['nom']; + } + if (res['prenom'] === undefined || res['prenom'] === '') { + res['prenom'] = studentResult['prenom']; + } + if (res['mail'] === undefined || res['mail'] === '') { + res['mail'] = studentResult['mail']; + } + if (res[exaname.get(examId)!] === undefined || res[exaname.get(examId)!] === '') { + if (studentResult['abi'] === false) { + res[exaname.get(examId)!] = studentResult['note']; + } else { + res[exaname.get(examId)!] = studentResult['abi']; + } + } + } + } + + const worksheetgen = xlsx.utils.json_to_sheet([...general.values()]); + sheets.set('summary', worksheetgen); + + for (const examId of examresult.keys()) { + for (const studentsresult of examresult.get(examId)!) { + studentsresult.forEach((e: any) => delete e.id); + } + const worksheet = xlsx.utils.json_to_sheet(examresult.get(examId)!); + sheets.set('exam ' + examId, worksheet); + } + + for (const sheet of sheets.keys()) { + sheetsF[sheet] = sheets.get(sheet); + sheetsName.push(sheet); + } + const workbook = { Sheets: sheetsF, SheetNames: sheetsName }; + const excelBuffer: any = xlsx.write(workbook, { bookType: 'xlsx', type: 'array' }); + this.saveAsExcelFile(excelBuffer, fileName); + } + + exportExcel(studentsresult: any[], libelles: any, filename: string): void { + import('xlsx').then(xlsx => { + this.prepareDataToExport(studentsresult, libelles, true); + const worksheet = xlsx.utils.json_to_sheet(studentsresult); + const workbook = { Sheets: { data: worksheet }, SheetNames: ['data'] }; + const excelBuffer: any = xlsx.write(workbook, { bookType: 'xlsx', type: 'array' }); + + this.saveAsExcelFile(excelBuffer, filename); + }); + } + + saveAsExcelFile(buffer: any, fileName: string): void { + const EXCEL_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8'; + const EXCEL_EXTENSION = '.xlsx'; + const data: Blob = new Blob([buffer], { + type: EXCEL_TYPE, + }); + FileSaver.saveAs(data, fileName + EXCEL_EXTENSION); + } + + exportCSV(studentsresult: any[], libelles: any, filename: string): void { + this.prepareDataToExport(studentsresult, libelles, true); + const csv = generateCsv(csvConfig)(studentsresult); + csvConfig.filename = filename; + // Get the button in your HTML + download(csvConfig)(csv); + } + + exportCsvForModule(exaname: Map, examresult: Map, fileName: string): void { + for (const examId of examresult.keys()) { + this.prepareDataToExport(examresult.get(examId)!, {}, false); + } + + const general = new Map(); + for (const examId of examresult.keys()) { + const studentResults = examresult.get(examId)!; + for (const studentResult of studentResults) { + const res = general.get(studentResult.id) ? general.get(studentResult.id) : ({} as any); + if (!general.has(studentResult.id)) { + general.set(studentResult.id, res); + } + if (res['nom'] === undefined || res['nom'] === '') { + res['nom'] = studentResult['nom']; + } + if (res['prenom'] === undefined || res['prenom'] === '') { + res['prenom'] = studentResult['prenom']; + } + if (res['mail'] === undefined || res['mail'] === '') { + res['mail'] = studentResult['mail']; + } + if (res[exaname.get(examId)!] === undefined || res[exaname.get(examId)!] === '') { + if (studentResult['abi'] === false) { + res[exaname.get(examId)!] = studentResult['note']; + } else { + res[exaname.get(examId)!] = studentResult['abi']; + } + } + } + } + + const csv = generateCsv(csvConfig)([...general.values()]); + csvConfig.filename = fileName; + // Get the button in your HTML + download(csvConfig)(csv); + } +} + +export function formatDate(date: Date): string { + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + return `${day}-${month}-${year}`; +} +export function formatDateTime(date: Date): string { + const formattedDate = formatDate(date); // Reuse formatDate function + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + return `${formattedDate} ${hours}:${minutes}:${seconds}`; +} diff --git a/src/main/webapp/app/scanexam/resultatstudentcourse/resultatstudentcourse.component.ts b/src/main/webapp/app/scanexam/resultatstudentcourse/resultatstudentcourse.component.ts index b9e77d43..ff9aeef9 100644 --- a/src/main/webapp/app/scanexam/resultatstudentcourse/resultatstudentcourse.component.ts +++ b/src/main/webapp/app/scanexam/resultatstudentcourse/resultatstudentcourse.component.ts @@ -9,15 +9,13 @@ import { TranslateService } from '@ngx-translate/core'; import { ApplicationConfigService } from 'app/core/config/application-config.service'; import { MessageService } from 'primeng/api'; import { ConfirmationService } from 'primeng/api'; -import * as FileSaver from 'file-saver'; import { IExam } from '../../entities/exam/exam.model'; import { ExamService } from '../../entities/exam/service/exam.service'; import { faEnvelope, faFileCsv, faFileExcel, faTemperatureThreeQuarters } from '@fortawesome/free-solid-svg-icons'; import { ExportPdfService } from '../exportanonymoupdf/exportanonymoupdf.service'; import { firstValueFrom } from 'rxjs'; import { Title } from '@angular/platform-browser'; -import { mkConfig, generateCsv, download } from 'export-to-csv'; -const csvConfig = mkConfig({ useKeysAsHeaders: true }); +import { ExportResultService, formatDateTime } from '../exportresult.service'; @Component({ selector: 'jhi-resultatstudentcourse', @@ -54,6 +52,7 @@ export class ResultatStudentcourseComponent implements OnInit { public confirmationService: ConfirmationService, public examService: ExamService, public exportPdfService: ExportPdfService, + public exportResultService: ExportResultService, private titleService: Title, ) {} @@ -176,141 +175,19 @@ export class ResultatStudentcourseComponent implements OnInit { exportExcel(): void { const studentsresult: any[] = JSON.parse(JSON.stringify(this.studentsresult)); - + const filename = this.exam?.name ? 'students_export-' + this.exam.name + '-' + formatDateTime(new Date()) : 'students'; this.loadLibelle().then(() => { - import('xlsx').then(xlsx => { - let maxQuestion = 0; - studentsresult.forEach(res => { - // eslint-disable-next-line no-console - for (const key in res.notequestions) { - // eslint-disable-next-line no-prototype-builtins - if (res.notequestions.hasOwnProperty(key)) { - if (+key > maxQuestion) { - maxQuestion = +key; - } - } - } - }); - studentsresult.forEach(res => { - for (let i = 1; i <= maxQuestion; i++) { - if (this.libelles[i] !== undefined && this.libelles[i] !== '') { - res['Q' + i + ' (' + this.libelles[i] + ')'] = undefined; - } else { - res['Q' + i] = undefined; - } - } - }); - - studentsresult.forEach(res => { - if (res['note'] !== undefined && (typeof res['note'] === 'string' || res['note'] instanceof String)) { - res['note'] = parseFloat((res['note'] as any).replaceAll(',', '.')); - } - if (res['abi'] !== undefined) { - if (res['abi'] === 1) { - res['abi'] = 'ABI'; - } else if (res['abi'] === 2) { - res['abi'] = 'ABJ'; - } else { - res['abi'] = false; - } - } - for (const key in res.notequestions) { - // eslint-disable-next-line no-prototype-builtins - if (res.notequestions.hasOwnProperty(key)) { - if (this.libelles[key] !== undefined && this.libelles[key] !== '') { - res['Q' + key + ' (' + this.libelles[key] + ')'] = parseFloat(res.notequestions[key].replaceAll(',', '.')); - } else { - res['Q' + key] = parseFloat(res.notequestions[key].replaceAll(',', '.')); - } - } - } - }); - studentsresult.forEach((e: any) => delete e.notequestions); - studentsresult.forEach((e: any) => delete e.id); - - const worksheet = xlsx.utils.json_to_sheet(studentsresult); - const workbook = { Sheets: { data: worksheet }, SheetNames: ['data'] }; - const excelBuffer: any = xlsx.write(workbook, { bookType: 'xlsx', type: 'array' }); - let filename = 'students'; - if (this.exam?.name) { - filename = 'students_export-' + this.exam.name + '-' + formatDateTime(new Date()); - } - - this.saveAsExcelFile(excelBuffer, filename); - }); + this.exportResultService.exportExcel(studentsresult, this.libelles, filename); }); } - saveAsExcelFile(buffer: any, fileName: string): void { - const EXCEL_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8'; - const EXCEL_EXTENSION = '.xlsx'; - const data: Blob = new Blob([buffer], { - type: EXCEL_TYPE, - }); - FileSaver.saveAs(data, fileName + EXCEL_EXTENSION); - } - exportCSV(): void { + const studentsresult: any[] = JSON.parse(JSON.stringify(this.studentsresult)); + const filename = this.exam?.name ? 'students_export-' + this.exam.name + '-' + formatDateTime(new Date()) : 'students'; this.loadLibelle().then(() => { - let maxQuestion = 0; - const studentsresult: any[] = JSON.parse(JSON.stringify(this.studentsresult)); - studentsresult.forEach(res => { - // eslint-disable-next-line no-console - for (const key in res.notequestions) { - // eslint-disable-next-line no-prototype-builtins - if (res.notequestions.hasOwnProperty(key)) { - if (+key > maxQuestion) { - maxQuestion = +key; - } - } - } - }); - studentsresult.forEach(res => { - for (let i = 1; i <= maxQuestion; i++) { - if (this.libelles[i] !== undefined && this.libelles[i] !== '') { - res['Q' + i + ' (' + this.libelles[i] + ')'] = undefined; - } else { - res['Q' + i] = undefined; - } - } - }); - - studentsresult.forEach(res => { - if (res['note'] !== undefined && (typeof res['note'] === 'string' || res['note'] instanceof String)) { - res['note'] = parseFloat((res['note'] as any).replaceAll(',', '.')); - } - if (res['abi'] !== undefined) { - if (res['abi'] === 1) { - res['abi'] = 'ABI'; - } else if (res['abi'] === 2) { - res['abi'] = 'ABJ'; - } else { - res['abi'] = false; - } - } - for (const key in res.notequestions) { - // eslint-disable-next-line no-prototype-builtins - if (res.notequestions.hasOwnProperty(key)) { - if (this.libelles[key] !== undefined && this.libelles[key] !== '') { - res['Q' + key + ' (' + this.libelles[key] + ')'] = parseFloat(res.notequestions[key].replaceAll(',', '.')); - } else { - res['Q' + key] = parseFloat(res.notequestions[key].replaceAll(',', '.')); - } - } - } - }); - studentsresult.forEach((e: any) => delete e.notequestions); - studentsresult.forEach((e: any) => delete e.id); - // delete this.studentsresult.notequestions - const csv = generateCsv(csvConfig)(studentsresult); - if (this.exam?.name) { - csvConfig.filename = 'students_export-' + this.exam.name + '-' + formatDateTime(new Date()); - } - // Get the button in your HTML - download(csvConfig)(csv); + this.exportResultService.exportCSV(studentsresult, this.libelles, filename); }); } - updateStudentABJ(student: any): void { if (student.id) { firstValueFrom( @@ -322,17 +199,3 @@ export class ResultatStudentcourseComponent implements OnInit { } } } - -function formatDate(date: Date): string { - const day = date.getDate().toString().padStart(2, '0'); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const year = date.getFullYear(); - return `${day}-${month}-${year}`; -} -function formatDateTime(date: Date): string { - const formattedDate = formatDate(date); // Reuse formatDate function - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - return `${formattedDate} ${hours}:${minutes}:${seconds}`; -} diff --git a/src/main/webapp/content/images/exportCsv.svg b/src/main/webapp/content/images/exportCsv.svg new file mode 100644 index 00000000..3fcbafa7 --- /dev/null +++ b/src/main/webapp/content/images/exportCsv.svg @@ -0,0 +1,50 @@ + + + + + + + + Export + diff --git a/src/main/webapp/content/images/exportExcel.svg b/src/main/webapp/content/images/exportExcel.svg new file mode 100644 index 00000000..d55b0caa --- /dev/null +++ b/src/main/webapp/content/images/exportExcel.svg @@ -0,0 +1,49 @@ + + + + + + + Export + diff --git a/src/main/webapp/i18n/en/scanexammodule.json b/src/main/webapp/i18n/en/scanexammodule.json index a0420f41..ea2537bd 100644 --- a/src/main/webapp/i18n/en/scanexammodule.json +++ b/src/main/webapp/i18n/en/scanexammodule.json @@ -458,6 +458,10 @@ "contourLengthAndNbreContour": "Length and number of contours", "contourLengthTooltip": "One-dimensional Kmeans based on contour length", "contourLengthAndNbreContourTooltip": "Two-dimensional Kmeans based on the length of contours and the number of contours with more than three pixels", - "abiabj": "Excused absence" + "abiabj": "Excused absence", + "exportCsv": "Export module grades in CSV format", + "exportExcel": "Export module grades in Excel format", + "exportCsvdetail": "Export grades for all exams in the module in CSV format", + "exportExceldetail": "Export grades for all exams in the module in Excel format (with details for each exam)" } } diff --git a/src/main/webapp/i18n/fr/scanexammodule.json b/src/main/webapp/i18n/fr/scanexammodule.json index f97b5d58..80e7a3e5 100644 --- a/src/main/webapp/i18n/fr/scanexammodule.json +++ b/src/main/webapp/i18n/fr/scanexammodule.json @@ -462,6 +462,10 @@ "contourLengthAndNbreContour": "Longueur et nombre de contours", "contourLengthTooltip": "Kmeans à une dimension fondé sur la longueur des contours", "contourLengthAndNbreContourTooltip": "Kmeans à deux dimensions fondé sur la longueur des contours et le nombre de contours de plus de trois pixels", - "abiabj": "Absence justifiée" + "abiabj": "Absence justifiée", + "exportCsv": "Exporter les notes du module au format CSV", + "exportExcel": "Exporter les notes du module au format Excel", + "exportCsvdetail": "Exporter les notes de tous les examens du module au format CSV", + "exportExceldetail": "Exporter les notes de tous les examens du module au format Excel (avec les détails pour chaque exam)" } }