diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 050a9decc0c0bba110e7d340b21f17ea37360dde..81a4a46a9494b6ca71b13b2c9cd92172a3e1972b 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -23,6 +23,7 @@ import { JobRendererComponent } from './components/manage-users/job-renderer/job import { ManageUsersComponent } from './components/manage-users/manage-users.component'; import { NavBarComponent } from './components/nav-bar/nav-bar.component'; import { AdminStructuresListComponent } from './components/structures-list/admin-structures-list.component'; +import { ButtonCellRendererComponent } from './components/button-cell-renderer/button-cell-renderer.component'; @NgModule({ declarations: [ @@ -46,6 +47,7 @@ import { AdminStructuresListComponent } from './components/structures-list/admin DeletedStructuresComponent, NavBarComponent, EspaceCoopCNFSComponent, + ButtonCellRendererComponent, ], imports: [CommonModule, AdminRoutingModule, SharedModule, AgGridModule], }) diff --git a/src/app/admin/admin.scss b/src/app/admin/admin.scss index 2dc3064b906cb0bf418fe137d1c5834844d9a923..7e52d1c767865fed4c62a0fd4a48bafcad9af6f7 100644 --- a/src/app/admin/admin.scss +++ b/src/app/admin/admin.scss @@ -1,26 +1,9 @@ @import 'color'; -.header { - padding-top: 32px; - display: flex; - flex-direction: column; - gap: 16px; - align-items: center; -} - -nav { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 1rem; - justify-content: center; - align-items: center; -} - .adminLayout { display: flex; flex-direction: column; - align-items: center; + align-items: start; gap: 1rem; margin: auto; padding-bottom: 1rem; @@ -57,3 +40,12 @@ h3.inline { ::ng-deep .ag-row-group-leaf-indent { margin-left: 0 !important; } + +::ng-deep .filter-option div { + min-height: 40px !important; + max-height: 40px !important; + justify-content: center; + border-radius: 20px !important; + font-size: 0.813rem !important; + box-shadow: none !important; +} diff --git a/src/app/admin/components/button-cell-renderer/button-cell-renderer.component.ts b/src/app/admin/components/button-cell-renderer/button-cell-renderer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..83818ad2172362ec60bca571dba62f0ce4de040a --- /dev/null +++ b/src/app/admin/components/button-cell-renderer/button-cell-renderer.component.ts @@ -0,0 +1,31 @@ +// button-cell-renderer.component.ts +import { Component } from '@angular/core'; +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +@Component({ + selector: 'app-button-cell-renderer', + template: `<app-button + size="small" + [disabled]="disabled" + [label]="'Relancer'" + [variant]="'secondary'" + [style]="'display: inline-block'" + />`, +}) +export class ButtonCellRendererComponent implements ICellRendererAngularComp { + public disabled = true; + + agInit(params: ICellRendererParams): void { + this.disabled = this.shouldDisable(params); + } + + refresh(params: ICellRendererParams): boolean { + this.disabled = this.shouldDisable(params); + return true; + } + + public shouldDisable(params: ICellRendererParams): boolean { + return !params.data.isOutdated || params.context.gridId === 'toClaim'; + } +} diff --git a/src/app/admin/components/nav-bar/nav-bar.component.html b/src/app/admin/components/nav-bar/nav-bar.component.html index d03c00d6c7d0465d9cbc9c3517b85bcaf583f278..569fc8cbeded90785057150fa77ff9c00a86a444 100644 --- a/src/app/admin/components/nav-bar/nav-bar.component.html +++ b/src/app/admin/components/nav-bar/nav-bar.component.html @@ -1,37 +1,23 @@ <div class="header"> - <h1>Administration</h1> - <nav> + <div class="title-and-ghost"> + <h1>Administration</h1> <app-button - [label]="'Revendication structure'" [variant]="'secondary'" - (action)="router.navigateByUrl(routes.pendingStructures.link)" + [label]="'Ghost'" + [type]="'button'" + (action)="openGhost()" /> - <app-button - [label]="'Liste structures'" - [variant]="'secondary'" - (action)="router.navigateByUrl(routes.structuresList.link)" - /> - <app-button - [label]="'Structures supprimées'" - [variant]="'secondary'" - (action)="router.navigateByUrl(routes.deletedStructures.link)" - /> - <app-button - [label]="'Gestion des utilisateurs'" - [variant]="'secondary'" - (action)="router.navigateByUrl(routes.manageUsers.link)" - /> - <app-button [label]="'Fonctions'" [variant]="'secondary'" (click)="router.navigateByUrl(routes.jobsList.link)" /> - <app-button - [label]="'Employeurs'" - [variant]="'secondary'" - (action)="router.navigateByUrl(routes.employersList.link)" - /> - <app-button - [label]="'CNFS Espace Coop'" - [variant]="'secondary'" - (action)="router.navigateByUrl(routes.espaceCoopCNFS.link)" - /> - <app-button [variant]="'tertiary'" [label]="'Ghost'" (action)="openGhost()" /> + </div> + <nav> + <ng-container *ngFor="let button of buttons"> + <div [class]="isActive(button.route)"> + <app-button + [variant]="'tertiary'" + [label]="button.label" + [type]="'button'" + (action)="navigateTo(button.route)" + /> + </div> + </ng-container> </nav> </div> diff --git a/src/app/admin/components/nav-bar/nav-bar.component.scss b/src/app/admin/components/nav-bar/nav-bar.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..57d5fe7d51930f883730890b23a961ab735fef01 --- /dev/null +++ b/src/app/admin/components/nav-bar/nav-bar.component.scss @@ -0,0 +1,40 @@ +.header { + display: flex; + flex-direction: column; + align-items: center; + + .title-and-ghost { + margin-top: 2rem; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 80%; + height: 2rem; + margin-bottom: 1.5rem; + } + + h1 { + justify-content: start; + font-weight: 400; + } + + nav { + margin-top: 0.5rem; + display: flex; + justify-content: space-between; + border-bottom: 1px solid #ccc; + margin-bottom: 1.5rem; + width: 80%; + overflow-x: auto; + } + + app-button { + height: 3.75rem; + position: relative; + } + + .active { + border-bottom: 3px solid black; + padding-bottom: 0.5rem; + } +} diff --git a/src/app/admin/components/nav-bar/nav-bar.component.ts b/src/app/admin/components/nav-bar/nav-bar.component.ts index a0f710da390d4698e3a18a9337f065448481cf1d..42e33463f6f0fbacc3c640b149d774b477fc650c 100644 --- a/src/app/admin/components/nav-bar/nav-bar.component.ts +++ b/src/app/admin/components/nav-bar/nav-bar.component.ts @@ -1,16 +1,28 @@ import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { Router, NavigationEnd } from '@angular/router'; import { ConfigService } from '../../../services/config.service'; import { AdminRoutes } from '../../admin-routing.module'; +import { filter } from 'rxjs/operators'; @Component({ selector: 'app-admin-nav-bar', templateUrl: './nav-bar.component.html', - styleUrl: '../../admin.scss', + styleUrls: ['./nav-bar.component.scss'], }) export class NavBarComponent implements OnInit { public routes = AdminRoutes; private ghostLink: string; + public currentRoute: string; + + public buttons = [ + { label: 'Revendication structure', route: this.routes.pendingStructures.link }, + { label: 'Liste structures', route: this.routes.structuresList.link }, + { label: 'Structures supprimées', route: this.routes.deletedStructures.link }, + { label: 'Gestion des utilisateurs', route: this.routes.manageUsers.link }, + { label: 'Fonctions', route: this.routes.jobsList.link }, + { label: 'Employeurs', route: this.routes.employersList.link }, + { label: 'CNFS Espace Coop', route: this.routes.espaceCoopCNFS.link }, + ]; constructor( public router: Router, @@ -18,12 +30,27 @@ export class NavBarComponent implements OnInit { ) {} ngOnInit(): void { + // Fetch configuration and set ghost link this.configService.getConfig().then((config) => { this.ghostLink = config.ghostAdminUrl; }); + + this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe((event: NavigationEnd) => { + this.currentRoute = event.urlAfterRedirects || event.url; + }); + + this.currentRoute = this.router.url.substring(1); } public openGhost(): void { window.open(this.ghostLink); } + + public navigateTo(route: string): void { + this.router.navigateByUrl(route); + } + + public isActive(route: string): string { + return this.currentRoute === route ? 'active' : ''; + } } diff --git a/src/app/admin/components/structures-list/admin-structures-list.component.html b/src/app/admin/components/structures-list/admin-structures-list.component.html index 6a241bbcfb51e0f75f7d7dbae24bbdfc02e3ba3c..2dfd726377a81846a7b0aeab782f025297b4b968 100644 --- a/src/app/admin/components/structures-list/admin-structures-list.component.html +++ b/src/app/admin/components/structures-list/admin-structures-list.component.html @@ -1,55 +1,37 @@ <app-admin-nav-bar /> + +<div class="filters-containers"> + <div *ngFor="let filterOption of filterOptions"> + <div class="filter"> + <app-radio-option + class="filter-option" + [id]="filterOption.value" + [label]="filterOption.label" + [value]="filterOption.value" + [selected]="filter === filterOption.value" + (selectedEvent)="setFilter($event.value)" + /> + </div> + </div> +</div> + <div *ngIf="isLoading" class="loader" aria-busy="true"> - <img class="loader-gif" src="/assets/gif/loader_circle.gif" alt /> + <img class="loader-gif" src="/assets/gif/loader_circle.gif" alt="" /> </div> <div *ngIf="!isLoading" class="adminLayout"> - <h2>Liste structures</h2> - <h3>Structures avec des données manquantes ({{ structuresIncomplete ? structuresIncomplete.length : 0 }})</h3> - <ag-grid-angular - *ngIf="structuresIncomplete.length" - class="ag-theme-alpine" - domLayout="autoHeight" - style="width: 100%" - [rowData]="structuresIncomplete" - [columnDefs]="columnDefs" - [rowHeight]="rowHeight" - [ngClass]="'red'" - /> - <div *ngIf="!structuresIncomplete?.length">Aucune structure</div> - - <h3>Structures en cours de revendication ({{ structuresInClaim.length }})</h3> - <ag-grid-angular - *ngIf="structuresInClaim.length" - class="ag-theme-alpine" - domLayout="autoHeight" - style="width: 100%" - [rowData]="structuresInClaim" - [columnDefs]="columnDefs" - [rowHeight]="rowHeight" - /> - <div *ngIf="!structuresInClaim?.length">Aucune structure</div> - - <h3>Structures à revendiquer ({{ structuresToClaim.length }})</h3> - <ag-grid-angular - *ngIf="structuresToClaim.length" - class="ag-theme-alpine" - domLayout="autoHeight" - style="width: 100%" - [rowData]="structuresToClaim" - [columnDefs]="columnDefs" - [rowHeight]="rowHeight" - /> - <div *ngIf="!structuresToClaim?.length">Aucune structure</div> - - <h3>Structures revendiquées ({{ structuresClaimed.length }})</h3> - <ag-grid-angular - *ngIf="structuresClaimed.length" - class="ag-theme-alpine" - domLayout="autoHeight" - style="width: 100%" - [rowData]="structuresClaimed" - [columnDefs]="columnDefs" - [rowHeight]="rowHeight" - /> - <div *ngIf="!structuresClaimed?.length">Aucune structure</div> + <div *ngFor="let section of filteredSections" class="section"> + <h3>{{ section.title }} ({{ section.data?.length || 0 }})</h3> + <ag-grid-angular + *ngIf="section.data?.length" + class="ag-theme-alpine" + domLayout="autoHeight" + style="width: 100%" + [gridOptions]="getGridContext(section.filter)" + [rowData]="section.data" + [columnDefs]="columnDefs" + [rowHeight]="rowHeight" + [ngClass]="section.cssClass" + /> + <div *ngIf="!section.data?.length" class="no-structures">Aucun renseignement</div> + </div> </div> diff --git a/src/app/admin/components/structures-list/admin-structures-list.component.scss b/src/app/admin/components/structures-list/admin-structures-list.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..a2fb1edba9065b0c9f255472d5e08b4aa5eeef8b --- /dev/null +++ b/src/app/admin/components/structures-list/admin-structures-list.component.scss @@ -0,0 +1,46 @@ +.section { + width: 100%; + margin-top: 0.5rem; + margin-bottom: 0; + text-decoration: none !important; + + h3 { + margin-bottom: 1rem; + line-height: 110%; + } + + .no-structures { + color: var(--grey-451-text, var(--grey-451-text, #767676)); + font-size: 14px; + font-style: italic; + font-weight: 400; + line-height: 120%; + } +} + +.filters-containers { + width: 80%; + display: flex; + margin-left: 10%; + flex-direction: row; + align-items: center; + margin-bottom: 1rem; + gap: 0.5rem; +} + +::ng-deep .ag-header-cell-text { + font-size: 18px; + font-weight: 700; +} + +/*to avoid having a 150px row if there is less than 3 rows*/ +::ng-deep .ag-theme-alpine .ag-layout-auto-height .ag-center-cols-clipper, +.ag-theme-alpine .ag-layout-auto-height .ag-center-cols-container, +.ag-theme-alpine .ag-layout-print .ag-center-cols-clipper, +.ag-theme-alpine .ag-layout-print .ag-center-cols-container, +.ag-theme-alpine-dark .ag-layout-auto-height .ag-center-cols-clipper, +.ag-theme-alpine-dark .ag-layout-auto-height .ag-center-cols-container, +.ag-theme-alpine-dark .ag-layout-print .ag-center-cols-clipper, +.ag-theme-alpine-dark .ag-layout-print .ag-center-cols-container { + min-height: 0; +} diff --git a/src/app/admin/components/structures-list/admin-structures-list.component.ts b/src/app/admin/components/structures-list/admin-structures-list.component.ts index 3342277bf451b8266a96f729746a621abff88996..ba29dab274670686b252bd322467797808308983 100644 --- a/src/app/admin/components/structures-list/admin-structures-list.component.ts +++ b/src/app/admin/components/structures-list/admin-structures-list.component.ts @@ -1,35 +1,47 @@ import { DatePipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; -import { ColDef } from 'ag-grid-community'; +import { ColDef, ICellRendererParams } from 'ag-grid-community'; import { finalize } from 'rxjs/operators'; +import { NotificationService } from '../../../services/notification.service'; import { AdminStructure } from '../../models/adminStructureList.interface'; import { AdminService } from '../../services/admin.service'; +import { ButtonCellRendererComponent } from '../button-cell-renderer/button-cell-renderer.component'; @Component({ selector: 'app-admin-structures-list', templateUrl: './admin-structures-list.component.html', - styleUrls: ['../../admin.scss'], + styleUrls: ['../../admin.scss', 'admin-structures-list.component.scss'], }) export class AdminStructuresListComponent implements OnInit { - constructor( - private adminService: AdminService, - private datePipe: DatePipe, - ) {} public structuresClaimed: AdminStructure[]; public structuresInClaim: AdminStructure[]; public structuresToClaim: AdminStructure[]; public structuresIncomplete: AdminStructure[]; - public isAll = false; public isLoading = true; - public rowHeight = 25; + public rowHeight = 48; + public filter = 'all'; + + constructor( + private adminService: AdminService, + private datePipe: DatePipe, + private notificationService: NotificationService, + ) {} + + public filterOptions = [ + { label: 'Toutes les structures', value: 'all' }, + { label: 'Données manquantes', value: 'incomplete' }, + { label: 'En cours de revendication', value: 'inClaim' }, + { label: 'À revendiquer', value: 'toClaim' }, + ]; public columnDefs: ColDef<AdminStructure>[] = [ { headerName: 'Structure', cellRenderer: (params): string => this.renderLink(params.data), comparator(_, __, nodeA, nodeB): number { - return nodeA.data.structureName.toLowerCase() > nodeB.data.structureName.toLocaleLowerCase() ? -1 : 1; + return nodeA.data.structureName.toLowerCase() > nodeB.data.structureName.toLowerCase() ? -1 : 1; }, + cellClass: 'cell-link', sortable: true, flex: 3, }, @@ -37,14 +49,30 @@ export class AdminStructuresListComponent implements OnInit { headerName: 'Date de mise à jour', field: 'updatedAt', valueFormatter: ({ value }): string => this.datePipe.transform(value, 'mediumDate'), - cellClass: (params): string => { - return params.data.isOutdated ? 'red' : ''; - }, + cellClass: (params): string => (params.data.isOutdated ? 'red' : ''), + flex: 1, + sortable: true, + }, + { + headerName: 'Dernière relance par mail', + field: 'lastUpdateMail', + valueFormatter: ({ value }): string => this.datePipe.transform(value, 'mediumDate'), flex: 1, sortable: true, }, + { + headerName: 'Relancer', + cellRenderer: ButtonCellRendererComponent, + cellRendererParams: { label: 'Relancer' }, + flex: 1, + sortable: false, + onCellClicked: this.handleRelance.bind(this), + }, ]; + public gridSections = []; + public filteredSections = []; + ngOnInit(): void { this.adminService .getAllStructureAdmin() @@ -54,10 +82,69 @@ export class AdminStructuresListComponent implements OnInit { this.structuresInClaim = response.inClaim; this.structuresToClaim = response.toClaim; this.structuresIncomplete = response.incomplete; + + this.gridSections = [ + { + filter: 'incomplete', + title: 'Structures avec des données manquantes', + data: this.structuresIncomplete, + cssClass: 'red', + }, + { + filter: 'inClaim', + title: 'Structures en cours de revendication', + data: this.structuresInClaim, + cssClass: '', + }, + { + filter: 'toClaim', + title: 'Structures à revendiquer', + data: this.structuresToClaim, + cssClass: '', + }, + { + filter: 'claimed', + title: 'Structures revendiquées', + data: this.structuresClaimed, + cssClass: '', + }, + ]; + + this.updateFilteredSections(); }); } + setFilter(filter: string | boolean): void { + this.filter = filter as string; + this.updateFilteredSections(); + } + + updateFilteredSections(): void { + if (this.filter === 'all') { + this.filteredSections = this.gridSections; + } else { + this.filteredSections = this.gridSections.filter((section) => section.filter === this.filter); + } + } + + public getGridContext(filterName: string): { context: { gridId: string } } { + return { context: { gridId: filterName } }; + } + private renderLink(structure: AdminStructure): string { return `<a href="/acteurs?structure=${structure.permalink}" target="_blank">${structure.structureName}</a>`; } + + private handleRelance(params: ICellRendererParams): void { + this.adminService.updateLastUpdateMail(params.data.structureId).subscribe({ + next: () => { + params.api.getRowNode(String(params.rowIndex)).setDataValue('lastUpdateMail', new Date()); + this.notificationService.showSuccess('Relance effectuée'); + }, + error: (error) => { + console.error('Error updating last update mail:', error); + this.notificationService.showError('Erreur: ' + (error.message || 'unknown error')); + }, + }); + } } diff --git a/src/app/admin/models/adminStructureList.interface.ts b/src/app/admin/models/adminStructureList.interface.ts index 8229ee48918e561ea36251988093b56b9db28b64..e4f3d219bf22811d4a93c39fa3be84c4a44af001 100644 --- a/src/app/admin/models/adminStructureList.interface.ts +++ b/src/app/admin/models/adminStructureList.interface.ts @@ -9,6 +9,7 @@ export interface AdminStructure { structureId: string; structureName: string; updatedAt: Date; + lastUpdateMail: Date; isOutdated: boolean; permalink: string; } diff --git a/src/app/admin/services/admin.service.ts b/src/app/admin/services/admin.service.ts index c99547c6fd469f329349b9ca899324773d43088e..b6b629753d3ff936eabbbfd55c407fb49b2b6d95 100644 --- a/src/app/admin/services/admin.service.ts +++ b/src/app/admin/services/admin.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { Employer } from '../../models/employer.model'; import { EspaceCoopCNFS } from '../../models/espaceCoopCNFS.model'; import { Job } from '../../models/job.model'; @@ -217,4 +218,14 @@ export class AdminService { public getAllResinCNFS(): Observable<User[]> { return this.http.get<User[]>(`${this.baseUrl}/resinCNFS`); } + + public updateLastUpdateMail(structureId: string): Observable<{ message: string }> { + return this.http.post<{ message: string }>(`${this.baseUrl}/updateLastUpdateMail/${structureId}`, {}).pipe( + catchError((error) => { + console.error('Error updating last update mail:', error); + const errorMessage = error.error.message || 'Erreur lors de la relance'; + return throwError(() => new Error(errorMessage)); + }), + ); + } } diff --git a/src/app/models/structure.model.ts b/src/app/models/structure.model.ts index 5008e15809a72e2101ff5491ee70141778b7ce69..99415a64ecc0991270e52a668d467a7a8185eb47 100644 --- a/src/app/models/structure.model.ts +++ b/src/app/models/structure.model.ts @@ -58,6 +58,7 @@ export class Structure { public hasNoUserDN?: boolean = null; public hasUserWithAppointmentDN?: boolean = null; public permalink = ''; + public lastUpdateMail: Date = null; constructor(obj?: any) { Object.assign(this, obj, {