diff --git a/angular.json b/angular.json index b232b7f9f..cb7ad6a57 100644 --- a/angular.json +++ b/angular.json @@ -30,8 +30,7 @@ "scripts": [ "node_modules/jquery/dist/jquery.js", "node_modules/bootstrap/dist/js/bootstrap.js", - "node_modules/bootstrap-notify/bootstrap-notify.js", - "node_modules/chartist/dist/chartist.js" + "node_modules/bootstrap-notify/bootstrap-notify.js" ] }, "configurations": { diff --git a/package.json b/package.json index c51c02de7..4607cce47 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,15 @@ "@types/googlemaps": "3.43.3", "animate.css": "4.1.1", "arrive": "2.4.1", - "bootstrap": "3.3.7", + "bootstrap": "^3.3.7", "bootstrap-notify": "3.1.3", "chartist": "0.11.4", "googleapis": "66.0.0", "jquery": "3.5.1", + "jwt-decode": "^4.0.0", "perfect-scrollbar": "1.5.0", "rxjs": "~7.5.0", + "sweetalert2": "^11.17.2", "tslib": "^2.3.0", "zone.js": "~0.11.4" }, @@ -43,22 +45,22 @@ "@angular-devkit/build-angular": "^14.2.3", "@angular/cli": "~14.2.3", "@angular/compiler-cli": "^14.2.0", + "@types/chartist": "0.11.0", "@types/jasmine": "~5.1.4", + "@types/jasminewd2": "~2.0.13", + "@types/jquery": "3.5.30", + "@types/node": "20.14.11", + "codelyzer": "6.0.2", + "cross-env": "^7.0.3", "jasmine-core": "~4.3.0", + "jasmine-spec-reporter": "~7.0.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.0.0", - "typescript": "~4.7.2", - "@types/jasminewd2": "~2.0.13", - "@types/chartist": "0.11.0", - "@types/jquery": "3.5.30", - "@types/node": "20.14.11", - "codelyzer": "6.0.2", - "jasmine-spec-reporter": "~7.0.0", "protractor": "7.0.0", "ts-node": "~10.7.0", - "cross-env": "^7.0.3" + "typescript": "~4.7.2" } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 24da5d994..225b8fe0b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -3,6 +3,10 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { AuthInterceptor } from './interceptors/auth.interceptor'; + import { AppRoutingModule } from './app.routing'; import { NavbarModule } from './shared/navbar/navbar.module'; @@ -12,6 +16,14 @@ import { SidebarModule } from './sidebar/sidebar.module'; import { AppComponent } from './app.component'; import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component'; +import { LoginComponent } from './login/login.component'; +import { UsersListComponent } from './users-list/users-list.component'; +import { FormateurListComponent } from './formateur-list/formateur-list.component'; +import { FormationListeComponent } from './formation-liste/formation-liste.component'; +import { ParticipantListComponent } from './participant-list/participant-list.component'; +import { SearchPaginationComponent } from './search-pagination/search-pagination.component'; +import { EmployeurListComponent } from './employeur-list/employeur-list.component'; +import { NotAuthorizedComponent } from './not-authorized/not-authorized.component'; @NgModule({ imports: [ @@ -22,13 +34,23 @@ import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.compon NavbarModule, FooterModule, SidebarModule, + ReactiveFormsModule, + AppRoutingModule ], declarations: [ AppComponent, - AdminLayoutComponent - ], - providers: [], + AdminLayoutComponent, + LoginComponent, + UsersListComponent, + FormateurListComponent, + FormationListeComponent, + ParticipantListComponent, + SearchPaginationComponent, + EmployeurListComponent, + NotAuthorizedComponent, +], + providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 993dc346d..be35698c7 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -3,24 +3,29 @@ import { CommonModule, } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; import { Routes, RouterModule } from '@angular/router'; +import { LoginComponent } from './login/login.component'; import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component'; const routes: Routes =[ + { + path: 'login', + component: LoginComponent, + }, { path: '', - redirectTo: 'dashboard', + redirectTo: 'login', pathMatch: 'full', }, { path: '', component: AdminLayoutComponent, - children: [ + children: [ { path: '', loadChildren: () => import('./layouts/admin-layout/admin-layout.module').then(x => x.AdminLayoutModule) }]}, { path: '**', - redirectTo: 'dashboard' + redirectTo: 'login' } ]; diff --git a/src/app/employeur-list.service.spec.ts b/src/app/employeur-list.service.spec.ts new file mode 100644 index 000000000..9671fdb5b --- /dev/null +++ b/src/app/employeur-list.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EmployeurListService } from './employeur-list.service'; + +describe('EmployeurListService', () => { + let service: EmployeurListService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EmployeurListService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/employeur-list.service.ts b/src/app/employeur-list.service.ts new file mode 100644 index 000000000..0408ff33a --- /dev/null +++ b/src/app/employeur-list.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class EmployeurListService { + + constructor() { } +} diff --git a/src/app/employeur-list/employeur-list.component.css b/src/app/employeur-list/employeur-list.component.css new file mode 100644 index 000000000..20575b25a --- /dev/null +++ b/src/app/employeur-list/employeur-list.component.css @@ -0,0 +1,56 @@ +/* Amélioration de l'apparence des boutons */ +.btn { + border-radius: 4px; + font-weight: 500; + padding: 0.375rem 0.75rem; + transition: all 0.2s ease-in-out; + margin-right: 8px; +} + +.btn-sm { + font-size: 1.3rem; + padding: 0.25rem 0.5rem; +} + +.btn-info { + box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3); +} + +.btn-danger { + box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3); +} + +.btn-primary { + box-shadow: 0 2px 4px rgba(13, 110, 253, 0.3); +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn i { + margin-right: 5px; +} + +/* Amélioration de l'espacement dans le tableau */ +.table td, .table th { + padding: 12px 15px; + vertical-align: middle; +} + +/* Amélioration pour le bouton Ajouter */ +.header .btn-primary { + padding: 0.5rem 1rem; + display: flex; + align-items: center; +} + +.header .btn-primary i { + margin-right: 8px; +} + +.modal-body { + max-height: 400px; /* Taille maximale du contenu */ + overflow-y: auto; /* Permet de faire défiler le contenu */ +} \ No newline at end of file diff --git a/src/app/employeur-list/employeur-list.component.html b/src/app/employeur-list/employeur-list.component.html new file mode 100644 index 000000000..87d4a24bb --- /dev/null +++ b/src/app/employeur-list/employeur-list.component.html @@ -0,0 +1,112 @@ +
+
+
+
+
+
+
+
+

Liste des employeurs

+

Gérer les employeurs

+
+ +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + +
{{ cell }}Actions
{{ employeur.id }}{{ employeur.nomEmployeur }} +
+ + +
+
+
+
+
+
+
+
+ + + + + + + + diff --git a/src/app/employeur-list/employeur-list.component.spec.ts b/src/app/employeur-list/employeur-list.component.spec.ts new file mode 100644 index 000000000..93c70abb0 --- /dev/null +++ b/src/app/employeur-list/employeur-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EmployeurListComponent } from './employeur-list.component'; + +describe('EmployeurListComponent', () => { + let component: EmployeurListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EmployeurListComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EmployeurListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/employeur-list/employeur-list.component.ts b/src/app/employeur-list/employeur-list.component.ts new file mode 100644 index 000000000..af595a964 --- /dev/null +++ b/src/app/employeur-list/employeur-list.component.ts @@ -0,0 +1,274 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EmployeurService } from 'app/services/employeur.service'; +import Swal from 'sweetalert2'; +declare interface TableData { + headerRow: string[]; + dataRows: string[][]; +} + +@Component({ + selector: 'app-employeurs-list', + templateUrl: './employeur-list.component.html', + styleUrls: ['./employeur-list.component.css'] +}) +export class EmployeurListComponent implements OnInit { + public tableData1: TableData; + public employeurs: any[] = []; + public employeurForm: FormGroup; + public isEditMode: boolean = false; + public selectedEmployeurIndex: number = -1; + public selectedEmployeur: string[] = null; + + // Pagination + public filteredEmployeurs: any[] = []; + public pageSize: number = 5; + public currentPage: number = 1; + // fin - Pagination + + + // Variables pour contrôler l'affichage des modals + public showEmployeurModal: boolean = false; + public showDeleteModal: boolean = false; + + constructor(private formBuilder: FormBuilder, private employeurService: EmployeurService) { } + + ngOnInit() { + // Pagination + const savedPage = localStorage.getItem('employeursListCurrentPage'); + this.currentPage = savedPage ? parseInt(savedPage) : 1; + // Fin - Pagination + + let data: string[][] = []; + this.loadEmployeurs(); // Charger les employeurs + + this.tableData1 = { + headerRow: ['ID', 'Nom'], + dataRows: data + }; + + this.initForm(); + } + + loadEmployeurs(): void { + + this.employeurService.getEmployeurs().subscribe({ + next: (data) => { + console.log(data) + this.employeurs = data; + // Pagination + this.filteredEmployeurs = data; + this.tableData1.dataRows = data.map((employeur: any) => [ + String(employeur.id), + String(employeur.nomEmployeur) + ]); + }, + error: (error) => { + console.error('Erreur lors du chargement des employeurs', error); + } + }); + } + ngAfterViewInit(){ + this.loadEmployeurs(); + } + initForm() { + this.employeurForm = this.formBuilder.group({ + id: ['', Validators.required], + nomEmployeur: ['', Validators.required] + }); + } + + openAddModal() { + this.isEditMode = false; + this.selectedEmployeurIndex = -1; + + // Générer un nouvel ID + const nextId = (Math.max(...this.tableData1.dataRows.map(row => parseInt(row[0]))) + 1).toString(); + + this.employeurForm.reset(); + this.employeurForm.patchValue({ + id: nextId, + nomEmployeur: '' + }); + + this.showEmployeurModal = true; + } + + openEditModal(index: number) { + this.isEditMode = true; + this.selectedEmployeurIndex = index; + const employeurData = this.employeurs[index]; + + this.employeurForm.patchValue({ + id: employeurData.id, + nomEmployeur: employeurData.nomEmployeur + }); + + this.showEmployeurModal = true; + } + + saveEmployeur() { + if (this.employeurForm.invalid) { + console.log("Formulaire invalide"); + return; + } + + const formValues = this.employeurForm.value; + const employeurData: any = { + nomEmployeur: formValues.nomEmployeur + }; + + if (this.isEditMode) { + // Mettre à jour l'employeur existant + this.employeurService.updateEmployeur(formValues.id, employeurData).subscribe({ + next: (updatedEmployeur) => { + console.log('Employeur mis à jour:', updatedEmployeur); + Swal.fire({ + title: 'Succès!', + text: 'Employeur mis à jour avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + this.loadEmployeurs(); // Recharger la liste des employeurs + this.showEmployeurModal = false; + }, + error: (err) => { + console.error('Erreur lors de la mise à jour de l\'employeur:', err); + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La modification a échoué pour les raisons suivantes :

+ +
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + }); + } else { + // Ajouter un nouvel employeur + this.employeurService.createEmployeur(employeurData).subscribe({ + next: (createdEmployeur) => { + console.log('Employeur créé:', createdEmployeur); + Swal.fire({ + title: 'Succès!', + text: 'Le employeur a été créé avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + this.loadEmployeurs(); // Recharger la liste des employeurs + this.showEmployeurModal = false; + }, + error: (err) => { + console.error('Erreur lors de la création de l\'employeur:', err); + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La création a échoué pour les raisons suivantes :

+ +
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + }); + } + + // Fermer la modal + this.showEmployeurModal = false; + this.employeurForm.reset(); + } + ngOnDestroy() { + // Save current page when component is destroyed + localStorage.setItem('employeursListCurrentPage', this.currentPage.toString()); + } + + onSearchChange(searchTerm: string) { + this.filteredEmployeurs = this.employeurs.filter(user => + user.nomEmployeur.toLowerCase().includes(searchTerm.toLowerCase()) + + ); + this.currentPage = 1; + } + + onPageChange(page: number) { + this.currentPage = page; + localStorage.setItem('employeursListCurrentPage', page.toString()); + + } + + paginatedEmployeurs() { + + const startIndex = (this.currentPage - 1) * this.pageSize; + return this.filteredEmployeurs.slice(startIndex, startIndex + this.pageSize); + } + + + // Méthode pour supprimer un employeur + deleteEmployeur(index: number) { + // Vérification de la validité de l'index + if (!this.tableData1?.dataRows || index < 0 || index >= this.tableData1.dataRows.length) { + console.error('Index invalide ou données non chargées'); + return; + } + + // Récupérer l'employeur sélectionné + this.selectedEmployeurIndex = index; + this.selectedEmployeur = this.tableData1.dataRows[index]; + + console.log("Employeur sélectionné:", this.selectedEmployeur); + this.showDeleteModal = true; + } + + confirmDelete() { + const employeurId = this.selectedEmployeur[0]; // Supposé que l'ID est le premier élément + console.log("ID de l'employeur à supprimer:", employeurId); + + this.employeurService.deleteEmployeur(employeurId).subscribe({ + next: () => { + Swal.fire({ + title: 'Succès !', + text: 'Employeur supprimé avec succès.', + icon: 'success', + confirmButtonText: 'OK' + }); + + this.loadEmployeurs(); // Recharger la liste des employeurs + this.showDeleteModal = false; + this.selectedEmployeurIndex = -1; + this.selectedEmployeur = null; + }, + error: (err) => { + console.error('Erreur:', err); + Swal.fire({ + title: 'Erreur !', + text: 'Suppression impossible : ' + (err.error?.message || 'Cet emplyeur à peut-être des formateurs en relation!'), + icon: 'error', + confirmButtonText: 'OK' + }); + } + }); + } + + closeEmployeurModal() { + this.showEmployeurModal = false; + } + + closeDeleteModal() { + this.showDeleteModal = false; + } +} diff --git a/src/app/formateur-list/formateur-list.component.css b/src/app/formateur-list/formateur-list.component.css new file mode 100644 index 000000000..20575b25a --- /dev/null +++ b/src/app/formateur-list/formateur-list.component.css @@ -0,0 +1,56 @@ +/* Amélioration de l'apparence des boutons */ +.btn { + border-radius: 4px; + font-weight: 500; + padding: 0.375rem 0.75rem; + transition: all 0.2s ease-in-out; + margin-right: 8px; +} + +.btn-sm { + font-size: 1.3rem; + padding: 0.25rem 0.5rem; +} + +.btn-info { + box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3); +} + +.btn-danger { + box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3); +} + +.btn-primary { + box-shadow: 0 2px 4px rgba(13, 110, 253, 0.3); +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn i { + margin-right: 5px; +} + +/* Amélioration de l'espacement dans le tableau */ +.table td, .table th { + padding: 12px 15px; + vertical-align: middle; +} + +/* Amélioration pour le bouton Ajouter */ +.header .btn-primary { + padding: 0.5rem 1rem; + display: flex; + align-items: center; +} + +.header .btn-primary i { + margin-right: 8px; +} + +.modal-body { + max-height: 400px; /* Taille maximale du contenu */ + overflow-y: auto; /* Permet de faire défiler le contenu */ +} \ No newline at end of file diff --git a/src/app/formateur-list/formateur-list.component.html b/src/app/formateur-list/formateur-list.component.html new file mode 100644 index 000000000..baf493460 --- /dev/null +++ b/src/app/formateur-list/formateur-list.component.html @@ -0,0 +1,148 @@ +
+
+
+
+
+
+
+
+

