diff --git a/package.json b/package.json index c51c02de7..79f059e33 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,14 @@ "@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", "perfect-scrollbar": "1.5.0", "rxjs": "~7.5.0", + "sweetalert2": "^11.17.2", "tslib": "^2.3.0", "zone.js": "~0.11.4" }, @@ -43,22 +44,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..bb112b9e5 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,11 @@ 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'; @NgModule({ imports: [ @@ -22,13 +31,20 @@ import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.compon NavbarModule, FooterModule, SidebarModule, + ReactiveFormsModule, + AppRoutingModule ], declarations: [ AppComponent, - AdminLayoutComponent + AdminLayoutComponent, + LoginComponent, + UsersListComponent, + FormateurListComponent, + FormationListeComponent, + ParticipantListComponent ], - providers: [], + 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/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..8146ddf03 --- /dev/null +++ b/src/app/formateur-list/formateur-list.component.html @@ -0,0 +1,130 @@ +
+
+
+
+
+
+
+
+

Liste des formateurs

+

Gérer les formateurs

+
+ +
+
+
+ + + + + + + + + + + + + +
{{ cell }}Actions
{{cell}} +
+ + +
+
+
+
+
+
+
+
+ + + + + + + + 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..1b0891b4b --- /dev/null +++ b/src/app/formateur-list/formateur-list.component.ts @@ -0,0 +1,144 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +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 formateurForm: FormGroup; + public isEditMode: boolean = false; + public selectedFormateurIndex: number = -1; + public selectedFormateur: string[] = null; + + // Variables pour contrôler l'affichage des modals + public showFormateurModal: boolean = false; + public showDeleteModal: boolean = false; + employeurs: string[] = ['Entreprise A', 'Entreprise B', 'Entreprise C', 'Freelance']; + types: string[]=['interne','externe'] + constructor(private formBuilder: FormBuilder) { } + + ngOnInit() { + this.tableData1 = { + headerRow: ['ID', 'Nom', 'Prénom', 'Email', 'Tel', 'Type', 'Employeur'], + dataRows: [ + ['1', 'Dakota', 'Rice', 'dakota@gmail.com', '123456789', 'interne', 'Entreprise A'], + ['2', 'Minerva', 'Hooper', 'minerva@gmail.com', '987654321', 'externe', 'Entreprise A'], + // ... autres données + ] + }; + + this.initForm(); + } + + 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.tableData1.dataRows[index]; + + this.formateurForm.patchValue({ + id: formateurData[0], + nom: formateurData[1], + prenom: formateurData[2], + email: formateurData[3], + tel: formateurData[4], + type: formateurData[5], + employeur: formateurData[6] + }); + + this.showFormateurModal = true; + } + + saveFormateur() { + if (this.formateurForm.invalid) { + return; + } + + const formValues = this.formateurForm.value; + const formateurData = [ + formValues.id, + formValues.nom, + formValues.prenom, + formValues.email, + formValues.tel, + formValues.type, + formValues.employeur + ]; + + if (this.isEditMode) { + // Update existing formateur + this.tableData1.dataRows[this.selectedFormateurIndex] = formateurData; + } else { + // Add new formateur + this.tableData1.dataRows.push(formateurData); + } + + // Close modal + this.showFormateurModal = false; + this.formateurForm.reset(); + } + + deleteFormateur(index: number) { + this.selectedFormateurIndex = index; + this.selectedFormateur = this.tableData1.dataRows[index]; + this.showDeleteModal = true; + } + + confirmDelete() { + // Remove formateur from array + this.tableData1.dataRows.splice(this.selectedFormateurIndex, 1); + + // Close modal + this.showDeleteModal = false; + this.selectedFormateurIndex = -1; + this.selectedFormateur = null; + } + + 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..775afa3f9 --- /dev/null +++ b/src/app/formation-liste/formation-liste.component.html @@ -0,0 +1,266 @@ +
+
+
+
+
+
+
+
+

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..70af89bb7 --- /dev/null +++ b/src/app/formation-liste/formation-liste.component.ts @@ -0,0 +1,269 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-formation-liste', + templateUrl: './formation-liste.component.html', + styleUrls: ['./formation-liste.component.css'] +}) +export class FormationListeComponent implements OnInit { + // Variables pour la table + tableData = { + headerRow: ['ID', 'Titre', 'Date Début', 'Date Fin', 'Durée (jours)', 'Domaine', 'Formateur', 'Budget (€)', 'Participants'], + dataRows: [ + ['F001', 'Angular Avancé', '10/05/2025', '15/05/2025', '5', 'Développement Web', 'Dupont Jean', '3500', '15'], + ['F002', 'React Fondamentaux', '01/06/2025', '05/06/2025', '5', 'Développement Web', 'Martin Sophie', '3000', '12'], + ['F003', 'Management d\'équipe', '15/06/2025', '17/06/2025', '3', 'Management', 'Dubois Pierre', '2500', '8'], + ['F004', 'Excel Avancé', '20/06/2025', '21/06/2025', '2', 'Bureautique', 'Leroy Marie', '1200', '20'] + ] + }; + + // Variables pour les modals + showFormationModal = false; + showDeleteModal = false; + showEmailModal = false; + showCertificatModal = false; + isEditMode = false; + selectedFormation: any = null; + selectedIndex: number = -1; + + // Listes pour les select + domaines = ['Développement Web', 'Management', 'Bureautique', 'Communication', 'Marketing Digital', 'Data Science']; + formateurs = ['Dupont Jean', 'Martin Sophie', 'Dubois Pierre', 'Leroy Marie', 'Garcia Thomas']; + participants = [ + { id: 'P001', nom: 'miladi', prenom: 'imen', email: 'miladiphg@gmail.com' }, + { id: 'P002', nom: 'Moreau', prenom: 'Thomas', email: 'thomas.moreau@example.com' }, + { id: 'P003', nom: 'Petit', prenom: 'Sophie', email: 'sophie.petit@example.com' }, + { id: 'P004', nom: 'Bernard', prenom: 'Luc', email: 'luc.bernard@example.com' }, + { id: 'P005', nom: 'Durant', prenom: 'Emma', email: 'emma.durant@example.com' } + ]; + + // Form Group + formationForm: FormGroup; + emailForm: FormGroup; + certificatForm: FormGroup; + + // Liste des participants sélectionnés pour la formation en cours d'édition + selectedParticipants: any[] = []; + + constructor(private fb: FormBuilder) { + 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 + } + + // Méthodes pour la gestion des formations + openAddModal() { + this.isEditMode = false; + this.formationForm.reset(); + this.generateNewId(); + this.selectedParticipants = []; + this.showFormationModal = true; + + } + + openEditModal(index: number) { + this.isEditMode = true; + this.selectedIndex = index; + const formation = this.tableData.dataRows[index]; + + // Simulation des participants pour l'édition + this.selectedParticipants = this.participants.slice(0, parseInt(formation[8])); + + this.formationForm.patchValue({ + id: formation[0], + titre: formation[1], + dateDebut: this.formatDateForInput(formation[2]), + dateFin: this.formatDateForInput(formation[3]), + duree: formation[4], + domaine: formation[5], + formateur: formation[6], + budget: formation[7], + participants: this.selectedParticipants.map(p => p.id) + }); + + this.showFormationModal = true; + } + + closeFormationModal() { + this.showFormationModal = false; + this.formationForm.reset(); + } + + saveFormation() { + const formValues = this.formationForm.value; + const formationData = [ + formValues.id, + formValues.titre, + this.formatDateForDisplay(formValues.dateDebut), + this.formatDateForDisplay(formValues.dateFin), + formValues.duree.toString(), + formValues.domaine, + formValues.formateur, + formValues.budget.toString(), + this.selectedParticipants.length.toString() + ]; + + if (this.isEditMode) { + this.tableData.dataRows[this.selectedIndex] = formationData; + } else { + this.tableData.dataRows.push(formationData); + } + + this.closeFormationModal(); + } + + deleteFormation(index: number) { + this.selectedIndex = index; + this.selectedFormation = this.tableData.dataRows[index]; + this.showDeleteModal = true; + } + + closeDeleteModal() { + this.showDeleteModal = false; + this.selectedFormation = null; + } + + confirmDelete() { + this.tableData.dataRows.splice(this.selectedIndex, 1); + this.closeDeleteModal(); + } + + // Méthodes pour l'envoi d'email + openEmailModal(index: number) { + this.selectedIndex = index; + this.selectedFormation = this.tableData.dataRows[index]; + + // Simulation des destinataires pour l'email + const nbParticipants = parseInt(this.selectedFormation[8]); + const destinataires = this.participants.slice(0, nbParticipants); + + 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: destinataires.map(p => p.id) + }); + + this.showEmailModal = true; + } + + closeEmailModal() { + this.showEmailModal = false; + } + + sendEmail() { + // Simulation d'envoi d'email + console.log('Email envoyé !', this.emailForm.value); + alert('Les emails ont été envoyés avec succès !'); + this.closeEmailModal(); + } + + // Méthodes pour la génération de certificats + openCertificatModal(index: number) { + this.selectedIndex = index; + this.selectedFormation = this.tableData.dataRows[index]; + + // Simulation des participants pour les certificats + const nbParticipants = parseInt(this.selectedFormation[8]); + const participantsForCert = this.participants.slice(0, nbParticipants); + + this.certificatForm.patchValue({ + titre: `Certificat de réussite - ${this.selectedFormation[1]}`, + participants: participantsForCert.map(p => p.id) + }); + + this.showCertificatModal = true; + } + + closeCertificatModal() { + this.showCertificatModal = false; + } + + generateCertificats() { + // Simulation de génération de certificats + console.log('Certificats générés !', this.certificatForm.value); + alert('Les certificats ont été générés avec succès !'); + this.closeCertificatModal(); + } + + // 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..a5fbc9200 --- /dev/null +++ b/src/app/guards/auth.guard.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree ,Router } from '@angular/router'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + constructor(private router: Router) {} + canActivate(): boolean { + if (localStorage.getItem('token')) { + return true; + } + this.router.navigate(['/login']); + return false; + } + +} 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..09818768b 100644 --- a/src/app/layouts/admin-layout/admin-layout.routing.ts +++ b/src/app/layouts/admin-layout/admin-layout.routing.ts @@ -2,20 +2,33 @@ import { Routes } from '@angular/router'; import { HomeComponent } from '../../home/home.component'; import { UserComponent } from '../../user/user.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'; 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]}, + { path: 'user', component: UserComponent ,canActivate: [AuthGuard]}, + + { path: 'user-list', component: UsersListComponent ,canActivate: [AuthGuard]}, + { path: 'participant-list', component: ParticipantListComponent ,canActivate: [AuthGuard]}, + { 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]}, + {path: 'formation-liste', component:FormationListeComponent ,canActivate: [AuthGuard]}, + { path: 'participant-list', component: ParticipantListComponent ,canActivate: [AuthGuard]}, + ]; 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..ea6f0796e --- /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..9d79d272d --- /dev/null +++ b/src/app/login/login.component.ts @@ -0,0 +1,53 @@ +// login.component.ts +import { Component } 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 { + username: string = ''; + password: string = ''; + rememberMe: boolean = false; + + credentials = { + login: '', + motDePasse: '' + }; + + constructor(private authService: AuthService, private router: Router) {} + + onLogin() { + this.authService.login(this.credentials).subscribe({ + next: (response) => { + localStorage.setItem('token', response.token); + this.router.navigate(['/dashboard']); + }, + 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/participant-list/participant-list.component.css b/src/app/participant-list/participant-list.component.css new file mode 100644 index 000000000..892a9b472 --- /dev/null +++ b/src/app/participant-list/participant-list.component.css @@ -0,0 +1,51 @@ +/* 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; +} \ 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..92e006c7b --- /dev/null +++ b/src/app/participant-list/participant-list.component.html @@ -0,0 +1,138 @@ +
+
+
+
+
+
+
+
+

