diff --git a/src/app/models/structure.model.ts b/src/app/models/structure.model.ts index 344e442ea40ef2f2c895ea841c480a9954c9d73d..8841a553a56777a2a35b326d6421bb678f40cbc8 100644 --- a/src/app/models/structure.model.ts +++ b/src/app/models/structure.model.ts @@ -12,6 +12,7 @@ export class Structure { public numero: string = null; public createdAt: string = null; public updatedAt: string = null; + public toBeDeletedAt: string = null; public structureName: string = null; public structureType: string = null; public description: string = null; diff --git a/src/app/profile/profile-routing.module.ts b/src/app/profile/profile-routing.module.ts index 0143e14e2c58be6d1d7ccedcb92774800ff74659..2ddd5a0c625b8678862632bfa81f7bebd37bb6b8 100644 --- a/src/app/profile/profile-routing.module.ts +++ b/src/app/profile/profile-routing.module.ts @@ -4,6 +4,7 @@ import { RoleGuard } from '../guards/role.guard'; import { StructureResolver } from '../resolvers/structure.resolver'; import { RouteRole } from '../shared/enum/routeRole.enum'; import { StructureMembersManagementComponent } from './structure-members-management/structure-members-management.component'; +import { StructuresManagementComponent } from './structures-management/structures-management.component'; import { ProfileComponent } from './profile.component'; import { StructureEditionSummaryComponent } from './structure-edition-summary/structure-edition-summary.component'; import { AuthGuard } from '../guards/auth.guard'; @@ -22,6 +23,10 @@ const routes: Routes = [ canActivate: [AuthGuard], component: EditComponent, }, + { + path: 'structures-management', + component: StructuresManagementComponent, + }, footerOutletRoute, { path: '', diff --git a/src/app/profile/profile.component.html b/src/app/profile/profile.component.html index a5a2cfcc48e16fd06e0681610e07d46459776c1f..6918307a61478c37a029ec2f1e73c70fba59ef66 100644 --- a/src/app/profile/profile.component.html +++ b/src/app/profile/profile.component.html @@ -71,24 +71,23 @@ <div fxLayout="row" fxLayoutAlign="start center" fxFill> <h1>Structures</h1> <app-button - *ngIf="!isPublic" + *ngIf="!isPublic && userProfile.structuresLink.length > 0" class="hide-on-mobile" [type]="'button'" [iconBtn]="'edit'" [text]="'Gérer mes structures'" [style]="buttonTypeEnum.SecondaryWide" - routerLink="./" + routerLink="./structures-management" [routerLinkActive]="'active'" - [disabled]="true" ></app-button> <app-button + *ngIf="!isPublic && userProfile.structuresLink.length > 0" class="hide-on-desktop" [type]="'button'" [iconBtn]="'edit'" [style]="buttonTypeEnum.SecondaryOnlyIcon" - routerLink="/" + routerLink="./structures-management" [routerLinkActive]="'active'" - [disabled]="true" ></app-button> </div> <div fxLayoutGap="16px" fxLayout="column" fxFill> @@ -102,6 +101,16 @@ </div> </ng-container> </div> + <div fxLayoutAlign="center center" fxFill> + <app-button + *ngIf="!isPublic && userProfile.structuresLink.length == 0" + [style]="buttonTypeEnum.SecondaryUltraWide" + [iconBtn]="'add'" + [text]="'Ajouter une structure'" + routerLink="/form/structure" + tabindex="0" + ></app-button> + </div> </div> </section> diff --git a/src/app/profile/profile.component.ts b/src/app/profile/profile.component.ts index d977fbecdd4bd3f6e1f41cf44b30de1d48ad39f1..7749a50f89bf4ed77e28af79ab035cfd37a5c126 100644 --- a/src/app/profile/profile.component.ts +++ b/src/app/profile/profile.component.ts @@ -7,14 +7,14 @@ import { StructureService } from '../services/structure.service'; import { ButtonType } from '../shared/components/button/buttonType.enum'; import { ProfileService } from './services/profile.service'; import { Utils } from '../utils/utils'; -import { catchError, map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { forkJoin, Observable } from 'rxjs'; import { Location } from '@angular/common'; @Component({ selector: 'app-profile', templateUrl: './profile.component.html', - styleUrls: ['./profile.component.scss'] + styleUrls: ['./profile.component.scss'], }) export class ProfileComponent implements OnInit { public userProfile: User; @@ -61,11 +61,20 @@ export class ProfileComponent implements OnInit { } private getStructuresFromProfile() { + const structures$: Observable<any>[] = []; this.structures = []; + this.userProfile.structuresLink.forEach((structureId) => { - this.structureService.getStructureWithOwners(structureId, null).subscribe((s) => { - this.structures.push(s); - }); + structures$.push( + this.structureService.getStructureWithOwners(structureId, null).pipe( + tap((structure) => { + this.structures.push(structure); + }) + ) + ); + }); + forkJoin(structures$).subscribe(() => { + this.structures.sort((a, b) => a.structure.structureName.localeCompare(b.structure.structureName)); }); } diff --git a/src/app/profile/profile.module.ts b/src/app/profile/profile.module.ts index 632d3356d5c3fc28c1f16ce796ad4ae05c643dcd..a81d170f96d829b077869be5db766f80f3d1c227 100644 --- a/src/app/profile/profile.module.ts +++ b/src/app/profile/profile.module.ts @@ -9,6 +9,7 @@ import { StructureEditionSummaryComponent } from './structure-edition-summary/st import { EditComponent } from './edit/edit.component'; import { StructureMembersManagementComponent } from './structure-members-management/structure-members-management.component'; import { StructureAddMemberModalComponent } from './structure-add-member-modal/structure-add-member-modal.component'; +import { StructuresManagementComponent } from './structures-management/structures-management.component'; import { MissingInformationComponent } from './structure-edition-summary/missing-information/missing-information.component'; import { NoInformationComponent } from './structure-edition-summary/no-information/no-information.component'; @@ -23,6 +24,7 @@ import { NoInformationComponent } from './structure-edition-summary/no-informati StructureAddMemberModalComponent, StructureEditionSummaryComponent, StructureMembersManagementComponent, + StructuresManagementComponent, ], imports: [CommonModule, ProfileRoutingModule, SharedModule], }) diff --git a/src/app/profile/structure-members-management/structure-members-management.component.html b/src/app/profile/structure-members-management/structure-members-management.component.html index 6d2f1c3da4c555c72cea800d2cf733e2494d2aaa..8d393238fe9bc97047791250b63e9bb4e556371d 100644 --- a/src/app/profile/structure-members-management/structure-members-management.component.html +++ b/src/app/profile/structure-members-management/structure-members-management.component.html @@ -78,17 +78,19 @@ [structure]="structureWithOwners" (closed)="closeAddMemberModal($event)" ></app-structure-add-member-modal> -<app-modal-confirmation +<app-custom-modal *ngIf="excludeModalOpenned" [openned]="excludeModalOpenned" [content]="'Souhaitez-vous exclure ce membre\n(' + displayMemberName(memberToExclude) + ') ?'" - [customConfirmationText]="'Oui'" + [hideTitle]="true" + [customValidationButton]="'Oui'" (closed)="excludeMember(memberToExclude, $event)" -></app-modal-confirmation> -<app-modal-confirmation +></app-custom-modal> +<app-custom-modal *ngIf="cancelAddTempUserModalOpenned" [openned]="cancelAddTempUserModalOpenned" [content]="'Souhaitez-vous annuler la demande de rattachement de ce membre\n(' + tempUserToCancel.email + ') ?'" - [customConfirmationText]="'Oui'" + [hideTitle]="true" + [customValidationButton]="'Oui'" (closed)="cancelAddTempUser(tempUserToCancel, $event)" -></app-modal-confirmation> +></app-custom-modal> diff --git a/src/app/profile/structures-management/structures-management.component.html b/src/app/profile/structures-management/structures-management.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5b343bc1bc8cb7e286b576b6e44397c9a52327e9 --- /dev/null +++ b/src/app/profile/structures-management/structures-management.component.html @@ -0,0 +1,89 @@ +<div class="content-container full-screen"> + <div class="container"> + <div class="header"> + <div fxLayout="row wrap" fxLayoutAlign="space-between center" fxFill> + <div fxLayout="row" fxLayoutAlign="start center" class="headerBack"> + <app-svg-icon [iconClass]="'backArrow'" [type]="'ico'" [icon]="'arrowBack'" (click)="goBack()"></app-svg-icon> + <h1>Gérer mes structures</h1> + </div> + <app-button + [style]="buttonTypeEnum.SecondaryWide" + [iconBtn]="'add'" + [text]="'Ajouter une structure'" + routerLink="/form/structure" + tabindex="0" + ></app-button> + </div> + </div> + <div *ngIf="structures"> + <div fxLayoutGap="16px" fxLayout="column" fxFill> + <ng-container *ngIf="structures"> + <div *ngFor="let elt of structures" class="structureCard"> + <div class="structureCardContent" fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="16px"> + <div class="structureDetails" fxLayout="column" fxLayoutAlign="space-between start"> + <p class="structureName">{{ elt.structure.structureName }}</p> + <p class="structureLocation">{{ elt.structure.address.commune }}</p> + </div> + <div class="warningContainer"> + <div *ngIf="isBeingDeleted(elt.structure)" class="deleteInProgress"> + <app-svg-icon [iconClass]="'icon-26'" [type]="'form'" [icon]="'notValidate'"></app-svg-icon> + <span>Suppression en cours</span> + </div> + </div> + <div class="buttons" fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="12px"> + <app-button + [type]="'button'" + [text]="'Quitter la structure'" + [style]="buttonTypeEnum.SecondaryWide" + (click)="selectedStructure = elt.structure; leaveModalOpenned = true" + ></app-button> + <app-button + *ngIf="!isBeingDeleted(elt.structure)" + class="deleteAction" + [type]="'button'" + [text]="'Supprimer la structure'" + [style]="buttonTypeEnum.SecondaryWide" + routerLink="./" + [routerLinkActive]="'active'" + (click)="selectedStructure = elt.structure; deleteModalOpenned = true" + ></app-button> + + <app-button + *ngIf="isBeingDeleted(elt.structure)" + class="deleteAction" + [type]="'button'" + [text]="'Annuler la suppression'" + [style]="buttonTypeEnum.SecondaryWide" + routerLink="./" + [routerLinkActive]="'active'" + (click)="selectedStructure = elt.structure; cancelDeleteModalOpenned = true" + ></app-button> + </div> + </div> + </div> + </ng-container> + </div> + </div> + <app-custom-modal + *ngIf="leaveModalOpenned" + [openned]="leaveModalOpenned" + [content]="'Souhaitez-vous quitter cette structure ?'" + [hideTitle]="true" + (closed)="leaveStructure(selectedStructure, $event)" + ></app-custom-modal> + <app-custom-modal + *ngIf="deleteModalOpenned" + [openned]="deleteModalOpenned" + [content]="'Souhaitez-vous supprimer cette structure ?'" + [hideTitle]="true" + (closed)="deleteStructure(selectedStructure, $event)" + ></app-custom-modal> + <app-custom-modal + *ngIf="cancelDeleteModalOpenned" + [openned]="cancelDeleteModalOpenned" + [content]="'Souhaitez-vous annuler la suppression de cette structure ?'" + [hideTitle]="true" + (closed)="cancelDelete(selectedStructure, $event)" + ></app-custom-modal> + </div> +</div> diff --git a/src/app/profile/structures-management/structures-management.component.scss b/src/app/profile/structures-management/structures-management.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..7796be699d37df2b36cd7d48de1e01faba50e33a --- /dev/null +++ b/src/app/profile/structures-management/structures-management.component.scss @@ -0,0 +1,78 @@ +@import '../../../assets/scss/color'; +@import '../../../assets/scss/typography'; +@import '../../../assets/scss/breakpoint'; + +.container { + margin: 1rem auto; + max-width: 980px; + padding: 2rem; + background: $white; + .header { + margin-bottom: 2rem; + } + .headerBack { + cursor: pointer; + span { + color: $red; + } + } + h1 { + @include lato-regular-24; + color: $grey-1; + cursor: initial; + } + + .structureCard { + padding: 5px 6px; + border-bottom: 1px solid $grey-8; + overflow: hidden; + .structureCardContent { + @media #{$phone} { + flex-wrap: wrap; + } + .structureDetails { + width: 45%; + margin-bottom: 8px; + @media #{$phone} { + width: initial; + } + p { + margin: 0 !important; + &.structureName { + @include lato-bold-16; + } + &.structureLocation { + @include lato-regular-13; + font-style: italic; + color: $grey-3; + } + } + } + .warningContainer { + width: 8rem; + .deleteInProgress { + display: flex; + color: $orange-warning; + padding-bottom: 5px; + span { + max-width: 6.875rem; + } + ::ng-deep svg { + margin-right: 8px; + } + } + } + .buttons { + @media #{$tablet} { + max-width: 45%; + } + @media #{$phone} { + max-width: initial; + } + app-button.deleteAction > ::ng-deep button > div { + color: $red; + } + } + } + } +} diff --git a/src/app/profile/structures-management/structures-management.component.spec.ts b/src/app/profile/structures-management/structures-management.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d8850d4e508961943b595a2873669a807f5e02f --- /dev/null +++ b/src/app/profile/structures-management/structures-management.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StructuresManagementComponent } from './structures-management.component'; + +describe('StructuresManagementComponent', () => { + let component: StructuresManagementComponent; + let fixture: ComponentFixture<StructuresManagementComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StructuresManagementComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StructuresManagementComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/profile/structures-management/structures-management.component.ts b/src/app/profile/structures-management/structures-management.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a409d2c4625edce0b770a2ffc425ef726beffb92 --- /dev/null +++ b/src/app/profile/structures-management/structures-management.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit } from '@angular/core'; +import { forkJoin, Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { Structure } from '../../models/structure.model'; +import { StructureWithOwners } from '../../models/structureWithOwners.model'; +import { User } from '../../models/user.model'; +import { NotificationService } from '../../services/notification.service'; +import { StructureService } from '../../services/structure.service'; +import { ButtonType } from '../../shared/components/button/buttonType.enum'; +import { ProfileService } from '../services/profile.service'; + +@Component({ + selector: 'app-structures-management', + templateUrl: './structures-management.component.html', + styleUrls: ['./structures-management.component.scss'], +}) +export class StructuresManagementComponent implements OnInit { + public userProfile: User; + public structures: StructureWithOwners[] = []; + public selectedStructure: string; + public deleteInProgress: boolean; + public buttonTypeEnum = ButtonType; + public leaveModalOpenned: boolean = false; + public deleteModalOpenned: boolean = false; + public cancelDeleteModalOpenned: boolean = false; + + constructor( + private profileService: ProfileService, + private structureService: StructureService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.getUserAndStructures(); + } + + public goBack(): void { + history.back(); + } + + private getStructures(): void { + const structures$: Observable<any>[] = []; + this.structures = []; + + this.userProfile.structuresLink.forEach((structureId) => { + structures$.push( + this.structureService.getStructureWithOwners(structureId, null).pipe( + tap((structure) => { + this.structures.push(structure); + }) + ) + ); + }); + forkJoin(structures$).subscribe(() => { + this.structures.sort((a, b) => a.structure.structureName.localeCompare(b.structure.structureName)); + }); + } + + private getUserAndStructures(): void { + this.profileService.getProfile().then((profile: User) => { + this.userProfile = profile; + this.getStructures(); + }); + } + + public leaveStructure(structure: Structure, shouldExclude: boolean): void { + this.leaveModalOpenned = false; + if (shouldExclude) { + this.structureService.removeOwnerFromStructure(this.userProfile._id, structure._id).subscribe( + () => { + this.structures = this.structures.filter((obj) => obj.structure._id !== structure._id); + this.notificationService.showSuccess(`Vous avez bien quitté ${structure.structureName}`, ''); + }, + () => { + this.notificationService.showError('Une erreur est survenue, veuillez réessayer.', ''); + } + ); + } + } + + public deleteStructure(structure: Structure, shouldDelete: boolean): void { + this.deleteModalOpenned = false; + if (shouldDelete) { + this.structureService.delete(structure._id).subscribe((res) => { + this.getUserAndStructures(); + }); + } + } + + public isBeingDeleted(structure: Structure): boolean { + return Boolean(structure.toBeDeletedAt); + } + + public cancelDelete(structure: Structure, shouldCancel: boolean): void { + this.cancelDeleteModalOpenned = false; + if (shouldCancel) { + this.structureService.cancelDelete(structure._id).subscribe((res) => { + this.getStructures(); + }); + } + } +} diff --git a/src/app/services/structure.service.ts b/src/app/services/structure.service.ts index 662d69955b2277f25d0e6802ab5915929fff72d0..698064f0d7386eb0e246166f80d51ac387ac0649 100644 --- a/src/app/services/structure.service.ts +++ b/src/app/services/structure.service.ts @@ -64,6 +64,10 @@ export class StructureService { return this.http.delete<Structure>(`${this.baseUrl}/${id}`); } + public cancelDelete(id: string): Observable<Structure> { + return this.http.post<Structure>(`${this.baseUrl}/${id}/cancelDelete`, null); + } + public removeTempUserFromStructure(idOwner: string, idStructure: string): Observable<any> { return this.http.delete<any>(`${this.baseUrl}/${idStructure}/tempUser/${idOwner}`); } diff --git a/src/app/shared/components/custom-modal/custom-modal.component.html b/src/app/shared/components/custom-modal/custom-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..34f4b756c202311033c5aba4a49e0de72f4c5d3d --- /dev/null +++ b/src/app/shared/components/custom-modal/custom-modal.component.html @@ -0,0 +1,28 @@ +<div *ngIf="openned" class="modalBackground"> + <div class="modal"> + <div class="contentModal" fxLayout="column" fxLayoutAlign="space-around center"> + <div class="headerModal"> + <svg aria-hidden="true" (click)="closeModal(false)"> + <use [attr.xlink:href]="'assets/form/sprite.svg#close'"></use> + </svg> + <div class="contentText"> + <h3 *ngIf="!hideTitle && customTitle">{{ customTitle }}</h3> + <h3 *ngIf="!hideTitle && !customTitle">ATTENTION</h3> + <p [ngClass]="{ mainText: hideTitle }">{{ content }}</p> + </div> + </div> + <div class="footerModal" fxLayout="row" fxLayoutAlign="space-around center" fxLayoutGap="8px"> + <app-button + (action)="closeModal(false)" + [text]="customCancelButton ? customCancelButton : 'Annuler'" + [style]="buttonTypeEnum.modalSecondary" + ></app-button> + <app-button + (action)="closeModal(true)" + [text]="customValidationButton ? customValidationButton : 'Valider'" + [style]="buttonTypeEnum.modalPrimary" + ></app-button> + </div> + </div> + </div> +</div> diff --git a/src/app/shared/components/custom-modal/custom-modal.component.scss b/src/app/shared/components/custom-modal/custom-modal.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..6c682cc2ca925b6d91c6b1fb49a499dc5dae9e89 --- /dev/null +++ b/src/app/shared/components/custom-modal/custom-modal.component.scss @@ -0,0 +1,46 @@ +@import '../../../../assets/scss/color'; +@import '../../../../assets/scss/typography'; +@import '../../../../assets/scss/shapes'; +@import '../../../../assets/scss/z-index'; + +.modalBackground { + .modal { + max-width: 390px; + .contentModal { + padding: 15px 5px 15px 45px; + .headerModal { + display: flex; + flex-direction: row-reverse; + svg { + cursor: pointer; + height: 40px; + width: 40px; + } + .contentText { + flex-direction: column; + max-width: 300px; + h3 { + @include lato-bold-18; + color: $red; + margin: 10px 0 25px; + text-align: center; + } + p { + text-align: center; + margin: 10px; + } + p.mainText { + @include lato-regular-18; + } + } + } + .footerModal { + padding-right: 40px; + gap: 8px; + app-button { + flex: 1; + } + } + } + } +} diff --git a/src/app/shared/components/custom-modal/custom-modal.component.spec.ts b/src/app/shared/components/custom-modal/custom-modal.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..899fc30d386a87619c482fd3dc297d92f84de811 --- /dev/null +++ b/src/app/shared/components/custom-modal/custom-modal.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomModalComponent } from './custom-modal.component'; + +describe('CustomModalComponent', () => { + let component: CustomModalComponent; + let fixture: ComponentFixture<CustomModalComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CustomModalComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CustomModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/custom-modal/custom-modal.component.ts b/src/app/shared/components/custom-modal/custom-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5401d3bcc47542f52ee2be986c2ad503ad2c6eb --- /dev/null +++ b/src/app/shared/components/custom-modal/custom-modal.component.ts @@ -0,0 +1,22 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ButtonType } from '../button/buttonType.enum'; + +@Component({ + selector: 'app-custom-modal', + templateUrl: './custom-modal.component.html', + styleUrls: ['./custom-modal.component.scss'], +}) +export class CustomModalComponent { + @Input() public openned: boolean; + @Input() public content: string; + @Input() public hideTitle?: boolean; + @Input() public customTitle?: string; + @Input() public customValidationButton?: string; + @Input() public customCancelButton?: string; + @Output() closed = new EventEmitter<boolean>(); + public buttonTypeEnum = ButtonType; + + public closeModal(value: boolean): void { + this.closed.emit(value); + } +} diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index b8449db73ec60f3a1a5985da8ee7824e5dc2be29..1c8245ed8058bf4f90711a1d0c728993ad91191b 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -10,6 +10,7 @@ import { HourPickerComponent } from './hour-picker/hour-picker.component'; import { CopyPasteComponent } from './hour-picker/copy-paste/copy-paste.component'; import { RadioFormComponent } from './radio-form/radio-form.component'; import { ModalConfirmationComponent } from './modal-confirmation/modal-confirmation.component'; +import { CustomModalComponent } from './custom-modal/custom-modal.component'; import { ModalJoinConfirmationComponent } from './modal-join-confirmation/modal-join-confirmation.component'; import { StructureOptionsModalComponent } from './structure-options-modal/structure-options-modal.component'; import { ModalOptionsComponent } from './modal-options/modal-options.component'; @@ -32,6 +33,7 @@ export { CopyPasteComponent, RadioFormComponent, ModalConfirmationComponent, + CustomModalComponent, StructureOptionsModalComponent, ModalOptionsComponent, TextInputModalComponent, @@ -53,6 +55,7 @@ export const SharedComponents = [ CopyPasteComponent, RadioFormComponent, ModalConfirmationComponent, + CustomModalComponent, ModalJoinConfirmationComponent, StructureOptionsModalComponent, ModalOptionsComponent,