Liste des formateurs

+

Gérer les formateurs

+
+ +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
{{ cell }}Actions
{{formateur.id}}{{formateur.nom}}{{formateur.prenom}}{{formateur.email}}{{formateur.tel}}{{formateur.type}}{{formateur.employeur.nomEmployeur}} +
+ + +
+
+
+
+
+
+
+
+ + + + + + + + diff --git a/src/app/formateur-list/formateur-list.component.spec.ts b/src/app/formateur-list/formateur-list.component.spec.ts new file mode 100644 index 000000000..6cd8d0edc --- /dev/null +++ b/src/app/formateur-list/formateur-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormateurListComponent } from './formateur-list.component'; + +describe('FormateurListComponent', () => { + let component: FormateurListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FormateurListComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FormateurListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/formateur-list/formateur-list.component.ts b/src/app/formateur-list/formateur-list.component.ts new file mode 100644 index 000000000..60ef1dd65 --- /dev/null +++ b/src/app/formateur-list/formateur-list.component.ts @@ -0,0 +1,343 @@ +import { Component, OnInit , } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormateurListService } from 'app/services/formateur-list.service'; +import { EmployeurService } from 'app/services/employeur.service'; +import Swal from 'sweetalert2'; + +declare interface TableData { + headerRow: string[]; + dataRows: string[][]; +} + +@Component({ + selector: 'app-formateurs-list', + templateUrl: './formateur-list.component.html', + styleUrls: ['./formateur-list.component.css'] +}) +export class FormateurListComponent implements OnInit { + public tableData1: TableData; + public formateurs : any; + public formateurForm: FormGroup; + public isEditMode: boolean = false; + public selectedFormateurIndex: number = -1; + public selectedFormateur: string[] = null; + + // Pagination + public filteredFormateurs: any[] = []; + public pageSize: number = 5; + public currentPage: number = 1; + // fin - Pagination + + // Variables pour contrôler l'affichage des modals + public showFormateurModal: boolean = false; + public showDeleteModal: boolean = false; + employeurs: any[] = []; + types: string[]=['interne','externe'] + constructor(private formBuilder: FormBuilder , private formateurService:FormateurListService, private employeurService:EmployeurService) { } + + ngOnInit() { + // Pagination + const savedPage = localStorage.getItem('formateursListCurrentPage'); + this.currentPage = savedPage ? parseInt(savedPage) : 1; + // Fin - Pagination + let data: string[][] = []; + this.loadEmployeurs() + this.formateurService.getAllFormateurs().subscribe({ + next: (res) => { + console.log('Formateurs fetched:', res); + this.formateurs = res; + data = res.map((formateur: any) => [ + String(formateur.id), + String(formateur.nom), + String(formateur.prenom), + String(formateur.email), + String(formateur.tel), + String(formateur.type), + String(formateur.employeur) + ]); + console.log("data = ", data); + }, + error: (err) => { + console.error('Error fetching formateurs:', err); + }, + complete: () => { + console.log('Formateur fetching completed.'); + } + }); + + this.tableData1 = { + headerRow: ['ID', 'Nom', 'Prénom', 'Email', 'Téléphone','Type', 'Employeur'], + dataRows: data + }; + + this.initForm(); + } + + loadEmployeurs(): void { + this.employeurService.getEmployeurs().subscribe( + (data) => { + this.employeurs = data; + }, + (error) => { + console.error('Erreur lors du chargement des employeurs', error); + } + ); + } + ngAfterViewInit(){ + this.loadFormateurs(); + } + + initForm() { + this.formateurForm = this.formBuilder.group({ + id: ['', Validators.required], + nom: ['', Validators.required], + prenom: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + tel: ['', Validators.required], + type: ['', Validators.required], + employeur: ['', Validators.required] + }); + } + + openAddModal() { + this.isEditMode = false; + this.selectedFormateurIndex = -1; + + // Generate a new ID + const nextId = (Math.max(...this.tableData1.dataRows.map(row => parseInt(row[0]))) + 1).toString(); + + this.formateurForm.reset(); + this.formateurForm.patchValue({ + id: nextId, + nom: '', + prenom: '', + email: '', + tel: '', + type: '', + employeur: '' + }); + + this.showFormateurModal = true; + } + + openEditModal(index: number) { + this.isEditMode = true; + this.selectedFormateurIndex = index; + const formateurData = this.formateurs[index]; + + this.formateurForm.patchValue({ + id: formateurData.id, + nom: formateurData.nom, + prenom: formateurData.prenom, + email: formateurData.email, + tel: formateurData.tel, + type: formateurData.type, + employeur: formateurData.employeur.id + }); + + this.showFormateurModal = true; + } + + saveFormateur() { + if (this.formateurForm.invalid) { + console.log("unvalid"); + } + + const formValues = this.formateurForm.value; + const employeurId=formValues.employeur; + console.log("el ID DYEL EMPLOYEUR",employeurId); + const formateurData :any = { + nom: formValues.nom, + prenom: formValues.prenom, + email: formValues.email, + tel: formValues.tel, + type: formValues.type + }; + const formateurData2 :any = { + nom: formValues.nom, + prenom: formValues.prenom, + email: formValues.email, + tel: formValues.tel, + type: formValues.type, + employeur: formValues.employeur + }; + if (this.isEditMode) { + // Update existing formateur + this.formateurService.updateFormateur(formValues.id, formateurData2).subscribe({ + next: (updatedFormateur) => { + console.log('Formateur updated:', updatedFormateur); + Swal.fire({ + title: 'Succès!', + text: 'Formateur mis à jour avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + this.loadFormateurs(); // Refresh the formateur list + this.showFormateurModal = false; + }, + error: (err) => { + console.error('Error updating formateur:', err); + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La modification a échoué pour les raisons suivantes :

+
    +
  • ${err.error?.message || 'Erreur serveur'}
  • + ${err.error?.errors?.map(e => `
  • ${e}
  • `).join('') || ''} +
+
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + }); + } else { + // Add new formateur + this.formateurService.createFormateur(formateurData, employeurId).subscribe({ + next: (createdFormateur) => { + console.log('Formateur created:', createdFormateur); + Swal.fire({ + title: 'Succès!', + text: 'Le formateur a été créé avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + this.loadFormateurs(); // Refresh the formateur list + this.showFormateurModal = false; + }, + error: (err) => { + console.error('Error creating formateur:', err); + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La création a échoué pour les raisons suivantes :

+
    +
  • ${err.error?.message || 'Erreur serveur'}
  • + ${err.error?.errors?.map(e => `
  • ${e}
  • `).join('') || ''} +
+
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + }); + } + + // Close modal + this.showFormateurModal = false; + this.formateurForm.reset(); + } + ngOnDestroy() { + // Save current page when component is destroyed + localStorage.setItem('formateursListCurrentPage', this.currentPage.toString()); + } + // Add this method to refresh formateur data + loadFormateurs() { + this.formateurService.getAllFormateurs().subscribe({ + next: (res) => { + console.log('Formateurs fetched:', res); + this.formateurs = res; + // Pagination + this.filteredFormateurs = res; + // this.currentPage = 1; // Reset to first page + // Fin - Pagination + this.tableData1.dataRows = res.map((formateur: any) => [ + String(formateur.id), + String(formateur.nom), + String(formateur.prenom), + String(formateur.email), + String(formateur.tel), + String(formateur.type), + String(formateur.employeur ) + ]); + }, + error: (err) => { + console.error('Error fetching formateurs:', err); + } + }); + } + onSearchChange(searchTerm: string) { + this.filteredFormateurs = this.formateurs.filter(user => + user.nom.toLowerCase().includes(searchTerm.toLowerCase()) || + user.prenom.toLowerCase().includes(searchTerm.toLowerCase()) + ); + this.currentPage = 1; + } + + onPageChange(page: number) { + this.currentPage = page; + localStorage.setItem('formateursListCurrentPage', page.toString()); + + } + + paginatedFormateurs() { + const startIndex = (this.currentPage - 1) * this.pageSize; + return this.filteredFormateurs.slice(startIndex, startIndex + this.pageSize); + } + deleteFormateur(index: number) { + // 1. Vérifiez que l'index est valide + if (!this.tableData1?.dataRows || index < 0 || index >= this.tableData1.dataRows.length) { + console.error('Index invalide ou données non chargées'); + return; + } + + // 2. Récupérez l'élément en vérifiant son existence + this.selectedFormateurIndex = index; + this.selectedFormateur = this.tableData1.dataRows[index]; + + // 3. Vérification supplémentaire + if (!this.selectedFormateur) { + console.error('Aucun formateur trouvé à cet index'); + return; + } + + console.log("Formateur sélectionné :", this.selectedFormateur); + this.showDeleteModal = true; + } + confirmDelete() { + const formateurId = this.selectedFormateur[0]; // Assuming ID is the first element + console.log("elid fassakh",formateurId); + this.formateurService.deleteFormateur(formateurId).subscribe({ + next: () => { + this.loadFormateurs(); + // Close modal + this.showDeleteModal = false; + this.selectedFormateurIndex = -1; + this.selectedFormateur = null; + + Swal.fire({ + title: 'Succès !', + text: 'Formateur supprimé avec succès.', + icon: 'success', + confirmButtonText: 'OK' + }); + + }, + error: (err) => { + console.error('Error deleting formateur:', err); + Swal.fire({ + title: 'Erreur !', + text: 'Suppression impossible du formateur', + icon: 'error', + confirmButtonText: 'OK' + }); + } + }); +} + + closeFormateurModal() { + this.showFormateurModal = false; + } + + closeDeleteModal() { + this.showDeleteModal = false; + } +} diff --git a/src/app/formation-liste/formation-liste.component.css b/src/app/formation-liste/formation-liste.component.css new file mode 100644 index 000000000..41dd2d1f7 --- /dev/null +++ b/src/app/formation-liste/formation-liste.component.css @@ -0,0 +1,126 @@ +/* Amélioration de l'apparence des boutons */ +.btn { + border-radius: 4px; + font-weight: 500; + padding: 0.375rem 0.75rem; + transition: all 0.2s ease-in-out; + margin-right: 8px; +} + +.btn-sm { + font-size: 0.875rem; + padding: 0.25rem 0.5rem; +} + +.btn-info { + box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3); +} + +.btn-danger { + box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3); +} + +.btn-primary { + box-shadow: 0 2px 4px rgba(13, 110, 253, 0.3); +} + +.btn-success { + box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3); +} + +.btn-warning { + box-shadow: 0 2px 4px rgba(255, 193, 7, 0.3); + color: #212529; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn i { + margin-right: 5px; +} + +/* Amélioration de l'espacement dans le tableau */ +.table td, .table th { + padding: 12px 15px; + vertical-align: middle; +} + +/* Amélioration pour le bouton Ajouter */ +.header .btn-primary { + padding: 0.5rem 1rem; + display: flex; + align-items: center; +} + +.header .btn-primary i { + margin-right: 8px; +} + +/* Style pour les modales */ +.modal-body { + max-height: 70vh; + overflow-y: auto; +} + +/* Style pour la liste des participants */ +.list-group-item { + transition: background-color 0.2s ease; +} + +.list-group-item.active { + background-color: #007bff; + border-color: #007bff; +} + +.list-group-item:hover { + background-color: #f8f9fa; +} + +.list-group-item.active:hover { + background-color: #0069d9; +} + +.badge { + transition: all 0.2s ease; +} + +/* Ajustements pour les modales de grande taille */ +.modal-lg { + max-width: 800px; +} + +/* Styles pour les cartes */ +.card { + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; +} + +.card .header { + padding: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.card .content { + padding: 15px; +} + +/* Style pour l'entête du tableau */ +.table thead th { + background-color: #f8f9fa; + font-weight: 600; + border-top: none; +} + +/* Style pour les lignes alternées du tableau */ +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.02); +} + +/* Style pour le survol des lignes du tableau */ +.table-hover tbody tr:hover { + background-color: rgba(0, 123, 255, 0.05); +} \ No newline at end of file diff --git a/src/app/formation-liste/formation-liste.component.html b/src/app/formation-liste/formation-liste.component.html new file mode 100644 index 000000000..40434a738 --- /dev/null +++ b/src/app/formation-liste/formation-liste.component.html @@ -0,0 +1,277 @@ +
+
+
+
+
+
+
+
+