Liste des participants

+

Gérer les participants

+
+ +
+
+
+ + + + + + + + + + + + + +
{{ cell }}Actions
{{cell}} +
+ + +
+
+
+
+
+
+
+
+ + + + + + + + \ 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..571ab3126 --- /dev/null +++ b/src/app/participant-list/participant-list.component.ts @@ -0,0 +1,133 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +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 { + public tableData1: TableData; + public userForm: FormGroup; + public isEditMode: boolean = false; + public selectedUserIndex: number = -1; + public selectedUser: string[] = null; + + // Variables pour contrôler l'affichage des modals + public showUserModal: boolean = false; + public showDeleteModal: boolean = false; + + constructor(private formBuilder: FormBuilder) { } + + ngOnInit() { + this.tableData1 = { + headerRow: ['ID', 'Name', 'Country', 'City', 'Salary'], + dataRows: [ + ['1', 'Dakota Rice', 'Niger', 'Oud-Turnhout', '$36,738'], + ['2', 'Minerva Hooper', 'Curaçao', 'Sinaai-Waas', '$23,789'], + // ... autres données + ] + }; + + this.initForm(); + } + initForm() { + this.userForm = this.formBuilder.group({ + id: ['', Validators.required], + name: ['', Validators.required], + country: ['', Validators.required], + city: ['', Validators.required], + salary: ['', Validators.required] + }); + } + openAddModal() { + this.isEditMode = false; + this.selectedUserIndex = -1; + + // Generate a new ID + const nextId = (Math.max(...this.tableData1.dataRows.map(row => parseInt(row[0]))) + 1).toString(); + + this.userForm.reset(); + this.userForm.patchValue({ + id: nextId, + name: '', + country: '', + city: '', + salary: '' + }); + + this.showUserModal = true; + } + + openEditModal(index: number) { + this.isEditMode = true; + this.selectedUserIndex = index; + const userData = this.tableData1.dataRows[index]; + + this.userForm.patchValue({ + id: userData[0], + name: userData[1], + country: userData[2], + city: userData[3], + salary: userData[4] + }); + + this.showUserModal = true; + } + + saveUser() { + if (this.userForm.invalid) { + return; + } + + const formValues = this.userForm.value; + const userData = [ + formValues.id, + formValues.name, + formValues.country, + formValues.city, + formValues.salary + ]; + + if (this.isEditMode) { + // Update existing user + this.tableData1.dataRows[this.selectedUserIndex] = userData; + } else { + // Add new user + this.tableData1.dataRows.push(userData); + } + + // Close modal + this.showUserModal = false; + this.userForm.reset(); + } + + deleteUser(index: number) { + this.selectedUserIndex = index; + this.selectedUser = this.tableData1.dataRows[index]; + this.showDeleteModal = true; + } + + confirmDelete() { + // Remove user from array + this.tableData1.dataRows.splice(this.selectedUserIndex, 1); + + // Close modal + this.showDeleteModal = false; + this.selectedUserIndex = -1; + this.selectedUser = null; + } + + closeUserModal() { + this.showUserModal = false; + } + + closeDeleteModal() { + this.showDeleteModal = false; + } +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 000000000..d10f2d7e3 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } 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); + } + + 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/shared/footer/footer.component.html b/src/app/shared/footer/footer.component.html index 12fe1788b..c17954ad9 100644 --- a/src/app/shared/footer/footer.component.html +++ b/src/app/shared/footer/footer.component.html @@ -25,7 +25,7 @@ diff --git a/src/app/shared/navbar/navbar.component.html b/src/app/shared/navbar/navbar.component.html index 6a5aca258..f9630c9ef 100644 --- a/src/app/shared/navbar/navbar.component.html +++ b/src/app/shared/navbar/navbar.component.html @@ -67,7 +67,7 @@
  • - + Log out
  • diff --git a/src/app/shared/navbar/navbar.component.ts b/src/app/shared/navbar/navbar.component.ts index 068ed7ee8..4fa508b22 100644 --- a/src/app/shared/navbar/navbar.component.ts +++ b/src/app/shared/navbar/navbar.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, ElementRef } from '@angular/core'; import { ROUTES } from '../../sidebar/sidebar.component'; import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common'; +import { Router } from '@angular/router'; @Component({ // moduleId: module.id, @@ -14,7 +15,7 @@ export class NavbarComponent implements OnInit{ private toggleButton: any; private sidebarVisible: boolean; - constructor(location: Location, private element: ElementRef) { + constructor(location: Location, private element: ElementRef , private router: Router) { this.location = location; this.sidebarVisible = false; } @@ -63,4 +64,9 @@ export class NavbarComponent implements OnInit{ } return 'Dashboard'; } + + logout() { + localStorage.removeItem('token'); // Supprime le token + this.router.navigate(['/login']); // Redirige vers la page de connexion + } } diff --git a/src/app/sidebar/sidebar.component.html b/src/app/sidebar/sidebar.component.html index 817b194c6..dfe3fd145 100644 --- a/src/app/sidebar/sidebar.component.html +++ b/src/app/sidebar/sidebar.component.html @@ -1,11 +1,11 @@