+
+
+
+
+
+
+
+
+
+
+
+
{{participantsCount}}
+
Participants
+
+
+
+
+
+
+
{{formateursCount}}
+
Formateurs
+
+
+
+
+
+
+
{{formationsCount}}
+
Formations
+
+
+
+
+
+
+
{{utilisateursCount}}
+
Utilisateurs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [footerIconClass]="'fa fa-check'"
+ [footerText]="'Data information certified'">
+
-
+
+
+
+
+
+
+
+
+
+
+
+ # |
+ Nom |
+ Prénom |
+ Email |
+ Téléphone |
+ Spécialité |
+ Employeur |
+ Nb 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 @@
+
+
+
+
+

+
Welcome to WeLearn
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ cell }} |
+ Actions |
+
+
+
+
+ {{ participant.id }} |
+ {{ participant.nom }} |
+ {{ participant.prenom }} |
+ {{ participant.structure?.libelle }} |
+ {{ participant.profile?.libelle }} |
+ {{ participant.email }} |
+ {{ participant.tel }} |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Êtes-vous sûr de vouloir supprimer cet participant?
+
+ Nom: {{ selectedUser[1] }}
+ ID: {{ selectedUser[0] }}
+
+
+
+
+
+
+
\ 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 @@
- -
+
-
-
+
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..28f50ff37 100644
--- a/src/app/sidebar/sidebar.component.html
+++ b/src/app/sidebar/sidebar.component.html
@@ -1,15 +1,15 @@