Liste des formations

+

Gérer les formations

+
+ +
+
+ +
+ + + + + + + + + + + + + + +
{{ cell }}Actions
{{cell}} +
+ + + + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/formation-liste/formation-liste.component.spec.ts b/src/app/formation-liste/formation-liste.component.spec.ts new file mode 100644 index 000000000..47e2490f7 --- /dev/null +++ b/src/app/formation-liste/formation-liste.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormationListeComponent } from './formation-liste.component'; + +describe('FormationListeComponent', () => { + let component: FormationListeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FormationListeComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FormationListeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/formation-liste/formation-liste.component.ts b/src/app/formation-liste/formation-liste.component.ts new file mode 100644 index 000000000..3a0ed5e3f --- /dev/null +++ b/src/app/formation-liste/formation-liste.component.ts @@ -0,0 +1,787 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormationListService } from 'app/services/formation-list.service'; +import { ParticipantListService } from 'app/services/participant-list.service'; +import { FormateurListService } from 'app/services/formateur-list.service'; +import { CertifService } from 'app/services/certif.service'; +import { EmailService } from 'app/services/email.service'; + +import Swal from 'sweetalert2'; + +@Component({ + selector: 'app-formation-liste', + templateUrl: './formation-liste.component.html', + styleUrls: ['./formation-liste.component.css'] +}) +export class FormationListeComponent implements OnInit { + + // Pagination + public filteredFormations: any[] = []; + public pageSize: number = 5; + public currentPage: number = 1; + // fin - Pagination + + public formations : any; + public selectedFormationId : any; + // Variables pour la table + tableData = { + headerRow: ['ID', 'Titre', 'Date Début', 'Date Fin', 'Durée (jours)', 'Domaine', 'Formateur', 'Budget (DT)', 'Participants'], + dataRows: [] + }; + + // Variables pour les modals + showFormationModal = false; + showDeleteModal = false; + showEmailModal = false; + showCertificatModal = false; + isEditMode = false; + selectedFormation: any = null; + selectedIndex: number = -1; + domaines:any ; + formateurs:any; + participants: any; + isLoading = false; + progress = 0; + + // Form Group + formationForm: FormGroup; + emailForm: FormGroup; + certificatForm: FormGroup; + + private currentFormationId: number | null = null; + private initialParticipantIds: number[] = []; + currentParticipants: number[] = []; // Pour le template + + // Liste des participants sélectionnés pour la formation en cours d'édition + selectedParticipants: any[] = []; + initialParticipants: any[] = []; + + + constructor(private fb: FormBuilder ,private participantListService:ParticipantListService, private formationListService:FormationListService , private formateurListService:FormateurListService,private certifService:CertifService,private emailService :EmailService) { + this.formationForm = this.fb.group({ + // id: ['', Validators.required], + titre: ['', Validators.required], + dateDebut: ['', Validators.required], + dateFin: ['', Validators.required], + duree: ['', [Validators.required, Validators.min(1)]], + domaine: ['', Validators.required], + formateur: ['', Validators.required], + budget: ['', [Validators.required, Validators.min(0)]], + participants: [[]] + }); + + this.emailForm = this.fb.group({ + objet: ['Convocation à la formation', Validators.required], + message: ['', Validators.required], + destinataires: [[], Validators.required] + }); + + this.certificatForm = this.fb.group({ + titre: ['Certificat de réussite', Validators.required], + dateGeneration: [new Date().toISOString().split('T')[0], Validators.required], + participants: [[], Validators.required] + }); + } + + ngOnInit() { + // Initialisation des données si nécessaire + // Pagination + const savedPage = localStorage.getItem('FormationListCurrentPage'); + this.currentPage = savedPage ? parseInt(savedPage) : 1; + // Fin - Pagination + this.participantListService.getAllParticipant().subscribe({ + next: (res)=>{ + this.participants = res ; + console.log("allParticipants: ",this.participants); + }, + error: (err)=>{ + console.log('Error fetching participants: ',err); + }, + complete: ()=>{ + console.log('participants fetching completed.'); + + } + }); + + this.formationListService.getAllDomaines().subscribe({ + next: (res)=>{ + this.domaines = res ; + console.log("Alldomaines: ",this.domaines); + }, + error: (err)=>{ + console.log('Error fetching Alldomaines: ',err); + }, + complete: ()=>{ + console.log('Alldomaines fetching completed.'); + + } + }); + + this.formateurListService.getAllFormateurs().subscribe({ + next: (res)=>{ + this.formateurs = res ; + console.log("formateurs: ",this.formateurs); + }, + error: (err)=>{ + console.log('Error fetching formateurs: ',err); + }, + complete: ()=>{ + console.log('formateurs fetching completed.'); + + } + }) + + + let data: string[][] = []; + this.formationListService.getAllFormations().subscribe({ + next: async (res)=>{ + console.log('Formations fetched:', res); + this.formations = res ; + data = await Promise.all(res.map(async (formation: any) => { + // Helper function to safely convert to string with fallback to empty string + const safeString = (value: any) => (value !== null && value !== undefined) ? String(value) : ''; + + // Format date - returns '' if formation.date is null/undefined + const dateStr = safeString(formation.date); + const formattedDate = dateStr && dateStr.length >= 8 + ? `${dateStr.substr(6, 2)}/${dateStr.substr(4, 2)}/${dateStr.substr(0, 4)}` + : ''; + + // Calculate end date - returns '' if date is invalid + let formattedEndDate = ''; + if (dateStr && dateStr.length >= 8) { + try { + const startDate = new Date( + parseInt(dateStr.substr(0, 4)), + parseInt(dateStr.substr(4, 2)) - 1, + parseInt(dateStr.substr(6, 2)) + ); + const endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + parseInt(formation.duree || 0)); + formattedEndDate = `${endDate.getDate().toString().padStart(2, '0')}/${(endDate.getMonth() + 1).toString().padStart(2, '0')}/${endDate.getFullYear()}`; + } catch (e) { + console.error('Date calculation error:', e); + } + } + + // Get participants count - returns 0 if error occurs + let participantsCount = 0; + try { + const participants = formation.id + ? await this.formationListService.getFormationParticipants(formation.id).toPromise() + : null; + participantsCount = participants ? participants.length : 0; + } catch (error) { + console.error(`Error fetching participants for formation ${formation.id}:`, error); + } + + // Safely handle nested objects (domaine and formateur) + const domaineLibelle = formation.domaine ? safeString(formation.domaine.libelle) : ''; + const formateurName = formation.formateur + ? `${safeString(formation.formateur.nom)} ${safeString(formation.formateur.prenom)}`.trim() + : ''; + + return [ + safeString(formation.id), + safeString(formation.titre), + formattedDate, + formattedEndDate, + safeString(formation.duree), + domaineLibelle, + formateurName, + safeString(formation.budget), + safeString(participantsCount) + ]; + })); + console.log("formations : ",data); + this.tableData.dataRows = data; + }, + error: (err)=>{ + console.log('Error fetching formations: ',err); + }, + complete: ()=>{ + console.log('formations fetching completed.'); + + } + }) + } + //Pagination + ngOnDestroy() { + // Save current page when component is destroyed + localStorage.setItem('FormationListCurrentPage', this.currentPage.toString()); + } + //Fin - Pagination + + + ngAfterViewInit(){ + this.loadFormations(); + } + + loadFormations(){ + let data: string[][] = []; + this.formationListService.getAllFormations().subscribe({ + next: async (res)=>{ + console.log('Formations fetched:', res); + this.formations = res ; + // Pagination + this.filteredFormations = res; + // this.currentPage = 1; // Reset to first page + // Fin - Pagination + data = await Promise.all(res.map(async (formation: any) => { + // Helper function to safely convert to string with fallback to empty string + const safeString = (value: any) => (value !== null && value !== undefined) ? String(value) : ''; + + // Format date - returns '' if formation.date is null/undefined + const dateStr = safeString(formation.date); + const formattedDate = dateStr && dateStr.length >= 8 + ? `${dateStr.substr(6, 2)}/${dateStr.substr(4, 2)}/${dateStr.substr(0, 4)}` + : ''; + + // Calculate end date - returns '' if date is invalid + let formattedEndDate = ''; + if (dateStr && dateStr.length >= 8) { + try { + const startDate = new Date( + parseInt(dateStr.substr(0, 4)), + parseInt(dateStr.substr(4, 2)) - 1, + parseInt(dateStr.substr(6, 2)) + ); + const endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + parseInt(formation.duree || 0)); + formattedEndDate = `${endDate.getDate().toString().padStart(2, '0')}/${(endDate.getMonth() + 1).toString().padStart(2, '0')}/${endDate.getFullYear()}`; + } catch (e) { + console.error('Date calculation error:', e); + } + } + + // Get participants count - returns 0 if error occurs + let participantsCount = 0; + try { + const participants = formation.id + ? await this.formationListService.getFormationParticipants(formation.id).toPromise() + : null; + participantsCount = participants ? participants.length : 0; + } catch (error) { + console.error(`Error fetching participants for formation ${formation.id}:`, error); + } + + // Safely handle nested objects (domaine and formateur) + const domaineLibelle = formation.domaine ? safeString(formation.domaine.libelle) : ''; + const formateurName = formation.formateur + ? `${safeString(formation.formateur.nom)} ${safeString(formation.formateur.prenom)}`.trim() + : ''; + + return [ + safeString(formation.id), + safeString(formation.titre), + formattedDate, + formattedEndDate, + safeString(formation.duree), + domaineLibelle, + formateurName, + safeString(formation.budget), + safeString(participantsCount) + ]; + })); + console.log("formations : ",data); + this.tableData.dataRows = data; + }, + error: (err)=>{ + console.log('Error fetching formations: ',err); + }, + complete: ()=>{ + console.log('formations fetching completed.'); + + } + }) + } + + //Pagination + onSearchChange(searchTerm: string) { + const term = searchTerm.toLowerCase().trim(); + console.log("paginationFormat", this.paginatedFormations()); + this.filteredFormations = !term + ? [...this.formations] + : this.formations.filter(formation => { + const formateurNomComplet = formation.formateur + ? `${formation.formateur.nom || ''} ${formation.formateur.prenom || ''}`.toLowerCase() + : ''; + + const domaineLibelle = formation.domaine?.libelle?.toLowerCase() || ''; + console.log("rech",) + return ( + formation.titre?.toLowerCase().includes(term) || + formateurNomComplet.includes(term) || + domaineLibelle.includes(term) || + formation.date?.toString().includes(term) || + formation.duree?.toString().includes(term) || + formation.budget?.toString().includes(term) + ); + }); + console.log("filteredFormations:",this.filteredFormations); + this.currentPage = 1; + } + + onPageChange(page: number) { + this.currentPage = page; + localStorage.setItem('FormationListCurrentPage', page.toString()); + } + + paginatedFormations() { + const startIndex = (this.currentPage - 1) * this.pageSize; + return this.tableData.dataRows.slice(startIndex, startIndex + this.pageSize); + } + //Fin - Pagination + // Méthodes pour la gestion des formations + openAddModal() { + this.isEditMode = false; + this.formationForm.reset(); + this.generateNewId(); + this.selectedParticipants = []; + this.showFormationModal = true; + + } + + async openEditModal(index: number) { + console.log("paginatedFormations:") + this.isEditMode = true; + this.selectedIndex = index; + console.log("selectedIndex:",this.selectedIndex); + console.log("currentPage",this.currentPage); + + const formation = this.tableData.dataRows[index]; + this.selectedFormationId = formation[0]; // Stockez l'ID de la formation + + + // Extract names from table data + const formateurName = formation[6]; // e.g., "Jean Dupont" + const domaineName = formation[5]; // e.g., "Informatique" + + // Find matching formateur ID (if names are stored as "Nom Prénom") + const selectedFormateur = this.formateurs.find(f => `${f.nom} ${f.prenom}` === formateurName); + // Find matching domaine ID + const selectedDomaine = this.domaines.find(d => d.libelle === domaineName); + +// 2. Chargement des participants existants + try { + const existingParticipants = await this.formationListService + .getFormationParticipants(this.selectedFormationId) + .toPromise(); + + this.selectedParticipants = existingParticipants || []; + this.initialParticipants = [...this.selectedParticipants]; + + + // 3. Initialisation du formulaire + this.formationForm.patchValue({ + titre: formation[1], + dateDebut: this.formatDateForInput(formation[2]), + dateFin: this.formatDateForInput(formation[3]), + duree: formation[4], + domaine: selectedDomaine?.id || null, + formateur: selectedFormateur?.id || null, + budget: formation[7] + }); + + console.log("Participants chargés:", this.selectedParticipants); + this.showFormationModal = true; + + } catch (error) { + console.error("Erreur chargement participants:", error); + Swal.fire('Erreur', 'Impossible de charger les participants', 'error'); + } +} + + closeFormationModal() { + this.showFormationModal = false; + this.formationForm.reset(); + } + + saveFormation() { + const formValues = this.formationForm.value; + + const formationData = [ + formValues.titre, + this.formatDateForDisplay(formValues.dateDebut), + this.formatDateForDisplay(formValues.dateFin), + formValues.duree.toString(), + formValues.domaine, + formValues.formateur, + formValues.budget.toString(), + this.selectedParticipants.length.toString() + ]; + const formation = { + titre:formValues.titre, + date:formValues.dateDebut.replace(/-/g, ''), + duree:formValues.duree, + budget:formValues.budget, + } + const domaineId =formValues.domaine; + const formateurId =formValues.formateur; + + if (this.isEditMode) { + // this.tableData.dataRows[this.selectedIndex] = formationData; + console.log("formationData:",formationData); + const formationId=this.tableData.dataRows[this.selectedIndex][0];; + console.log("formationId:",formationId); + console.log("InitialParticipants:",this.initialParticipants); + console.log("SelectedParticipants:",this.selectedParticipants); + + // Création de Set pour simplifier la comparaison + const initialIds = new Set(this.initialParticipants.map(p => p.id)); + const selectedIds = new Set(this.selectedParticipants.map(p => p.id)); + // Supprimer les participants retirés + for (const participantId of initialIds) { + if (!selectedIds.has(participantId)) { + this.formationListService.removeParticipantFromFormation(formationId, participantId).subscribe({ + next: () => console.log(`Participant ${participantId} supprimé de la formation`), + error: (err) => console.error(`Erreur suppression participant ${participantId}:`, err) + }); + } + } + // Ajouter les nouveaux participants + for (const participantId of selectedIds) { + if (!initialIds.has(participantId)) { + this.formationListService.addParticipantToFormation(formationId, participantId).subscribe({ + next: () => console.log(`Participant ${participantId} ajouté à la formation`), + error: (err) => console.error(`Erreur ajout participant ${participantId}:`, err) + }); + } + } + + + this.formationListService.updateFormation(formationId,formation, formateurId, domaineId).subscribe({ + next: (createdFormation) => { + console.log('Formation créée:', createdFormation); + + // Notification de succès + Swal.fire({ + title: 'Succès!', + text: 'La formation a été modifiée avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + + this.loadFormations(); // Rafraîchir la liste + this.showFormationModal = false; + }, + error: (err) => { + console.error('Erreur lors de la création:', err); + + // Notification d'erreur + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La modification a échoué pour les raisons suivantes :

+
    +
  • ${err.error?.message || 'Erreur serveur'}
  • + ${err.error?.errors?.map(e => `
  • ${e}
  • `).join('') || ''} +
+
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + }); + } else { + // Add new new formation + this.formationListService.createFormation1(formation, formateurId, domaineId).subscribe({ + next: (createdFormation) => { + console.log('Formation créée:', createdFormation); + + // Notification de succès + Swal.fire({ + title: 'Succès!', + text: 'La formation a été créée avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + + this.loadFormations(); // Rafraîchir la liste + this.showFormationModal = false; + }, + error: (err) => { + console.error('Erreur lors de la création:', err); + + // Notification d'erreur + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La création a échoué pour les raisons suivantes :

+
    +
  • ${err.error?.message || 'Erreur serveur'}
  • + ${err.error?.errors?.map(e => `
  • ${e}
  • `).join('') || ''} +
+
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + }); + } + + this.closeFormationModal(); + } + + deleteFormation(index: number) { + this.selectedIndex = index; + this.selectedFormation = this.tableData.dataRows[index]; + this.showDeleteModal = true; + console.log("selectedFormation",this.selectedFormation); + } + + closeDeleteModal() { + this.showDeleteModal = false; + this.selectedIndex = -1; + this.selectedFormation = null; + } + executeDelete() { + const formationId = this.selectedFormation[0]; + + this.formationListService.deleteFormation(formationId).subscribe({ + next: () => { + this.loadFormations(); + this.showDeleteModal = false; + this.selectedIndex = -1; + this.selectedFormation = null; + + Swal.fire({ + title: 'Succès !', + text: 'Formation supprimée avec succès.', + icon: 'success', + confirmButtonText: 'OK' + }); + }, + error: (err) => { + console.error('Erreur:', err); + Swal.fire({ + title: 'Erreur !', + text: 'Suppression impossible : ' + (err.error?.message || 'Cette formation à peut-être des participants!'), + icon: 'error', + confirmButtonText: 'OK' + }); + } + }); + } + confirmDelete() { + // Afficher une confirmation avant suppression + Swal.fire({ + title: 'Êtes-vous sûr ?', + text: 'Cette action est irréversible !', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + confirmButtonText: 'Oui, supprimer !', + cancelButtonText: 'Annuler' + }).then((result) => { + if (result.isConfirmed) { + // Lancer la suppression SEULEMENT si l'utilisateur confirme + this.executeDelete(); + } else { + // Fermer le modal sans supprimer + this.showDeleteModal = false; + this.selectedIndex = -1; + this.selectedFormation = null; + } + }); + } + + async openEmailModal(index: number) { + this.selectedIndex = index; + this.selectedFormation = this.tableData.dataRows[index]; + this.selectedFormationId = this.selectedFormation[0]; // Get the formation ID + + try { + // Fetch participants from server (same as in openEditModal) + const existingParticipants = await this.formationListService + .getFormationParticipants(this.selectedFormationId) + .toPromise(); + + this.selectedParticipants = existingParticipants || []; + const destinataires = this.selectedParticipants.map(p => p.email); + console.log("destinares:",destinataires); + + const formationTitle = this.selectedFormation[1]; + const startDate = this.selectedFormation[2]; + + this.emailForm.patchValue({ + objet: `Convocation à la formation "${formationTitle}"`, + message: `Bonjour,\n\nVous êtes convoqué(e) à la formation "${formationTitle}" qui débutera le ${startDate}.\nVoici le lien google meet de la formation https://meet.google.com/landing\nCordialement,\nLe service formation`, + destinataires: this.selectedParticipants.map(p => p.email) // Use the fetched participants + }); + + this.showEmailModal = true; + } catch (error) { + console.error("Erreur chargement participants:", error); + Swal.fire('Erreur', 'Impossible de charger les participants', 'error'); + } +} + + closeEmailModal() { + this.showEmailModal = false; + } + + sendEmail() { + const formValue=this.emailForm.value + const requestData={ + objet:formValue.objet, + message:formValue.message, + listeMails:formValue.destinataires + } + this.isLoading = true; + this.progress = 0; + Swal.fire({ + title: 'Envoi des emails...', + html: 'Veuillez patienter pendant l\'envoi des emails.

', + didOpen: () => { + Swal.showLoading(); // Montre le spinner de chargement de SweetAlert + }, + willClose: () => { + // Ferme la pop-up après l'envoi + this.isLoading = false; + }, + allowOutsideClick: false, // Empêche de fermer la pop-up en cliquant à l'extérieur + didClose: () => { + this.isLoading = false; // Assurez-vous que la pop-up se ferme correctement + } + }); + + console.log(formValue.destinataires) + this.emailService.envoyerEmails(requestData).subscribe({ + next: (response) => { + console.log(response.message); // "Certificats générés avec succès !" + Swal.fire('Succès', response.message, 'success'); + }, + error: (error) => { + console.error("Erreur lors de l'envoie des emails' :", error); + Swal.fire('Erreur', "Impossible d'envoyer les emails.", 'error'); + } + }); + let progress = 0; + const interval = setInterval(() => { + if (progress < 100) { + progress += 10; // Augmente de 10% à chaque intervalle + const progressBar = document.getElementById('progress-bar') as HTMLProgressElement; + + progressBar.value = progress; // Met à jour la valeur de la barre de progression + } else { + clearInterval(interval); // Arrête l'intervalle lorsque la barre atteint 100% + } + }, 800); // Mise à jour tous les 500ms +} + + + async openCertificatModal(index: number) { + this.selectedIndex = index; + this.selectedFormation = this.tableData.dataRows[index]; + this.selectedFormationId = this.selectedFormation[0]; // Récupérer l'ID de la formation + + try { + // Récupérer les participants depuis le serveur (comme dans openEditModal) + const existingParticipants = await this.formationListService + .getFormationParticipants(this.selectedFormationId) + .toPromise(); + + this.selectedParticipants = existingParticipants || []; + + this.certificatForm.patchValue({ + titre: `Certificat de réussite - ${this.selectedFormation[1]}`, + participants: this.selectedParticipants.map(p => `${p.nom} ${p.prenom}`) + }); + + this.showCertificatModal = true; + } catch (error) { + console.error("Erreur lors du chargement des participants:", error); + Swal.fire('Erreur', 'Impossible de charger les participants', 'error'); + } +} + + closeCertificatModal() { + this.showCertificatModal = false; + } + + generateCertificats() { + const formValue = this.certificatForm.value; + + const requestData = { + certTitle: formValue.titre, + date: formValue.dateGeneration, + participants: formValue.participants + }; + + this.certifService.generateCertificates(requestData).subscribe({ + next: (response) => { + console.log(response.message); // "Certificats générés avec succès !" + Swal.fire('Succès', response.message, 'success'); + }, + error: (error) => { + console.error('Erreur lors de la génération des certificats :', error); + Swal.fire('Erreur', 'Impossible de générer les certificats.', 'error'); + } + }); + } + + + // Gestion des participants + toggleParticipant(participant: any) { + const index = this.selectedParticipants.findIndex(p => p.id === participant.id); + + if (index === -1) { + this.selectedParticipants.push(participant); + } else { + this.selectedParticipants.splice(index, 1); + } + + this.formationForm.patchValue({ + participants: this.selectedParticipants.map(p => p.id) + }); + } + + isParticipantSelected(participant: any): boolean { + return this.selectedParticipants.some(p => p.id === participant.id); + } + + // Utilitaires + generateNewId() { + // Génère un ID basé sur le nombre de formations existantes + const newNum = this.tableData.dataRows.length + 1; + const id = 'F' + newNum.toString().padStart(3, '0'); + this.formationForm.patchValue({ id }); + } + + formatDateForInput(dateStr: string): string { + // Convertit DD/MM/YYYY en YYYY-MM-DD pour les inputs de type date + const parts = dateStr.split('/'); + return `${parts[2]}-${parts[1]}-${parts[0]}`; + } + + formatDateForDisplay(dateStr: string): string { + // Convertit YYYY-MM-DD en DD/MM/YYYY pour l'affichage + const parts = dateStr.split('-'); + return `${parts[2]}/${parts[1]}/${parts[0]}`; + } + + calculateDuration() { + // Calcule automatiquement la durée entre les dates + const dateDebut = this.formationForm.get('dateDebut')?.value; + const dateFin = this.formationForm.get('dateFin')?.value; + + if (dateDebut && dateFin) { + const start = new Date(dateDebut); + const end = new Date(dateFin); + const diffTime = Math.abs(end.getTime() - start.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 pour inclure le jour de début + + this.formationForm.patchValue({ duree: diffDays }); + } + } +} \ No newline at end of file diff --git a/src/app/guards/auth.guard.spec.ts b/src/app/guards/auth.guard.spec.ts new file mode 100644 index 000000000..68889d22d --- /dev/null +++ b/src/app/guards/auth.guard.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthGuard } from './auth.guard'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts new file mode 100644 index 000000000..296b38d2d --- /dev/null +++ b/src/app/guards/auth.guard.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, + ActivatedRouteSnapshot, + RouterStateSnapshot, + Router +} from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + + constructor(private router: Router) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + const token = localStorage.getItem('token'); + + if (!token) { + this.router.navigate(['/login']); + return false; + } + + // Vérifie les rôles autorisés depuis la route + const expectedRoles: number[] = route.data['roles']; + + if (expectedRoles && expectedRoles.length > 0) { + try { + const payload = JSON.parse(atob(token.split('.')[1])); // décode le token JWT + const userRoleId = payload.roleId; + + if (expectedRoles.includes(userRoleId)) { + return true; + } else { + this.router.navigate(['/not-authorized']); + return false; + } + } catch (error) { + console.error('Erreur de décodage du token', error); + this.router.navigate(['/login']); + return false; + } + } + + // Si aucun rôle spécifique n’est requis + return true; + } +} diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 53fc2fd3a..7df80bf7b 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,22 +1,143 @@
+
+ +
+
+
+

Statistiques Générales

+

Nombre total par catégorie

+
+
+
+ +
+
+ +

{{participantsCount}}

+

Participants

+
+
+ +
+
+ +

{{formateursCount}}

+

Formateurs

+
+
+ +
+
+ +

{{formationsCount}}

+

Formations

+
+
+ +
+
+ +

{{utilisateursCount}}

+

Utilisateurs

+
+
+
+ +
+
+
+
+ + + + +
+
+ [footerIconClass]="'fa fa-check'" + [footerText]="'Data information certified'"> +
-
+
+
+
+
+
+
+
+
+
+

Top 3 formateurs

+

Les formateurs ayant les plus grands nombres de formations

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
#NomPrénomEmailTéléphoneSpécialitéEmployeurNb formations
{{ i + 1 }}{{ formateur.nom }}{{ formateur.prenom }}{{ formateur.email }}{{ formateur.tel }}{{ formateur.specialite }}{{ formateur.employeur }}{{ formateur.nb }}
+
+
+
+
+
+
+ +
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 9aba51e04..f01ef31bf 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -2,6 +2,12 @@ import { Component, OnInit } from '@angular/core'; import { LocationStrategy, PlatformLocation, Location } from '@angular/common'; import { LegendItem, ChartType } from '../lbd/lbd-chart/lbd-chart.component'; import * as Chartist from 'chartist'; +import { ParticipantListService } from 'app/services/participant-list.service'; +import { UserListService } from 'app/services/user-list.service'; +import { FormateurListService } from 'app/services/formateur-list.service'; +import { FormationListService } from 'app/services/formation-list.service'; +import { ChangeDetectorRef } from '@angular/core'; +import { forkJoin } from 'rxjs'; @Component({ selector: 'app-home', @@ -9,104 +15,357 @@ import * as Chartist from 'chartist'; styleUrls: ['./home.component.css'] }) export class HomeComponent implements OnInit { - public emailChartType: ChartType; - public emailChartData: any; - public emailChartLegendItems: LegendItem[]; - - public hoursChartType: ChartType; - public hoursChartData: any; - public hoursChartOptions: any; - public hoursChartResponsive: any[]; - public hoursChartLegendItems: LegendItem[]; - - public activityChartType: ChartType; - public activityChartData: any; - public activityChartOptions: any; - public activityChartResponsive: any[]; - public activityChartLegendItems: LegendItem[]; - constructor() { } + // Compteurs + public participantsCount: number; + public formateursCount: number; + public formationsCount: number; + public utilisateursCount: number; + public formateurs: any; + + // Graphiques + public chartInitialized = true; // Tracks if chart should be rendered + public emailChartType: ChartType; + public emailChartData: any; + public emailChartLegendItems: LegendItem[]; + + public hoursChartType: ChartType; + public hoursChartData: any; + public hoursChartOptions: any; + public hoursChartResponsive: any[]; + public hoursChartLegendItems: LegendItem[]; + + public activityChartType: ChartType; + public activityChartData: any; + public activityChartOptions: any; + public activityChartResponsive: any[]; + public activityChartLegendItems: LegendItem[]; + loading: boolean = true; + public emailChartOptions: any; + public emailChartResponsive: any[]; + + constructor( + private participantListService: ParticipantListService, + private userListService: UserListService, + private formateurListService: FormateurListService, + private formationListService: FormationListService, + private cdr: ChangeDetectorRef + ) { } ngOnInit() { - this.emailChartType = ChartType.Pie; - this.emailChartData = { - labels: ['62%', '32%', '6%'], - series: [62, 32, 6] - }; - this.emailChartLegendItems = [ - { title: 'Open', imageClass: 'fa fa-circle text-info' }, - { title: 'Bounce', imageClass: 'fa fa-circle text-danger' }, - { title: 'Unsubscribe', imageClass: 'fa fa-circle text-warning' } - ]; - - this.hoursChartType = ChartType.Line; - this.hoursChartData = { - labels: ['9:00AM', '12:00AM', '3:00PM', '6:00PM', '9:00PM', '12:00PM', '3:00AM', '6:00AM'], - series: [ - [287, 385, 490, 492, 554, 586, 698, 695, 752, 788, 846, 944], - [67, 152, 143, 240, 287, 335, 435, 437, 539, 542, 544, 647], - [23, 113, 67, 108, 190, 239, 307, 308, 439, 410, 410, 509] - ] - }; - this.hoursChartOptions = { + this.loading = true; + + // Appeler initEmailChart ici en attendant les données réelles + this.initEmailChart(); // Garder l'initialisation par défaut tant que les données ne sont pas chargées + + forkJoin([ + this.participantListService.getCount(), + this.formateurListService.getFormateursCount(), + this.formationListService.getFormationsCount(), + this.userListService.getUtilisateursCount(), + this.formationListService.getBudgetsMensuelsTop3(), + this.formateurListService.getTopFormateurs(), + this.formationListService.getDomainePercentages() // Nouvel appel API + ]).subscribe({ + next: ([ + participantsCount, + formateursCount, + formationsCount, + utilisateursCount, + budgetData, + topFormateurs, + domaineData + ]) => { + this.participantsCount = participantsCount; + this.formateursCount = formateursCount; + this.formationsCount = formationsCount; + this.utilisateursCount = utilisateursCount; + this.formateurs = topFormateurs; + + // Mettre à jour les graphiques avec les données reçues + this.updateHoursChart(budgetData); + this.updateActivityChart(budgetData); + this.updateEmailChart(domaineData); // Appeler la nouvelle méthode + + this.loading = false; + }, + error: (error) => { + console.error('Error loading data:', error); + this.loading = false; + } + }); + } + + private resetChart() { + // 1. First destroy the chart + this.chartInitialized = false; + + // 2. Force Angular to update the view + this.cdr.detectChanges(); + + // 3. Recreate the chart after a tiny delay + setTimeout(() => { + this.chartInitialized = true; + this.cdr.detectChanges(); + }, 50); + } + + ngAfterViewInit(){ + console.log('emailChartData:',this.emailChartData); + } + + loadInitialData() { + this.loadTopFormateurs(); + + this.participantListService.getCount().subscribe( + count => this.participantsCount = count, + error => console.error('Erreur participants:', error) + ); + + this.formateurListService.getFormateursCount().subscribe( + count => this.formateursCount = count, + error => console.error('Erreur formateurs:', error) + ); + + this.formationListService.getFormationsCount().subscribe( + count => this.formationsCount = count, + error => console.error('Erreur formations:', error) + ); + + this.userListService.getUtilisateursCount().subscribe( + count => this.utilisateursCount = count, + error => console.error('Erreur utilisateurs:', error) + ); + } + + loadTopFormateurs() { + this.formateurListService.getTopFormateurs().subscribe({ + next: data => this.formateurs = data, + error: err => console.error('Erreur formateurs:', err) + }); + } + + loadBudgetData() { + this.formationListService.getBudgetsMensuelsTop3().subscribe({ + next: data => { + const correctedData = data.map(item => ({ + ...item, + budgetMoyen: Number(item.budgetMoyen) + })); + console.log('Budgets mensuels:', correctedData); + this.updateHoursChart(correctedData); // Mise à jour du graphique à barres + this.updateActivityChart(correctedData); + }, + error: err => console.error('Erreur budgets:', err) + }); + + } + updateEmailChart(domaineData: any[]) { + if (!domaineData || domaineData.length === 0) { + console.warn('Aucune donnée de domaine reçue'); + return; // Garder les valeurs par défaut si aucune donnée n'est disponible + } + + this.emailChartType = ChartType.Pie; + + // 1. Create new array references to force change detection + const formattedLabels = domaineData.map(item => `${Math.round(item.percentage)}%`); + const formattedSeries = [...domaineData.map(item => item.percentage)]; + + // 2. Update chart data with new references + this.emailChartData = { + labels: [...formattedLabels], // New array + series: formattedSeries // Already copied + }; + + // Mettre à jour les légendes + this.emailChartLegendItems = domaineData.map((item, index) => ({ + title: item.domaineName, + imageClass: this.getLegendIconClass(index) + })); + + // Ajouter des options spécifiques pour le graphique circulaire si nécessaire + this.emailChartOptions = { + height: '300px', + donut: false, + donutWidth: 30, + startAngle: 0, + total: 100, + showLabel: true, + labelOffset: 50, + labelDirection: 'explode', + labelInterpolationFnc: (value) => { + return value; + } + }; + + // Configuration responsive + this.emailChartResponsive = [ + ['screen and (max-width: 640px)', { + height: '240px', + chartPadding: 10, + labelOffset: 60, + labelDirection: 'explode' + }] + ]; + // 5. Force chart reset + this.resetChart(); + + console.log('Chart data updated:', this.emailChartData); + console.log('Email Chart Legends updated:', this.emailChartLegendItems); + } + updateHoursChart(backendData: any[]) { + // 1. Extraire les 3 premiers domaines uniques + const top3Domaines = [...new Set(backendData.map(item => item.domaine))].slice(0, 3); + + // 2. Créer un tableau complet des 12 mois + const allMonths = Array.from({ length: 12 }, (_, i) => i + 1); + const moisLabels = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Dec']; + + // 3. Préparer les séries de données pour chaque domaine + const series = top3Domaines.map(domaine => { + return allMonths.map(mois => { + const item = backendData.find(d => d.domaine === domaine && d.mois === mois); + return item ? item.budgetMoyen : 0; + }); + }); + + // 4. Configurer le graphique à barres groupées + this.hoursChartType = ChartType.Line; + this.hoursChartData = { + labels: moisLabels, + series: series + }; + + // 5. Options du graphique + this.hoursChartOptions = { + seriesBarDistance: 15, + stackBars: false, + axisX: { + showGrid: false, + labelInterpolationFnc: (value: any, index: any) => index % 2 === 0 ? value : null + }, + axisY: { + type: Chartist.FixedScaleAxis, low: 0, - high: 800, - showArea: true, - height: '245px', - axisX: { - showGrid: false, - }, - lineSmooth: Chartist.Interpolation.simple({ - divisor: 3 - }), - showLine: false, - showPoint: false, - }; - this.hoursChartResponsive = [ - ['screen and (max-width: 640px)', { - axisX: { - labelInterpolationFnc: function (value) { - return value[0]; - } - } - }] - ]; - this.hoursChartLegendItems = [ - { title: 'Open', imageClass: 'fa fa-circle text-info' }, - { title: 'Click', imageClass: 'fa fa-circle text-danger' }, - { title: 'Click Second Time', imageClass: 'fa fa-circle text-warning' } - ]; - - this.activityChartType = ChartType.Bar; - this.activityChartData = { - labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - series: [ - [542, 443, 320, 780, 553, 453, 326, 434, 568, 610, 756, 895], - [412, 243, 280, 580, 453, 353, 300, 364, 368, 410, 636, 695] - ] - }; - this.activityChartOptions = { + high: 10000, + ticks: [0, 2000, 4000, 6000, 8000, 10000], + labelInterpolationFnc: (value: any) => `${value}` + }, + height: '300px' + }; + + this.hoursChartResponsive = [ + ['screen and (max-width: 640px)', { seriesBarDistance: 10, axisX: { - showGrid: false - }, - height: '245px' - }; - this.activityChartResponsive = [ - ['screen and (max-width: 640px)', { - seriesBarDistance: 5, - axisX: { - labelInterpolationFnc: function (value) { - return value[0]; - } - } - }] - ]; - this.activityChartLegendItems = [ - { title: 'Tesla Model S', imageClass: 'fa fa-circle text-info' }, - { title: 'BMW 5 Series', imageClass: 'fa fa-circle text-danger' } - ]; + labelInterpolationFnc: (value: any) => value[0] + } + }] + ]; + // 6. Configurer les légendes + this.hoursChartLegendItems = top3Domaines.map((domaine, index) => ({ + title: domaine, + imageClass: this.getLegendIconClass(index), + color: this.getDomainColor(index) + })); + } - } -} + + updateActivityChart(backendData: any[]) { + // 1. Extraire les 3 premiers domaines uniques + const top3Domaines = [...new Set(backendData.map(item => item.domaine))].slice(0, 3); + + // 2. Créer un tableau complet des 12 mois + const allMonths = Array.from({ length: 12 }, (_, i) => i + 1); + const moisLabels = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Dec']; + + // 3. Préparer les séries de données pour chaque domaine + const series = top3Domaines.map(domaine => { + return allMonths.map(mois => { + const item = backendData.find(d => d.domaine === domaine && d.mois === mois); + return item ? item.budgetMoyen : 0; + }); + }); + + // 4. Configurer le graphique à barres groupées + this.activityChartType = ChartType.Bar; + this.activityChartData = { + labels: moisLabels, + series: series + }; + console.log('activityChartData', this.activityChartData); + + // 5. Options du graphique + this.activityChartOptions = { + seriesBarDistance: 15, // Espace entre les groupes de barres (mois) + stackBars: false, // Barres côte à côte (pas empilées) + axisX: { + showGrid: false, + labelInterpolationFnc: (value: any, index: any) => index % 2 === 0 ? value : null // Affiche un label sur deux + }, + axisY: { + type: Chartist.FixedScaleAxis, + low: 0, + high: 10000, // Échelle fixe jusqu'à 10000 DT + ticks: [0, 2000, 4000, 6000, 8000, 10000], // Graduations de l'axe Y + labelInterpolationFnc: (value: any) => `${value}` // Formatage des valeurs + }, + height: '300px' // Hauteur fixe du graphique + }; + + // 6. Configuration responsive + this.activityChartResponsive = [ + ['screen and (max-width: 640px)', { + seriesBarDistance: 10, // Réduit l'espacement sur mobile + axisX: { + labelInterpolationFnc: value => value[0] // Affiche seulement la première lettre du mois + } + }] + ]; + + // 7. Configuration des légendes + this.activityChartLegendItems = top3Domaines.map((domaine, index) => ({ + title: domaine, + imageClass: this.getLegendIconClass(index), // Classe CSS pour l'icône + color: this.getDomainColor(index) // Couleur correspondante + })); + } + + + + initEmailChart() { + this.emailChartType = ChartType.Pie; + this.emailChartData = { + labels: [], + series: [] + }; + this.emailChartLegendItems = [ + { title: 'Open', imageClass: 'fa fa-circle text-info' }, + { title: 'Bounce', imageClass: 'fa fa-circle text-danger' }, + { title: 'Unsubscribe', imageClass: 'fa fa-circle text-warning' } + ]; + } + + getDomainColor(index: number): string { + const colors = ['#4CAF50', '#2196F3', '#FF5722', '#9C27B0', '#FFC107']; + return colors[index % colors.length]; + } + + getLegendIconClass(index: number): string { + const classes = [ + 'text-info', // bleu + 'text-danger', // rouge + 'text-warning', // jaune/orange + 'text-success', // vert + 'text-primary', // bleu foncé + 'text-secondary', // gris + 'text-dark', // noir + 'text-muted', // gris clair + 'text-info', // bleu (répété) + 'text-danger' // rouge (répété) + ]; + return `fa fa-circle ${classes[index % classes.length]}`; + } +} \ No newline at end of file diff --git a/src/app/interceptors/auth.interceptor.spec.ts b/src/app/interceptors/auth.interceptor.spec.ts new file mode 100644 index 000000000..7ab58dbd1 --- /dev/null +++ b/src/app/interceptors/auth.interceptor.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthInterceptor } from './auth.interceptor'; + +describe('AuthInterceptor', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + AuthInterceptor + ] + })); + + it('should be created', () => { + const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor); + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/app/interceptors/auth.interceptor.ts b/src/app/interceptors/auth.interceptor.ts new file mode 100644 index 000000000..fe935125c --- /dev/null +++ b/src/app/interceptors/auth.interceptor.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor +} from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + + constructor() {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + const token = localStorage.getItem('token'); + if (token) { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + } + return next.handle(request); + } +} diff --git a/src/app/interfaces/login-request.interface.ts b/src/app/interfaces/login-request.interface.ts new file mode 100644 index 000000000..2b26d9f8c --- /dev/null +++ b/src/app/interfaces/login-request.interface.ts @@ -0,0 +1,4 @@ +export interface LoginRequest { + login: string; + motDePasse: string; + } \ No newline at end of file diff --git a/src/app/layouts/admin-layout/admin-layout.routing.ts b/src/app/layouts/admin-layout/admin-layout.routing.ts index e09417d82..6d920fcce 100644 --- a/src/app/layouts/admin-layout/admin-layout.routing.ts +++ b/src/app/layouts/admin-layout/admin-layout.routing.ts @@ -2,20 +2,38 @@ import { Routes } from '@angular/router'; import { HomeComponent } from '../../home/home.component'; import { UserComponent } from '../../user/user.component'; +import { NotAuthorizedComponent } from 'app/not-authorized/not-authorized.component'; import { TablesComponent } from '../../tables/tables.component'; import { TypographyComponent } from '../../typography/typography.component'; import { IconsComponent } from '../../icons/icons.component'; import { MapsComponent } from '../../maps/maps.component'; import { NotificationsComponent } from '../../notifications/notifications.component'; import { UpgradeComponent } from '../../upgrade/upgrade.component'; +import { UsersListComponent } from 'app/users-list/users-list.component'; +import { FormateurListComponent } from 'app/formateur-list/formateur-list.component'; +import { FormationListeComponent } from 'app/formation-liste/formation-liste.component'; +import { ParticipantListComponent } from 'app/participant-list/participant-list.component'; +import { AuthGuard } from 'app/guards/auth.guard'; +import { EmployeurListComponent } from 'app/employeur-list/employeur-list.component'; export const AdminLayoutRoutes: Routes = [ - { path: 'dashboard', component: HomeComponent }, - { path: 'user', component: UserComponent }, - { path: 'table', component: TablesComponent }, - { path: 'typography', component: TypographyComponent }, - { path: 'icons', component: IconsComponent }, - { path: 'maps', component: MapsComponent }, - { path: 'notifications', component: NotificationsComponent }, - { path: 'upgrade', component: UpgradeComponent }, + { path: 'dashboard', component: HomeComponent ,canActivate: [AuthGuard] , data: { roles: [1, 2] }}, + { path: 'user', component: UserComponent ,canActivate: [AuthGuard]}, + + { path: 'user-list', component: UsersListComponent ,canActivate: [AuthGuard], data: { roles: [1] } }, + { path: 'participant-list', component: ParticipantListComponent ,canActivate: [AuthGuard], data: { roles: [1, 2, 3]}}, + { path: 'table', component: TablesComponent ,canActivate: [AuthGuard]}, + { path: 'typography', component: TypographyComponent ,canActivate: [AuthGuard]}, + { path: 'icons', component: IconsComponent ,canActivate: [AuthGuard]}, + { path: 'maps', component: MapsComponent ,canActivate: [AuthGuard]}, + { path: 'notifications', component: NotificationsComponent ,canActivate: [AuthGuard]}, + { path: 'upgrade', component: UpgradeComponent ,canActivate: [AuthGuard]}, + {path: 'formateur-list', component:FormateurListComponent ,canActivate: [AuthGuard], data: { roles: [1, 2, 3]}}, + {path: 'formation-liste', component:FormationListeComponent ,canActivate: [AuthGuard], data: { roles: [1, 2, 3]}}, + { path: 'participant-list', component: ParticipantListComponent ,canActivate: [AuthGuard], data: { roles: [1, 2, 3]}}, + { path: 'employeur-list', component: EmployeurListComponent ,canActivate: [AuthGuard], data: { roles: [1, 2, 3]}}, + { + path: 'not-authorized', + component: NotAuthorizedComponent + } ]; diff --git a/src/app/lbd/lbd-chart/lbd-chart.component.css b/src/app/lbd/lbd-chart/lbd-chart.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/lbd/lbd-chart/lbd-chart.component.ts b/src/app/lbd/lbd-chart/lbd-chart.component.ts index 059800328..eec88e51e 100644 --- a/src/app/lbd/lbd-chart/lbd-chart.component.ts +++ b/src/app/lbd/lbd-chart/lbd-chart.component.ts @@ -15,6 +15,7 @@ export enum ChartType { @Component({ selector: 'lbd-chart', templateUrl: './lbd-chart.component.html', + styleUrls: ['./lbd-chart.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class LbdChartComponent implements OnInit, AfterViewInit { diff --git a/src/app/login/login.component.css b/src/app/login/login.component.css new file mode 100644 index 000000000..96cb2a90b --- /dev/null +++ b/src/app/login/login.component.css @@ -0,0 +1,106 @@ +/* login.component.css */ +.login-container { + height: 100vh; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + background-image: url('assets/img/background_login.jpg'); + background-size: cover; + background-position: center; + } + + .login-card { + width: 400px; + padding: 40px; + border-radius: 10px; + background-color: rgba(255, 255, 255, 0.95); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); + } + + .logo-container { + text-align: center; + margin-bottom: 30px; + } + + .logo { + width: 100px; + height: auto; + margin-bottom: 15px; + } + + h1 { + color: #d82c2c; + margin: 0; + font-size: 24px; + font-weight: 600; + } + + .login-form .form-group { + margin-bottom: 20px; + } + + .login-form label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #333; + } + + .login-form input[type="text"], + .login-form input[type="password"] { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 16px; + transition: border-color 0.3s; + } + + .login-form input:focus { + border-color: #d82c2c; + outline: none; + box-shadow: 0 0 0 2px rgba(216, 44, 44, 0.2); + } + + .remember-me { + display: flex; + align-items: center; + margin-bottom: 20px; + } + + .remember-me input { + margin-right: 8px; + } + + .login-button { + width: 100%; + padding: 14px; + border: none; + border-radius: 5px; + background-color: #d82c2c; + color: white; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.3s; + } + + .login-button:hover { + background-color: #b52222; + } + + .forgot-password { + margin-top: 15px; + text-align: center; + } + + .forgot-password a { + color: #d82c2c; + text-decoration: none; + font-size: 14px; + } + + .forgot-password a:hover { + text-decoration: underline; + } \ No newline at end of file diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html new file mode 100644 index 000000000..f045062ce --- /dev/null +++ b/src/app/login/login.component.html @@ -0,0 +1,44 @@ + + \ No newline at end of file diff --git a/src/app/login/login.component.spec.ts b/src/app/login/login.component.spec.ts new file mode 100644 index 000000000..10eca249d --- /dev/null +++ b/src/app/login/login.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts new file mode 100644 index 000000000..2a6860eab --- /dev/null +++ b/src/app/login/login.component.ts @@ -0,0 +1,56 @@ +// login.component.ts +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from 'app/services/auth.service'; +import Swal from 'sweetalert2'; +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] +}) +export class LoginComponent implements OnInit { + username: string = ''; + password: string = ''; + rememberMe: boolean = false; + + credentials = { + login: '', + motDePasse: '' + }; + + constructor(private authService: AuthService, private router: Router) {} + + ngOnInit(): void { + localStorage.clear() + } + onLogin() { + this.authService.login(this.credentials).subscribe({ + next: (response) => { + localStorage.setItem('token', response.token); + this.router.navigate(['/formation-liste']); + }, + error: (err) => { + if (err.status === 500 || err.status === 0) { + // Erreur serveur ou backend non joignable + Swal.fire({ + icon: 'error', + title: 'Erreur serveur', + text: 'Veuillez réessayer plus tard.', + confirmButtonColor: '#d82c2c', + confirmButtonText:"réessayer" + }); + } else { + // Erreur d’identifiants (ex : exception Runtime côté backend) + Swal.fire({ + icon: 'warning', + title: 'Identifiants invalides', + text: "Nom d'utilisateur ou mot de passe incorrect.", + confirmButtonColor: '#d82c2c', + confirmButtonText:"réessayer", + + + }); + } + }, + }); + }} \ No newline at end of file diff --git a/src/app/not-authorized/not-authorized.component.css b/src/app/not-authorized/not-authorized.component.css new file mode 100644 index 000000000..2ac549e9b --- /dev/null +++ b/src/app/not-authorized/not-authorized.component.css @@ -0,0 +1,128 @@ +/* not-authorized.component.css */ +.access-denied { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f8f9fa; + font-family: 'Roboto', Arial, sans-serif; + } + + .container { + max-width: 550px; + padding: 40px; + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + text-align: center; + animation: fadeIn 0.6s ease-in-out; + } + + .icon-container { + position: relative; + width: 80px; + height: 80px; + margin: 0 auto 20px; + background-color: #ff3e3e; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + } + + .lock-icon { + display: inline-block; + width: 40px; + height: 40px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 24 24'%3E%3Cpath d='M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + } + + .title { + color: #333333; + margin: 0 0 15px; + font-size: 28px; + font-weight: 600; + } + + .divider { + height: 3px; + width: 60px; + background-color: #ff3e3e; + margin: 0 auto 20px; + border-radius: 3px; + } + + .message { + color: #555555; + font-size: 18px; + margin-bottom: 10px; + line-height: 1.5; + } + + .sub-message { + color: #777777; + font-size: 16px; + margin-bottom: 30px; + } + + .btn-return { + display: inline-flex; + align-items: center; + background-color: #3e6fff; + color: white; + padding: 12px 24px; + border-radius: 30px; + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(62, 111, 255, 0.2); + } + + .btn-return:hover { + background-color: #2c5beb; + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(62, 111, 255, 0.3); + } + + .arrow-icon { + display: inline-block; + width: 20px; + height: 20px; + margin-right: 8px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 24 24'%3E%3Cpath d='M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* Média queries pour responsivité */ + @media screen and (max-width: 600px) { + .container { + max-width: 90%; + padding: 30px 20px; + } + + .title { + font-size: 24px; + } + + .message { + font-size: 16px; + } + + .sub-message { + font-size: 14px; + } + } \ No newline at end of file diff --git a/src/app/not-authorized/not-authorized.component.html b/src/app/not-authorized/not-authorized.component.html new file mode 100644 index 000000000..f650b9580 --- /dev/null +++ b/src/app/not-authorized/not-authorized.component.html @@ -0,0 +1,16 @@ + +
+
+
+ +
+

Accès Refusé

+
+

Vous n'avez pas les autorisations nécessaires pour accéder à cette page.

+

Veuillez contacter l'administrateur si vous pensez qu'il s'agit d'une erreur.

+ + + Retour au tableau de bord + +
+
\ No newline at end of file diff --git a/src/app/not-authorized/not-authorized.component.spec.ts b/src/app/not-authorized/not-authorized.component.spec.ts new file mode 100644 index 000000000..342cc227d --- /dev/null +++ b/src/app/not-authorized/not-authorized.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotAuthorizedComponent } from './not-authorized.component'; + +describe('NotAuthorizedComponent', () => { + let component: NotAuthorizedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NotAuthorizedComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NotAuthorizedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/not-authorized/not-authorized.component.ts b/src/app/not-authorized/not-authorized.component.ts new file mode 100644 index 000000000..7090efe36 --- /dev/null +++ b/src/app/not-authorized/not-authorized.component.ts @@ -0,0 +1,16 @@ +// not-authorized.component.ts +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-not-authorized', + templateUrl: './not-authorized.component.html', + styleUrls: ['./not-authorized.component.css'] +}) +export class NotAuthorizedComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + // Animation supplémentaire peut être ajoutée ici si nécessaire + } +} \ No newline at end of file diff --git a/src/app/participant-list/participant-list.component.css b/src/app/participant-list/participant-list.component.css new file mode 100644 index 000000000..35527918f --- /dev/null +++ b/src/app/participant-list/participant-list.component.css @@ -0,0 +1,56 @@ +/* Amélioration de l'apparence des boutons */ +.btn { + border-radius: 4px; + font-weight: 500; + padding: 0.375rem 0.75rem; + transition: all 0.2s ease-in-out; + margin-right: 8px; +} + +.btn-sm { + font-size: 1.3rem; + padding: 0.25rem 0.5rem; +} + +.btn-info { + box-shadow: 0 2px 4px rgba(23, 162, 184, 0.3); +} + +.btn-danger { + box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3); +} + +.btn-primary { + box-shadow: 0 2px 4px rgba(13, 110, 253, 0.3); +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn i { + margin-right: 5px; +} + +/* Amélioration de l'espacement dans le tableau */ +.table td, .table th { + padding: 12px 15px; + vertical-align: middle; +} + +/* Amélioration pour le bouton Ajouter */ +.header .btn-primary { + padding: 0.5rem 1rem; + display: flex; + align-items: center; +} + +.header .btn-primary i { + margin-right: 8px; +} + +.modal-body { + max-height: 70vh; + overflow-y: auto; +} \ No newline at end of file diff --git a/src/app/participant-list/participant-list.component.html b/src/app/participant-list/participant-list.component.html new file mode 100644 index 000000000..9005e8eff --- /dev/null +++ b/src/app/participant-list/participant-list.component.html @@ -0,0 +1,144 @@ +
+
+
+
+
+
+
+
+

Liste des participants

+

Gérer les participants

+
+ +
+
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + +
{{ cell }}Actions
{{ participant.id }}{{ participant.nom }}{{ participant.prenom }}{{ participant.structure?.libelle }}{{ participant.profile?.libelle }}{{ participant.email }}{{ participant.tel }} +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/src/app/participant-list/participant-list.component.spec.ts b/src/app/participant-list/participant-list.component.spec.ts new file mode 100644 index 000000000..e8050d745 --- /dev/null +++ b/src/app/participant-list/participant-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ParticipantListComponent } from './participant-list.component'; + +describe('ParticipantListComponent', () => { + let component: ParticipantListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ParticipantListComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ParticipantListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/participant-list/participant-list.component.ts b/src/app/participant-list/participant-list.component.ts new file mode 100644 index 000000000..ab6f57a7d --- /dev/null +++ b/src/app/participant-list/participant-list.component.ts @@ -0,0 +1,347 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ParticipantListService } from 'app/services/participant-list.service'; +import Swal from 'sweetalert2'; + +declare interface TableData { + headerRow: string[]; + dataRows: string[][]; +} + +@Component({ + selector: 'app-participant-list', + templateUrl: './participant-list.component.html', + styleUrls: ['./participant-list.component.css'] +}) +export class ParticipantListComponent implements OnInit { + // Pagination + public filteredParticipants: any[] = []; + public pageSize: number = 5; + public currentPage: number = 1; + // fin - Pagination + + public tableData1: TableData; + public userForm: FormGroup; + public isEditMode: boolean = false; + public selectedUserIndex: number = -1; + public selectedUser: string[] = null; + public participants : any; + public showUserModal: boolean = false; + public showDeleteModal: boolean = false; + structures: any[] = [ + { id: 1, name: 'Direction centrale' }, + { id: 2, name: 'Direction régionale' }, + ]; + profiles: any[] = [ + { id: 3, name: 'gestionnaire' }, + { id: 2, name: 'informaticien (bac + 3)' }, + { id: 1, name: 'informaticien (bac + 5)' }, + { id: 4, name: 'juriste' }, + { id: 5, name: 'technicien supérieur' }, + ]; + + + constructor(private formBuilder: FormBuilder,private participantService: ParticipantListService) {} + + + ngOnInit() { + // Pagination + const savedPage = localStorage.getItem('participantsListCurrentPage'); + this.currentPage = savedPage ? parseInt(savedPage) : 1; + // Fin - Pagination + let data: string[][] = []; + this.participantService.getAllParticipant().subscribe({ + next: (res) => { + console.log('Users fetched:', res); + this.participants = res ; + data = res.map((user: any) => [ + String(user.id), + String(user.nom), + String(user.prenom), + String(user.structure?.libelle ), + String(user.profile?.libelle ), + String(user.email), + String(user.tel) + ]); + this.tableData1 = { + headerRow: ['ID', 'Nom', 'Prénom', 'Structure', 'Profil', 'Email', 'Téléphone'], + dataRows: data + }; + + console.log("data = ",data); + }, + error: (err) => { + console.error('Error fetching users:', err); + }, + complete: () => { + console.log('User fetching completed.'); + } + }); + + this.tableData1 = { + headerRow: ['ID', 'Nom', 'Prénom', 'Structure', 'Profil', 'Email', 'Téléphone'], + dataRows: data + }; + + + + this.initForm(); + } + + ngOnDestroy() { + // Save current page when component is destroyed + localStorage.setItem('participantsListCurrentPage', this.currentPage.toString()); + } + + ngAfterViewInit(){ + this.loadParticipants(); + } + + initForm() { + this.userForm = this.formBuilder.group({ + id: ['', Validators.required], + nom: ['', Validators.required], + prenom: ['', Validators.required], + structure: ['', Validators.required], + profile: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + telephone: ['', [Validators.required, Validators.pattern('^[0-9]{8}$')]] + }); + } + + openAddModal() { + this.isEditMode = false; + this.selectedUserIndex = -1; + + const nextId = ( + Math.max(...this.tableData1.dataRows.map(row => parseInt(row[0]))) + 1 + ).toString(); + + this.userForm.reset(); + this.userForm.patchValue({ id: nextId }); + + this.showUserModal = true; + } + + openEditModal(index: number) { + this.isEditMode = true; + this.selectedUserIndex = index; + const userData = this.participants[index] + console.log("userData",userData); + this.userForm.patchValue({ + id: userData.id, + nom: userData.nom, + prenom: userData.prenom, + structure: userData.structure.id, + profile: userData.profile.id, + email: userData.email, + telephone: userData.tel + }); + + this.showUserModal = true; + } + + saveUser() { + if (this.userForm.invalid) { + return; + } + + const formValues = this.userForm.value; + + // Récupérer les IDs de profile et structure sélectionnés + const profileId = formValues.profile; // ID du profil sélectionné + const structureId = formValues.structure; // ID de la structure sélectionnée + + if (this.isEditMode) { + const newParticipant = { + nom: formValues.nom, + prenom: formValues.prenom, + email: formValues.email, + tel: formValues.telephone, + profileId : formValues.profile, + structureId : formValues.structure + }; + // Update existing user + this.participantService.updateParticipant(formValues.id, newParticipant).subscribe({ + next: (updatedUser) => { + // Notification de succès + Swal.fire({ + title: 'Succès!', + text: 'Le participant a été modifié avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + console.log('User updated:', updatedUser); + this.loadParticipants(); // Refresh the user list + console.log("this.participants(aprés modification):",this.participants); + this.showUserModal = false; + }, + error: (err) => { + console.error('Error updating user:', err); + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La modification a échoué pour les raisons suivantes :

+
    +
  • ${err.error?.message || 'Erreur serveur'}
  • + ${err.error?.errors?.map(e => `
  • ${e}
  • `).join('') || ''} +
+
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + }); + } else + // Envoie de la requête POST avec les IDs de profile et structure en paramètres + { + + // Créer l'objet participant sans les IDs de profile et structure + const newParticipant = { + nom: formValues.nom, + prenom: formValues.prenom, + email: formValues.email, + tel: formValues.telephone + }; + console.log(newParticipant) + // Récupérer les IDs de profile et structure sélectionnés + const profileId = formValues.profile; // ID du profil sélectionné + const structureId = formValues.structure; // ID de la structure sélectionnée + + + this.participantService.createParticipant(newParticipant, profileId, structureId) + .subscribe( + (response) => { + // Notification de succès + Swal.fire({ + title: 'Succès!', + text: 'Le particiant a été créé avec succès.', + icon: 'success', + confirmButtonText: 'OK', + confirmButtonColor: '#3085d6' + }); + console.log('Participant créé avec succès', response); + this.closeUserModal(); + this.userForm.reset(); + this.initForm; + // Actualiser la liste des participants + this.loadParticipants(); + }, + (err) => { + console.error('Erreur lors de la création du participant', err); + Swal.fire({ + title: 'Erreur!', + html: ` +
+

La création a échoué pour les raisons suivantes :

+
    +
  • ${err.error?.message || 'Erreur serveur'}
  • + ${err.error?.errors?.map(e => `
  • ${e}
  • `).join('') || ''} +
+
+ `, + icon: 'error', + confirmButtonText: 'Compris', + confirmButtonColor: '#d33' + }); + } + ); + }} + loadParticipants(): void { + this.participantService.getAllParticipant().subscribe( + (data) => { + this.participants = data; // Mettre à jour le tableau des participants + // Pagination + this.filteredParticipants = data; + // this.currentPage = 1; // Reset to first page + // Fin - Pagination + this.tableData1.dataRows = data.map((participant:any)=>[ + String(participant.id), + String(participant.nom), + String(participant.prenom), + String(participant.structure?.libelle ), + String(participant.profile?.libelle ), + String(participant.email), + String(participant.tel) + ]) + }, + (error) => { + console.error('Erreur lors du chargement des participants:', error); + } + ); + } + + // Pagination + + onSearchChange(searchTerm: string) { + this.filteredParticipants = this.participants.filter(participant => + participant.nom.toLowerCase().includes(searchTerm.toLowerCase()) || + participant.prenom.toLowerCase().includes(searchTerm.toLowerCase()) + ); + this.currentPage = 1; + } + + onPageChange(page: number) { + this.currentPage = page; + localStorage.setItem('participantsListCurrentPage', page.toString()); + + } + + paginatedUsers() { + const startIndex = (this.currentPage - 1) * this.pageSize; + return this.filteredParticipants.slice(startIndex, startIndex + this.pageSize); + } + // Fin - Pagination + + + deleteParticipant(index: number) { + this.selectedUserIndex = index; + console.log("selectedParticipantIndex!", this.selectedUserIndex); + this.selectedUser = this.tableData1.dataRows[index]; + console.log("selectedParticipant!", this.selectedUser); + this.showDeleteModal = true; + } + + confirmDelete() { + const participantId = this.selectedUser[0]; // ID du participant sélectionné + console.log('Deleting participant with ID:', participantId); + + // Effectuer la suppression + this.participantService.deleteParticipant(participantId).subscribe({ + next: () => { + Swal.fire({ + title: 'Succès !', + text: 'Participant supprimé avec succès.', + icon: 'success', + confirmButtonText: 'OK' + }); + this.loadParticipants(); // Recharger les participants + // Close modal + this.showDeleteModal = false; + this.selectedUserIndex = -1; + this.selectedUser = null; + }, + error: (err) => { + Swal.fire({ + title: 'Erreur !', + text: 'Suppression impossible : ' + (err.error?.message), + icon: 'error', + confirmButtonText: 'OK' + }); + console.error('Error deleting user:', err); + } + }); + } + + closeUserModal() { + this.showUserModal = false; + } + + closeDeleteModal() { + this.showDeleteModal = false; + } +} diff --git a/src/app/search-pagination/search-pagination.component.css b/src/app/search-pagination/search-pagination.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/search-pagination/search-pagination.component.html b/src/app/search-pagination/search-pagination.component.html new file mode 100644 index 000000000..9da4dab7a --- /dev/null +++ b/src/app/search-pagination/search-pagination.component.html @@ -0,0 +1,20 @@ +
+ +
+ + + \ No newline at end of file diff --git a/src/app/search-pagination/search-pagination.component.spec.ts b/src/app/search-pagination/search-pagination.component.spec.ts new file mode 100644 index 000000000..7d1ded857 --- /dev/null +++ b/src/app/search-pagination/search-pagination.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SearchPaginationComponent } from './search-pagination.component'; + +describe('SearchPaginationComponent', () => { + let component: SearchPaginationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SearchPaginationComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SearchPaginationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/search-pagination/search-pagination.component.ts b/src/app/search-pagination/search-pagination.component.ts new file mode 100644 index 000000000..de64b1b5e --- /dev/null +++ b/src/app/search-pagination/search-pagination.component.ts @@ -0,0 +1,32 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-search-pagination', + templateUrl: './search-pagination.component.html', + styleUrls: ['./search-pagination.component.css'] +}) +export class SearchPaginationComponent { + @Input() totalItems: number = 0; + @Input() pageSize: number = 5; + @Input() currentPage: number = 1; + @Output() pageChange = new EventEmitter(); + @Output() searchChange = new EventEmitter(); + + searchTerm: string = ''; + + onSearchChange() { + this.searchChange.emit(this.searchTerm); + } + + onPageChange(page: number) { + this.pageChange.emit(page); + } + + get totalPages(): number { + return Math.ceil(this.totalItems / this.pageSize); + } + + pages(): number[] { + return Array(this.totalPages).fill(0).map((x,i) => i+1); + } +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 000000000..9b9ae1478 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private apiUrl = 'http://localhost:8094/authenticate'; // URL du backend + + constructor(private http: HttpClient) {} + + login(credentials: { login: string, motDePasse: string }): Observable<{token: string}> { + return this.http.post<{token: string}>(this.apiUrl, credentials); + } + + private getAuthHeaders(): HttpHeaders { + const token = localStorage.getItem('token'); + return new HttpHeaders({ + 'Authorization': `Bearer ${token}` + }); + } + + getUserId(): Observable<{ userId: number }> { + return this.http.get<{ userId: number }>(`${this.apiUrl}/userId`, { + headers: this.getAuthHeaders() + }); + } + + getRoleId(): Observable<{ roleId: number }> { + return this.http.get<{ roleId: number }>(`${this.apiUrl}/roleId`, { + headers: this.getAuthHeaders() + }); + } + saveToken(token: string) { + localStorage.setItem('jwt', token); + } + + getToken(): string | null { + return localStorage.getItem('jwt'); + } + + isLoggedIn(): boolean { + return !!this.getToken(); + } + + logout() { + localStorage.removeItem('jwt'); + } +} diff --git a/src/app/services/certif.service.spec.ts b/src/app/services/certif.service.spec.ts new file mode 100644 index 000000000..609b3f405 --- /dev/null +++ b/src/app/services/certif.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CertifService } from './certif.service'; + +describe('CertifService', () => { + let service: CertifService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CertifService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/certif.service.ts b/src/app/services/certif.service.ts new file mode 100644 index 000000000..d6c675461 --- /dev/null +++ b/src/app/services/certif.service.ts @@ -0,0 +1,17 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CertifService { +private apiUrl = 'http://localhost:8094/api/certificates/generate'; + constructor(private http: HttpClient) {} + + generateCertificates(data: { certTitle: string; date: string; participants: any[] }){ + console.log(data) + + return this.http.post(this.apiUrl, data); + } + +} diff --git a/src/app/services/email.service.spec.ts b/src/app/services/email.service.spec.ts new file mode 100644 index 000000000..1d765b40f --- /dev/null +++ b/src/app/services/email.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EmailService } from './email.service'; + +describe('EmailService', () => { + let service: EmailService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EmailService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/email.service.ts b/src/app/services/email.service.ts new file mode 100644 index 000000000..684f56555 --- /dev/null +++ b/src/app/services/email.service.ts @@ -0,0 +1,18 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class EmailService { + + private apiUrl = 'http://localhost:8094/api/email/envoyer'; + + constructor(private http: HttpClient) { } + + // Fonction pour envoyer les e-mails + envoyerEmails(emailRequest: { objet: any, message: any, listeMails: any[] }) { + console.log(emailRequest) + return this.http.post(this.apiUrl, emailRequest); + } +} \ No newline at end of file diff --git a/src/app/services/employeur.service.spec.ts b/src/app/services/employeur.service.spec.ts new file mode 100644 index 000000000..cc8431328 --- /dev/null +++ b/src/app/services/employeur.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EmployeurService } from './employeur.service'; + +describe('EmployeurService', () => { + let service: EmployeurService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EmployeurService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/employeur.service.ts b/src/app/services/employeur.service.ts new file mode 100644 index 000000000..e6293054e --- /dev/null +++ b/src/app/services/employeur.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { catchError, Observable } from 'rxjs'; +import Swal from 'sweetalert2'; + +@Injectable({ + providedIn: 'root' +}) +export class EmployeurService { + private apiUrl = 'http://localhost:8094/employeurs'; // URL du backend pour les employeurs + + constructor(private http: HttpClient) { } + + // Méthode pour obtenir tous les employeurs + getEmployeurs(): Observable { + console.log(this.http.get(this.apiUrl)) + return this.http.get(this.apiUrl); + + } + + // Méthode pour créer un employeur + createEmployeur(employeur: any): Observable { + console.log(this.http.get(this.apiUrl)) + return this.http.post(this.apiUrl, employeur); + } + + // Méthode pour mettre à jour un employeur + updateEmployeur(employeurId: any, employeur: any): Observable { + console.log(this.http.get(this.apiUrl)) + return this.http.put(`${this.apiUrl}/${employeurId}`, employeur); + } + + // Méthode pour supprimer un employeur + deleteEmployeur(id: any): Observable { + return this.http.delete(`${this.apiUrl}/${id}`).pipe( + catchError((error) => { + // Si une erreur se produit lors de la requête HTTP, on la gère ici. + Swal.fire({ + confirmButtonColor: '#d82c2c', + icon: 'error', + title: 'Erreur', + text: error.error.message || 'Une erreur est survenue lors de la suppression.', + }); + throw error; // Lancer l'erreur à nouveau pour la gestion dans le composant si nécessaire + }) + ); + } + + + // Méthode pour obtenir un employeur par son ID + getEmployeurById(id: any): Observable { + return this.http.get(`${this.apiUrl}/${id}`); + } +} diff --git a/src/app/services/formateur-list.service.spec.ts b/src/app/services/formateur-list.service.spec.ts new file mode 100644 index 000000000..a4d15b3d2 --- /dev/null +++ b/src/app/services/formateur-list.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FormateurListService } from './formateur-list.service'; + +describe('FormateurListService', () => { + let service: FormateurListService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FormateurListService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/formateur-list.service.ts b/src/app/services/formateur-list.service.ts new file mode 100644 index 000000000..342f41f96 --- /dev/null +++ b/src/app/services/formateur-list.service.ts @@ -0,0 +1,33 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class FormateurListService { + private apiUrl = 'http://localhost:8094/formateurs'; // URL du backend pour les formateurs + + constructor(private http: HttpClient) { } + + getAllFormateurs(): Observable { + return this.http.get(this.apiUrl); + } + + createFormateur(formateur: any,employeurId: Number): Observable { + return this.http.post(`${this.apiUrl}?employeurId=${employeurId}`, formateur); } + + updateFormateur(formateurId: number, formateur: any): Observable { + return this.http.put(`${this.apiUrl}/${formateurId}`, formateur); + } + + deleteFormateur(formateurId: any): Observable { + return this.http.delete(`${this.apiUrl}/${formateurId}`); + } + getFormateursCount() { + return this.http.get(`${this.apiUrl}/count`); + } + getTopFormateurs() { + return this.http.get(`${this.apiUrl}/top-3-details`); + } +} diff --git a/src/app/services/formation-list.service.spec.ts b/src/app/services/formation-list.service.spec.ts new file mode 100644 index 000000000..454591644 --- /dev/null +++ b/src/app/services/formation-list.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FormationListService } from './formation-list.service'; + +describe('FormationListService', () => { + let service: FormationListService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FormationListService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/formation-list.service.ts b/src/app/services/formation-list.service.ts new file mode 100644 index 000000000..9a3737d63 --- /dev/null +++ b/src/app/services/formation-list.service.ts @@ -0,0 +1,66 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class FormationListService { + private apiUrl = 'http://localhost:8094/formations'; + + constructor(private http: HttpClient) { } + + getAllFormations(): Observable { + return this.http.get(this.apiUrl); + } + deleteFormation(formationId: any): Observable { + return this.http.delete(`${this.apiUrl}/${formationId}`); + } + + getFormationParticipants(formationId:any): Observable { + return this.http.get(`${this.apiUrl}/${formationId}/participants`); + } + + createFormation1(formation: any, formateurId: number, domaineId: number): Observable { + const params = new HttpParams() + .set('formateurId', formateurId.toString()) + .set('domaineId', domaineId.toString()); + + return this.http.post(`${this.apiUrl}`, formation, { params }); + } + + updateFormation(formationId:number,formation: any, formateurId: number, domaineId: number): Observable { + const params = new HttpParams() + .set('formateurId', formateurId.toString()) + .set('domaineId', domaineId.toString()); + + return this.http.put(`${this.apiUrl}/${formationId}`, formation, { params }); + } + + // EXECEPTION : getAllDomains + getAllDomaines(): Observable { + return this.http.get('http://localhost:8094/domaines'); + } + addParticipantToFormation(formationId: number, participantId: number): Observable { + return this.http.post( + `${this.apiUrl}/${formationId}/participants/${participantId}`, + {} + ); + } + removeParticipantFromFormation(formationId: number, participantId: number): Observable { + return this.http.delete( + `${this.apiUrl}/${formationId}/participants/${participantId}` + ); + } + + getFormationsCount() { + return this.http.get(`${this.apiUrl}/count`); + } + // Nouvelle méthode pour récupérer les budgets mensuels top 3 domaines + getBudgetsMensuelsTop3(){ + return this.http.get(`${this.apiUrl}/stats/budgets-mensuels-top3`); + } + getDomainePercentages() { + return this.http.get(`${this.apiUrl}/stats/domaines`); + } +} diff --git a/src/app/services/participant-list.service.spec.ts b/src/app/services/participant-list.service.spec.ts new file mode 100644 index 000000000..f5ba56654 --- /dev/null +++ b/src/app/services/participant-list.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ParticipantListService } from './participant-list.service'; + +describe('ParticipantListService', () => { + let service: ParticipantListService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ParticipantListService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/participant-list.service.ts b/src/app/services/participant-list.service.ts new file mode 100644 index 000000000..b73d8b3dc --- /dev/null +++ b/src/app/services/participant-list.service.ts @@ -0,0 +1,57 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; + +@Injectable({ + providedIn: 'root' +}) +export class ParticipantListService { + + private apiUrl = 'http://localhost:8094/participants'; + + constructor(private http: HttpClient) { } + + getAllParticipant() { + return this.http.get(this.apiUrl); + } + + deleteParticipant(participantId: any){ + return this.http.delete(`${this.apiUrl}/${participantId}`); + } + + createParticipant(participant: any, profileId: number, structureId: number){ + console.log(participant) + const body = { + ...participant + }; + return this.http.post(this.apiUrl, body, { + params: { + profileId: profileId.toString(), + structureId: structureId.toString() + } + }); + } + + updateParticipant(participantId: number, participant: any): Observable { + const participantToSend = { + nom: participant.nom, + prenom: participant.prenom, + email: participant.email, + tel: participant.tel, + profile: { id: participant.profileId }, + structure: { id: participant.structureId } + }; + + console.log('Participant envoyé :', participantToSend); + + return this.http.put(`${this.apiUrl}/${participantId}`, participantToSend); + } + + + getCount() { + return this.http.get(`${this.apiUrl}/count`); + } +} + + + diff --git a/src/app/services/user-list.service.spec.ts b/src/app/services/user-list.service.spec.ts new file mode 100644 index 000000000..a65e883e1 --- /dev/null +++ b/src/app/services/user-list.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserListService } from './user-list.service'; + +describe('UserListService', () => { + let service: UserListService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserListService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/user-list.service.ts b/src/app/services/user-list.service.ts new file mode 100644 index 000000000..efc4bc88c --- /dev/null +++ b/src/app/services/user-list.service.ts @@ -0,0 +1,33 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class UserListService { + private apiUrl = 'http://localhost:8094/utilisateurs'; // URL du backend + + constructor(private http: HttpClient) { } + + getAllUsers() { + return this.http.get(this.apiUrl); + } + + + createUser(user: any, roleId: any): Observable { + return this.http.post(`${this.apiUrl}?roleId=${roleId}`, user); + } + + updateUser(userId:any ,user: any, roleId: any): Observable { + return this.http.put(`${this.apiUrl}/${userId}?roleId=${roleId}`, user); + } + + deleteUser(userId: any): Observable { + return this.http.delete(`${this.apiUrl}/${userId}`); + + } + getUtilisateursCount() { + return this.http.get(`${this.apiUrl}/count`); + } +} diff --git a/src/app/shared/footer/footer.component.html b/src/app/shared/footer/footer.component.html index 12fe1788b..eacbd5bde 100644 --- a/src/app/shared/footer/footer.component.html +++ b/src/app/shared/footer/footer.component.html @@ -1,7 +1,7 @@
diff --git a/src/app/shared/navbar/navbar.component.html b/src/app/shared/navbar/navbar.component.html index 6a5aca258..d17f10e26 100644 --- a/src/app/shared/navbar/navbar.component.html +++ b/src/app/shared/navbar/navbar.component.html @@ -11,13 +11,13 @@