diff --git a/src/app/form/footer-form/footer-form.component.ts b/src/app/form/footer-form/footer-form.component.ts index 67b905386c29a549ab75c477ea8119148dc1158a..b83c8bdf013037e103a142e1a5c7725f11dc48c1 100644 --- a/src/app/form/footer-form/footer-form.component.ts +++ b/src/app/form/footer-form/footer-form.component.ts @@ -17,13 +17,12 @@ import { stepType } from '../step.type'; @Component({ selector: 'app-footer-form', templateUrl: './footer-form.component.html', - styleUrls: ['./footer-form.component.scss'] + styleUrls: ['./footer-form.component.scss'], }) export class FooterFormComponent implements OnChanges { @Input() currentForm: formType; @Input() isValid: boolean; @Input() isClaimMode: boolean; - @Input() isAccountMode: boolean; @Input() btnName: string[]; @Input() nbPagesForm: number; @Input() form: UntypedFormGroup; @@ -74,7 +73,8 @@ export class FooterFormComponent implements OnChanges { } if ( this.currentForm === formType.structure && - (this.currentStep === structureFormStep.noStructure || this.currentStep === structureFormStep.StructureInfoUnknown) + (this.currentStep === structureFormStep.noStructure || + this.currentStep === structureFormStep.StructureInfoUnknown) ) { this.isLastFormStep = true; } @@ -134,8 +134,11 @@ export class FooterFormComponent implements OnChanges { const user = new User(this.form.value); // Create user with structure user.structuresLink = this.linkedStructureId; - this.authService.register(user).subscribe(() => {}); - this.newsletterService.newsletterSubscribe(user.email).subscribe(() => {}); + this.authService.register(user).subscribe(() => { + if (this.acceptNewsletter) { + this.newsletterService.newsletterSubscribe(user.email).subscribe(); + } + }); document.getElementsByClassName('page')[0].scrollTo(0, 0); } if (this.isProfileLastPage()) { @@ -195,7 +198,9 @@ export class FooterFormComponent implements OnChanges { private isStructureChoiceValid(): boolean { return ( - this.currentForm === formType.structure && this.currentStep === structureFormStep.structureChoice && this.form.value._id + this.currentForm === formType.structure && + this.currentStep === structureFormStep.structureChoice && + this.form.value._id ); } @@ -215,7 +220,8 @@ export class FooterFormComponent implements OnChanges { private isPersonalOfferpage(): boolean { return ( - this.currentForm === formType.personaloffer && this.currentStep === personalOfferFormStep.personalOfferStructureChoice + this.currentForm === formType.personaloffer && + this.currentStep === personalOfferFormStep.personalOfferStructureChoice ); } public isPersonalOfferFirstPage(): boolean { diff --git a/src/app/form/form-view/account-form/account-credentials/account-credentials.component.scss b/src/app/form/form-view/account-form/account-credentials/account-credentials.component.scss index 6399786d796a2c9c31e1c3b3b4dc1a77e223eeff..26d92dee3593f78938933097080a4e91ff45c85d 100644 --- a/src/app/form/form-view/account-form/account-credentials/account-credentials.component.scss +++ b/src/app/form/form-view/account-form/account-credentials/account-credentials.component.scss @@ -1,6 +1,11 @@ @import '../../../../../assets/scss/color'; @import '../../../../../assets/scss/typography'; +.disabled { + opacity: 0.4; + cursor: not-allowed; +} + p.special { @include lato-regular-14; color: $grey-3; diff --git a/src/app/form/form-view/account-form/account-form.component.html b/src/app/form/form-view/account-form/account-form.component.html index eda10b761bf96597b90edcdd8e96d1f6e48e2bc1..deda50a65e9c87670b4e09c7dd53b52b84657bc3 100644 --- a/src/app/form/form-view/account-form/account-form.component.html +++ b/src/app/form/form-view/account-form/account-form.component.html @@ -11,6 +11,7 @@ <app-account-credentials [accountForm]="accountForm" [profile]="profile" + [isAccountMode]="isAccountMode" (validateForm)="setValidationsForm($event)" (userExists)="verifyUserExist($event)" ></app-account-credentials> diff --git a/src/app/form/form-view/account-form/account-form.component.ts b/src/app/form/form-view/account-form/account-form.component.ts index 9711d32af680cf983afd3db8f365b420cb288cbd..4235272090631a3336282f25dd97f9976bd78086 100644 --- a/src/app/form/form-view/account-form/account-form.component.ts +++ b/src/app/form/form-view/account-form/account-form.component.ts @@ -14,8 +14,8 @@ export class AccountFormComponent implements OnChanges { @Input() nbSteps: number; @Input() currentStep: accountFormStep; @Input() accountForm: UntypedFormGroup; + @Input() isAccountMode: boolean; public isClaimMode = false; - public isAccountMode = false; public pagesValidation = []; public userAcceptSavedDate = false; public isPageValid: boolean; diff --git a/src/app/form/form-view/form-view-routing.module.ts b/src/app/form/form-view/form-view-routing.module.ts index 12765adf267195f0a77c1444cc14d0452162ad25..57186ba5cc388c37ce25f8de4ac7d766c4a17ade 100644 --- a/src/app/form/form-view/form-view-routing.module.ts +++ b/src/app/form/form-view/form-view-routing.module.ts @@ -1,8 +1,10 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AuthGuard } from '../../guards/auth.guard'; +import { DeactivateGuard } from '../../guards/deactivate.guard'; import { RoleGuard } from '../../guards/role.guard'; import { StructureResolver } from '../../resolvers/structure.resolver'; +import { TempUserResolver } from '../../resolvers/temp-user.resolver'; import { RouteRole } from '../../shared/enum/routeRole.enum'; import { AccountFormComponent } from './account-form/account-form.component'; import { FormViewComponent } from './form-view.component'; @@ -21,9 +23,18 @@ const routes: Routes = [ structure: StructureResolver, }, }, + { + path: 'register/:id', + component: FormViewComponent, + canDeactivate: [DeactivateGuard], + resolve: { + user: TempUserResolver, + }, + }, { path: '', component: FormViewComponent, + canDeactivate: [DeactivateGuard], children: [ { path: 'structure', diff --git a/src/app/form/form-view/form-view.component.html b/src/app/form/form-view/form-view.component.html index ed72d6c083a0a451f4ff09bb6751a6f8d9972028..a4089fd03e20082f991c4e95402091ae05af5120 100644 --- a/src/app/form/form-view/form-view.component.html +++ b/src/app/form/form-view/form-view.component.html @@ -1,4 +1,9 @@ <div class="formView"> + <app-modal-confirmation + [openned]="showConfirmationModal" + [content]="'Il vous faudra de nouveau remplir le formulaire si vous quittez'" + (closed)="hasRedirectionAccepted($event)" + ></app-modal-confirmation> <app-progress-bar [formType]="formType[routeParam]" [isEditMode]="isEditMode" @@ -6,12 +11,13 @@ [nbSteps]="nbSteps" ></app-progress-bar> <div class="page"> - <ng-container *ngIf="formType[routeParam] === formType.account"> + <ng-container *ngIf="formType[routeParam] === formType.account || formType[this.routeParam] === formType.register"> <app-account-form [nbSteps]="nbSteps" [accountForm]="accountForm" [hoursForm]="hoursForm" [currentStep]="currentPage" + [isAccountMode]="isAccountMode" (goNext)="nextPage()" (pageValid)="validatePage($event)" (acceptNewsletter)="acceptReceiveNewsletter($event)" @@ -57,6 +63,7 @@ [currentStep]="currentPage" [currentForm]="currentFormType" [form]="currentForm" + [linkedStructureId]="linkedStructureId" [btnName]="['Précédent', 'Suivant']" [isValid]="isPageValid" [acceptNewsletter]="userAcceptNewsletter" diff --git a/src/app/form/form-view/form-view.component.ts b/src/app/form/form-view/form-view.component.ts index c9874c160ef0c0f986b2db2bf92551b2d40328d7..4c902fe7feb8f1a0a29b0600f3999276fb43b3d7 100644 --- a/src/app/form/form-view/form-view.component.ts +++ b/src/app/form/form-view/form-view.component.ts @@ -24,7 +24,7 @@ import { structureFormStep } from './structure-form/structureFormStep.enum'; @Component({ selector: 'app-form-view', templateUrl: './form-view.component.html', - styleUrls: ['./form-view.component.scss'] + styleUrls: ['./form-view.component.scss'], }) export class FormViewComponent implements OnInit { public routeParam: string; @@ -62,6 +62,10 @@ export class FormViewComponent implements OnInit { public showWebsite: boolean; public showSocialNetwork: boolean; + // Modal canExit var + public showConfirmationModal = false; + private resolve: Function; + public profile: User; public isAccountMode: boolean = false; public isClaimMode: boolean = false; @@ -71,6 +75,7 @@ export class FormViewComponent implements OnInit { public structureWithOwners: StructureWithOwners; public isPageValid: boolean = false; public hoursForm: UntypedFormGroup; + public isRegisterNewMember: boolean = false; constructor( private router: Router, @@ -87,26 +92,19 @@ export class FormViewComponent implements OnInit { this.initPage(); this.profileService.getProfile().then((user: User) => { this.profile = user; + + if (!this.isEditMode && this.profile) { + if (this.profile.structuresLink.length) { + // if register a new user as a new member structure, no structure to choose + this.isRegisterNewMember = true; + this.structureService.getStructure(this.profile.structuresLink[0]).subscribe((structure) => { + this.structure = new Structure(structure); + }); + } + } }); // Check if it's a new structure or edit structure - // this.isLoading = false; - if (history.state.newUser) { - this.isClaimMode = true; - // Handle join structure, the case is very similar to claim - if (history.state.isJoin) { - this.isJoinMode = true; - } - this.createAccountForm(); - this.claimStructure = history.state.newUser; - } - // Handle account creation when pre-register this.route.data.subscribe((data) => { - if (data.user) { - this.isAccountMode = true; - this.createAccountForm(data.user.email); - this.linkedStructureId = data.user.pendingStructuresLink; - this.currentPage = accountFormStep.accountInfo; - } if (data.structure) { this.isEditMode = true; this.structure = data.structure; @@ -138,6 +136,19 @@ export class FormViewComponent implements OnInit { this.createAccountForm(); this.currentForm = this.accountForm; } + if (formType[this.routeParam] === formType.register) { + this.nbSteps = 3; + this.currentPage = accountFormStep.accountInfo; + this.currentFormType = formType.account; + this.route.data.subscribe((data) => { + if (data.user) { + this.createAccountForm(data.user.email); + this.linkedStructureId = data.user.pendingStructuresLink; + this.currentForm = this.accountForm; + this.isAccountMode = true; + } + }); + } if (formType[this.routeParam] === formType.profile) { this.nbSteps = totalFormSteps; this.currentPage = profileFormStep.profileBeginningInfo; @@ -175,15 +186,21 @@ export class FormViewComponent implements OnInit { private createAccountForm(email?: string): void { this.accountForm = new UntypedFormGroup( { - email: new UntypedFormControl(email ? email : '', [Validators.required, Validators.pattern(CustomRegExp.EMAIL)]), + email: new UntypedFormControl(email ? email : '', [ + Validators.required, + Validators.pattern(CustomRegExp.EMAIL), + ]), name: new UntypedFormControl('', [Validators.required, Validators.pattern(CustomRegExp.TEXT_WITHOUT_NUMBER)]), - surname: new UntypedFormControl('', [Validators.required, Validators.pattern(CustomRegExp.TEXT_WITHOUT_NUMBER)]), + surname: new UntypedFormControl('', [ + Validators.required, + Validators.pattern(CustomRegExp.TEXT_WITHOUT_NUMBER), + ]), phone: new UntypedFormControl('', [Validators.required, Validators.pattern(CustomRegExp.PHONE)]), password: new UntypedFormControl('', [ Validators.required, - Validators.pattern(CustomRegExp.PASSWORD) //NOSONAR + Validators.pattern(CustomRegExp.PASSWORD), //NOSONAR ]), - confirmPassword: new UntypedFormControl('') + confirmPassword: new UntypedFormControl(''), }, [MustMatch('password', 'confirmPassword')] ); @@ -193,14 +210,14 @@ export class FormViewComponent implements OnInit { this.profileForm = new UntypedFormGroup({ employer: new UntypedFormGroup({ name: new UntypedFormControl('', [Validators.required]), - validated: new UntypedFormControl(false, [Validators.required]) + validated: new UntypedFormControl(false, [Validators.required]), }), job: new UntypedFormGroup({ name: new UntypedFormControl('', [Validators.required]), validated: new UntypedFormControl(true, [Validators.required]), - hasPersonalOffer: new UntypedFormControl(true, [Validators.required]) + hasPersonalOffer: new UntypedFormControl(true, [Validators.required]), }), - structure: new UntypedFormControl('', [Validators.required]) + structure: new UntypedFormControl('', [Validators.required]), }); } @@ -216,7 +233,7 @@ export class FormViewComponent implements OnInit { accessRight: new UntypedFormControl(personalOffer.accessRight), digitalCultureSecurity: new UntypedFormControl(personalOffer.digitalCultureSecurity), socialAndProfessional: new UntypedFormControl(personalOffer.socialAndProfessional), - parentingHelp: new UntypedFormControl(personalOffer.parentingHelp) + parentingHelp: new UntypedFormControl(personalOffer.parentingHelp), }); } @@ -316,11 +333,20 @@ export class FormViewComponent implements OnInit { profile: this.profileService .updateProfile(this.profileForm.get('employer').value.name, this.profileForm.get('job').value.name) .pipe( - map((res) => res), + map((res) => (this.profile = res)), catchError((_e) => of()) - ) - }).subscribe(() => { - this.router.navigateByUrl('form/structure'); + ), + }).subscribe(async () => { + // if register a new user as a new member structure, no structure to choose + if (this.isRegisterNewMember) { + if (this.profile.job && this.profile.job.hasPersonalOffer) { + this.router.navigateByUrl('form/personaloffer'); + } else { + this.router.navigateByUrl('/profile'); + } + } else { + this.router.navigateByUrl('form/structure'); + } }); } @@ -338,7 +364,7 @@ export class FormViewComponent implements OnInit { const newStructure = new Structure(this.structureForm.value); newStructure.hours = this.hoursForm.value; this.structureService.createStructure(newStructure, this.profile).subscribe((struct) => { - if (user.job.hasPersonalOffer) { + if (user.job && user.job.hasPersonalOffer) { this.structure = struct; this.router.navigateByUrl('form/personaloffer'); } else { @@ -365,30 +391,30 @@ export class FormViewComponent implements OnInit { switch (currentPage) { case structureFormStep.structureChoice: return { - _id: this.structureForm.get('_id').value + _id: this.structureForm.get('_id').value, }; case structureFormStep.structureNameAndAddress: return { structureName: this.structureForm.get('structureName').value, - address: this.structureForm.get('address').value + address: this.structureForm.get('address').value, }; case structureFormStep.structureContact: return { contactPhone: this.structureForm.get('contactPhone').value, - contactMail: this.structureForm.get('contactMail').value + contactMail: this.structureForm.get('contactMail').value, }; case structureFormStep.structureAccessModality: return { - accessModality: this.structureForm.get('accessModality').value + accessModality: this.structureForm.get('accessModality').value, }; case structureFormStep.structureHours: return { hours: this.hoursForm.value, - exceptionalClosures: this.structureForm.get('exceptionalClosures').value + exceptionalClosures: this.structureForm.get('exceptionalClosures').value, }; case structureFormStep.structurePmr: return { - pmrAccess: this.structureForm.get('pmrAccess').value + pmrAccess: this.structureForm.get('pmrAccess').value, }; case structureFormStep.structureWebAndSocialNetwork: return { @@ -396,13 +422,13 @@ export class FormViewComponent implements OnInit { instagram: this.structureForm.get('instagram').value, linkedin: this.structureForm.get('linkedin').value, twitter: this.structureForm.get('twitter').value, - website: this.structureForm.get('website').value + website: this.structureForm.get('website').value, }; case structureFormStep.structurePublicTarget: return { publics: this.structureForm.get('publics').value }; case structureFormStep.structureDigitalHelpingAccompaniment: return { - proceduresAccompaniment: this.structureForm.get('proceduresAccompaniment').value + proceduresAccompaniment: this.structureForm.get('proceduresAccompaniment').value, }; case structureFormStep.structureTrainingType: return { @@ -410,15 +436,15 @@ export class FormViewComponent implements OnInit { baseSkills: this.structureForm.get('baseSkills').value, digitalCultureSecurity: this.structureForm.get('digitalCultureSecurity').value, parentingHelp: this.structureForm.get('parentingHelp').value, - socialAndProfessional: this.structureForm.get('socialAndProfessional').value + socialAndProfessional: this.structureForm.get('socialAndProfessional').value, }; case structureFormStep.structureTrainingPrice: return { - freeWorkShop: this.structureForm.get('freeWorkShop').value + freeWorkShop: this.structureForm.get('freeWorkShop').value, }; case structureFormStep.structureWifi: return { - equipmentsAndServices: this.structureForm.get('equipmentsAndServices').value + equipmentsAndServices: this.structureForm.get('equipmentsAndServices').value, }; case structureFormStep.structureEquipments: return { @@ -426,28 +452,57 @@ export class FormViewComponent implements OnInit { nbNumericTerminal: this.structureForm.get('nbNumericTerminal').value, nbPrinters: this.structureForm.get('nbPrinters').value, nbScanners: this.structureForm.get('nbScanners').value, - nbTablets: this.structureForm.get('nbTablets').value + nbTablets: this.structureForm.get('nbTablets').value, }; case structureFormStep.structureLabels: return { - labelsQualifications: this.structureForm.get('labelsQualifications').value + labelsQualifications: this.structureForm.get('labelsQualifications').value, }; case structureFormStep.structureOtherServices: return { - equipmentsAndServices: this.structureForm.get('equipmentsAndServices').value + equipmentsAndServices: this.structureForm.get('equipmentsAndServices').value, }; case structureFormStep.structureDescription: return { - description: this.structureForm.get('description').value + description: this.structureForm.get('description').value, }; case structureFormStep.structureCovidInfo: return { - lockdownActivity: this.structureForm.get('lockdownActivity').value + lockdownActivity: this.structureForm.get('lockdownActivity').value, }; case structureFormStep.structureConsent: return { - dataShareConsentDate: this.structureForm.get('dataShareConsentDate').value ? new Date().toString() : null + dataShareConsentDate: this.structureForm.get('dataShareConsentDate').value ? new Date().toString() : null, }; } } + + public canExit(): Promise<boolean> { + // List all exit pages in order to authorise exit + const exitPages: stepType[] = [ + structureFormStep.noStructure, + structureFormStep.structureCreationFinishedInfo, + profileFormStep.profileJobSelection, + personalOfferFormStep.personalOfferFinishedInfo, + ]; + // Avoid confirmation when user submit form and leave. + if ( + this.currentPage === this.nbSteps || + this.currentPage < 1 || + this.isEditMode || + exitPages.includes(this.currentPage) + ) { + return new Promise((resolve) => resolve(true)); + } else { + return new Promise((resolve) => this.showModal(resolve)); + } + } + private showModal(resolve: Function): void { + this.showConfirmationModal = true; + this.resolve = resolve; + } + public hasRedirectionAccepted(hasAccept: boolean): void { + this.resolve(hasAccept); + this.showConfirmationModal = false; + } } diff --git a/src/app/form/form-view/formType.enum.ts b/src/app/form/form-view/formType.enum.ts index c82157735fde91ac6301dbb46707d703ef3be628..d5a7dd7dc763f9f220ef7cd86fadb3ff92ab87d5 100644 --- a/src/app/form/form-view/formType.enum.ts +++ b/src/app/form/form-view/formType.enum.ts @@ -3,4 +3,5 @@ export enum formType { profile, personaloffer, account, + register, } diff --git a/src/app/form/form-view/guards/personalOffer.guard.ts b/src/app/form/form-view/guards/personalOffer.guard.ts index 32b7d71c2db85ee4155d782ebe28bcbc6a80ba0e..d7fc5a8bfa845a775496a5424447e4173ec8c8b3 100644 --- a/src/app/form/form-view/guards/personalOffer.guard.ts +++ b/src/app/form/form-view/guards/personalOffer.guard.ts @@ -8,7 +8,11 @@ export class PersonalOfferGuard implements CanActivate { constructor(private router: Router) {} canActivate(route: ActivatedRouteSnapshot): UrlTree | boolean { - if (route.routeConfig.path === 'personaloffer' && this.router.routerState.snapshot.url === '/form/structure') { + if ( + route.routeConfig.path === 'personaloffer' && + (this.router.routerState.snapshot.url === '/form/profile' || + this.router.routerState.snapshot.url === '/form/structure') + ) { return true; } return this.router.parseUrl('/home'); diff --git a/src/app/form/form-view/personal-offer-form/personal-offer-form.component.ts b/src/app/form/form-view/personal-offer-form/personal-offer-form.component.ts index 9fc9ff204f47010b68d6f33cafcadfb03abd3c71..6312c60da03b6819df15b9dd9b3e354a620e15f5 100644 --- a/src/app/form/form-view/personal-offer-form/personal-offer-form.component.ts +++ b/src/app/form/form-view/personal-offer-form/personal-offer-form.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { formType } from '../formType.enum'; @@ -8,7 +8,7 @@ import { personalOfferFormStep } from './personalOfferFormStep.enum'; selector: 'app-personal-offer-form', templateUrl: './personal-offer-form.component.html', }) -export class PersonalOfferFormComponent { +export class PersonalOfferFormComponent implements OnChanges { @Input() nbSteps: number; @Input() currentStep: personalOfferFormStep; @Input() personalOfferForm: UntypedFormGroup; diff --git a/src/app/models/owner.model.ts b/src/app/models/owner.model.ts index 45a6de715c2934e0fac3ff79d39b6890db89d016..7342b3bce83c598886fc151a675a51c46b9ebf1f 100644 --- a/src/app/models/owner.model.ts +++ b/src/app/models/owner.model.ts @@ -1,4 +1,6 @@ export class Owner { email: string; _id: string; + name: string; + surname: string; } diff --git a/src/app/models/temp-user.model.ts b/src/app/models/temp-user.model.ts index 8540ce74e9034cc65018c679c39d06ff693449fc..6572cb28010099b82c26dbe28cc76a011601531d 100644 --- a/src/app/models/temp-user.model.ts +++ b/src/app/models/temp-user.model.ts @@ -2,4 +2,5 @@ export class TempUser { _id: string; email: string; pendingStructuresLink: string[]; + updatedAt: string; } diff --git a/src/app/profile/profile-routing.module.ts b/src/app/profile/profile-routing.module.ts index efa2a89b3aa637f97e9cc0bea9f80e0596411440..688a99bc9c395c5cddf04e09b4d4d6fb22f2ddd3 100644 --- a/src/app/profile/profile-routing.module.ts +++ b/src/app/profile/profile-routing.module.ts @@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router'; 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 { ProfileComponent } from './profile.component'; import { StructureEditionSummaryComponent } from './structure-edition-summary/structure-edition-summary.component'; @@ -29,6 +30,15 @@ const routes: Routes = [ structure: StructureResolver, }, }, + { + path: 'structure-members-management/:id', + component: StructureMembersManagementComponent, + canActivate: [RoleGuard], + data: { allowedRoles: [RouteRole.structureAdmin] }, + resolve: { + structure: StructureResolver, + }, + }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], diff --git a/src/app/profile/profile.module.ts b/src/app/profile/profile.module.ts index 7de2f27cea51e2ba8cbd2991c5ede08de5253e6e..1060d16a09bfb46dee8b104b7462b378cd5c54e7 100644 --- a/src/app/profile/profile.module.ts +++ b/src/app/profile/profile.module.ts @@ -5,11 +5,21 @@ import { CommonModule } from '@angular/common'; import { ProfileRoutingModule } from './profile-routing.module'; import { StructureEditionSummaryComponent } from './structure-edition-summary/structure-edition-summary.component'; 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 { MissingInformationComponent } from './structure-edition-summary/missing-information/missing-information.component'; import { NoInformationComponent } from './structure-edition-summary/no-information/no-information.component'; @NgModule({ - declarations: [ProfileComponent, StructureEditionSummaryComponent, EditComponent, MissingInformationComponent, NoInformationComponent], + declarations: [ + ProfileComponent, + StructureEditionSummaryComponent, + EditComponent, + StructureMembersManagementComponent, + StructureAddMemberModalComponent, + MissingInformationComponent, + NoInformationComponent, + ], imports: [CommonModule, ProfileRoutingModule, SharedModule], }) export class ProfileModule {} diff --git a/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.html b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b8aea48a543f61363a0a4fcbeadd75682275d74b --- /dev/null +++ b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.html @@ -0,0 +1,32 @@ +<div class="modalBackground"> + <div class="modal"> + <div class="modalHeader"> + <h3>Ajouter un membre</h3> + </div> + <form [formGroup]="formAddAccount" class="modalContent"> + <div class="form-group" fxLayout="column"> + <label for="email">Email de la personne à ajouter</label> + <p *ngIf="ownerAlreadyLinked" class="special invalid">L'email est déjà rattaché à la structure.</p> + <div fxLayout="row" fxLayoutGap="13px"> + <input type="text" formControlName="email" class="form-input" autocomplete="on" /> + <app-svg-icon *ngIf="fAddAccount.email.valid" [type]="'form'" [icon]="'validate'"></app-svg-icon> + <app-svg-icon + *ngIf="fAddAccount.email.invalid && fAddAccount.email.value" + [type]="'form'" + [icon]="'notValidate'" + ></app-svg-icon> + </div> + </div> + <div class="buttons" fxLayout="row" fxLayoutAlign="space-between center"> + <app-button [text]="'Annuler'" (action)="closeModal(false)"></app-button> + <app-button + [text]="'Valider'" + [disabled]="formAddAccount.invalid" + (action)="addOwner()" + [style]="buttonTypeEnum.Primary" + > + </app-button> + </div> + </form> + </div> +</div> diff --git a/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.scss b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..febea28dc8adc6aaf441cf95992cfd304b5eb6d9 --- /dev/null +++ b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.scss @@ -0,0 +1,44 @@ +@import '../../../assets/scss/color'; +@import '../../../assets/scss/typography'; +@import '../../../assets/scss/shapes'; +@import '../../../assets/scss/z-index'; + +.modalBackground { + .modal { + max-width: 390px; + background-color: $white; + .modalHeader { + display: flex; + justify-content: center; + align-items: center; + border: 1px solid $grey-6; + padding: 0 8px; + h3 { + @include lato-bold-18; + } + } + + .modalContent { + padding: 24px 40px; + } + + p { + text-align: center; + margin: 10px 0; + + &.special { + margin: 8px 0; + @include lato-regular-14; + color: $grey-3; + &.invalid { + color: $orange-warning; + } + } + } + + .buttons { + gap: 24px; + padding-top: 8px; + } + } +} diff --git a/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.spec.ts b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c66205e6dbebe4866aa3f00bb7ebefa1ba3a76ab --- /dev/null +++ b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StructureAddMemberModalComponent } from './structure-add-member-modal.component'; + +describe('StructureMembersManagementComponent', () => { + let component: StructureAddMemberModalComponent; + let fixture: ComponentFixture<StructureAddMemberModalComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StructureAddMemberModalComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StructureAddMemberModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.ts b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..66f7e5d3fb069bd09693eb35e9929f5070ff593a --- /dev/null +++ b/src/app/profile/structure-add-member-modal/structure-add-member-modal.component.ts @@ -0,0 +1,54 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { StructureWithOwners } from '../../models/structureWithOwners.model'; +import { TempUser } from '../../models/temp-user.model'; +import { StructureService } from '../../services/structure.service'; +import { ButtonType } from '../../shared/components/button/buttonType.enum'; +import { CustomRegExp } from '../../utils/CustomRegExp'; + +@Component({ + selector: 'app-structure-add-member-modal', + templateUrl: './structure-add-member-modal.component.html', + styleUrls: ['./structure-add-member-modal.component.scss'], +}) +export class StructureAddMemberModalComponent implements OnInit { + @Input() public structure: StructureWithOwners; + @Output() closed = new EventEmitter(); + public buttonTypeEnum = ButtonType; + public formAddAccount: FormGroup; + public ownerAlreadyLinked = false; + + constructor(private formBuilder: FormBuilder, private structureService: StructureService) {} + + ngOnInit(): void { + this.formAddAccount = this.formBuilder.group({ + email: ['', [Validators.required, Validators.pattern(CustomRegExp.EMAIL)]], + }); + } + + public closeModal(value: boolean): void { + this.closed.emit(value); + } + + // getter for form fields + get fAddAccount(): { [key: string]: AbstractControl } { + return this.formAddAccount.controls; + } + + public addOwner(): void { + // stop here if form is invalid + if (this.formAddAccount.invalid) { + return; + } + const user = new TempUser(); + user.email = this.fAddAccount.email.value; + this.structureService.addOwnerToStructure(user, this.structure.structure._id).subscribe( + () => { + this.closed.emit(true); + }, + (err) => { + this.ownerAlreadyLinked = true; + } + ); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..903dffbfb5d206e6533879f89fb5f761c2698142 --- /dev/null +++ b/src/app/profile/structure-members-management/structure-members-management.component.html @@ -0,0 +1,92 @@ +<div class="content-container full-screen"> + <div class="container members-management"> + <div class="header"> + <div fxLayout="row" 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 les membres<br />de {{ structure.structureName }}</h1> + </div> + <app-button + [style]="buttonTypeEnum.Secondary" + [text]="'Ajouter un membre'" + (click)="addMemberModalOpenned = true" + tabindex="0" + ></app-button> + </div> + </div> + <div *ngIf="structureWithOwners && tempUsers"> + <div fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="baseline baseline"> + <div *ngFor="let member of structureWithOwners.owners" class="member-card"> + <div fxLayout="row" fxLayoutAlign="space-between center" fxFill> + <div fxLayout="row" fxLayoutAlign="start center" class="user"> + <app-svg-icon + class="avatar" + [type]="'avatar'" + [icon]="'defaultAvatar'" + [iconClass]="'icon-40'" + ></app-svg-icon> + <div class="info-member"> + <p class="member">{{ displayMemberName(member) }}</p> + <p class="job" *ngIf="displayJobEmployer(member)">{{ displayJobEmployer(member) }}</p> + </div> + </div> + <app-button + class="button-member" + [style]="buttonTypeEnum.Secondary" + [text]="'Exclure ce membre'" + (click)="memberToExclude = member; excludeModalOpenned = true" + tabindex="0" + ></app-button> + </div> + </div> + <div *ngFor="let member of tempUsers" class="member-card"> + <div fxLayout="row" fxLayoutAlign="space-between center" fxFill class="card-container"> + <div fxLayout="row" fxLayoutAlign="start center" class="user"> + <app-svg-icon + class="avatar" + [type]="'avatar'" + [icon]="'defaultAvatar'" + [iconClass]="'icon-40'" + ></app-svg-icon> + <div class="info-member"> + <p class="member">{{ member.email }}</p> + </div> + </div> + <div fxLayout="row" fxLayoutAlign="start center" class="pendingContainer"> + <div class="info-pendingStructure"> + <app-svg-icon class="check-icon" [type]="'ico'" [icon]="'check'"></app-svg-icon> + <p class="text">Demande de rattachement envoyée le {{ member.updatedAt | date: 'dd/MM/YYYY' }}</p> + </div> + <app-button + [style]="buttonTypeEnum.Secondary" + [text]="'Annuler la demande'" + (click)="tempUserToCancel = member; cancelAddTempUserModalOpenned = true" + tabindex="0" + ></app-button> + </div> + </div> + </div> + </div> + </div> + </div> +</div> +<app-structure-add-member-modal + *ngIf="addMemberModalOpenned" + [openned]="addMemberModalOpenned" + [structure]="structureWithOwners" + (closed)="closeAddMemberModal($event)" +></app-structure-add-member-modal> +<app-modal-confirmation + *ngIf="excludeModalOpenned" + [openned]="excludeModalOpenned" + [content]="'Souhaitez-vous exclure ce membre\n(' + displayMemberName(memberToExclude) + ') ?'" + [customConfirmationText]="'Oui'" + (closed)="excludeMember(memberToExclude, $event)" +></app-modal-confirmation> +<app-modal-confirmation + *ngIf="cancelAddTempUserModalOpenned" + [openned]="cancelAddTempUserModalOpenned" + [content]="'Souhaitez-vous annuler la demande de rattachement de ce membre\n(' + tempUserToCancel.email + ') ?'" + [customConfirmationText]="'Oui'" + (closed)="cancelAddTempUser(tempUserToCancel, $event)" +></app-modal-confirmation> diff --git a/src/app/profile/structure-members-management/structure-members-management.component.scss b/src/app/profile/structure-members-management/structure-members-management.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..17db43964ca8645c694cb0e6761ffd130195e2ec --- /dev/null +++ b/src/app/profile/structure-members-management/structure-members-management.component.scss @@ -0,0 +1,83 @@ +@import '../../../assets/scss/color'; +@import '../../../assets/scss/typography'; +@import '../../../assets/scss/breakpoint'; + +.container { + margin: 1rem auto; + max-width: 980px; + padding: 2rem; + background: $white; + border-radius: 8px; + border: 1px solid $grey-6; + .header { + margin-bottom: 2rem; + } + .headerBack { + cursor: pointer; + } + h1 { + @include lato-regular-24; + color: $grey-1; + cursor: initial; + } + + .member-card { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + .user { + margin-right: 1rem; + } + .avatar { + background-color: $grey-8; + border-radius: 4px; + } + .info-member { + margin-left: 1rem; + p { + margin: 0; + } + .member { + @include lato-bold-14; + } + .job { + @include lato-regular-14; + } + } + .info-pendingStructure { + display: flex; + margin-right: 1rem; + max-width: 200px; + p { + margin: 0; + } + .text { + @include lato-regular-13; + color: $grey-3; + margin-left: 3px; + } + } + .card-container { + @media #{$large-phone} { + flex-direction: column !important; + align-items: flex-start !important; + } + } + } +} + +.members-management { + ::ng-deep .btn-regular.secondary .text { + width: 184px !important; + height: 24px !important; + } + .button-member { + ::ng-deep .btn-regular.secondary .text { + color: $red !important; + } + } + ::ng-deep .modalBackground p { + white-space: pre-wrap; + } +} diff --git a/src/app/profile/structure-members-management/structure-members-management.component.spec.ts b/src/app/profile/structure-members-management/structure-members-management.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0178b47bd351257df31ab0ae43e41def3f522c0 --- /dev/null +++ b/src/app/profile/structure-members-management/structure-members-management.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StructureMembersManagementComponent } from './structure-members-management.component'; + +describe('StructureMembersManagementComponent', () => { + let component: StructureMembersManagementComponent; + let fixture: ComponentFixture<StructureMembersManagementComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StructureMembersManagementComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StructureMembersManagementComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/profile/structure-members-management/structure-members-management.component.ts b/src/app/profile/structure-members-management/structure-members-management.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5292e48f1f2327212981e34d05b7a253d2942822 --- /dev/null +++ b/src/app/profile/structure-members-management/structure-members-management.component.ts @@ -0,0 +1,112 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Owner } from '../../models/owner.model'; +import { Structure } from '../../models/structure.model'; +import { StructureWithOwners } from '../../models/structureWithOwners.model'; +import { TempUser } from '../../models/temp-user.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 { Utils } from '../../utils/utils'; +import { ProfileService } from '../services/profile.service'; + +@Component({ + selector: 'app-structure-members-management', + templateUrl: './structure-members-management.component.html', + styleUrls: ['./structure-members-management.component.scss'], +}) +export class StructureMembersManagementComponent implements OnInit { + public structure: Structure; + public structureWithOwners: StructureWithOwners; + public tempUsers: TempUser[]; + public tempUserToCancel: TempUser; + public memberToExclude: Owner; + public addMemberModalOpenned: boolean = false; + public excludeModalOpenned = false; + public cancelAddTempUserModalOpenned = false; + public buttonTypeEnum = ButtonType; + + constructor( + private route: ActivatedRoute, + private structureService: StructureService, + private profileService: ProfileService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.route.data.subscribe(async (data) => { + if (data.structure) { + this.structure = new Structure(data.structure); + let currentProfile = await this.profileService.getProfile(); + this.structureService.getStructureWithOwners(data.structure._id, currentProfile).subscribe((s) => { + this.structureWithOwners = s; + }); + this.structureService.getTempUsers(data.structure._id).subscribe((data) => { + this.tempUsers = data; + }); + } + }); + } + public goBack(): void { + history.back(); + } + public displayJobEmployer(profile: User): string { + return new Utils().getJobEmployer(profile); + } + public displayMemberName(member: Owner): string { + return member.name + ' ' + member.surname.toUpperCase(); + } + + public excludeMember(member: Owner, shouldExclude: boolean): void { + this.excludeModalOpenned = false; + if (shouldExclude) { + this.structureService.removeOwnerFromStructure(member._id, this.structure._id).subscribe( + () => { + this.structureWithOwners.owners = this.structureWithOwners.owners.filter((obj) => obj._id !== member._id); + this.notificationService.showSuccess( + `${this.displayMemberName(member)} a bien été exclu de ${this.structure.structureName}`, + '' + ); + }, + () => { + this.notificationService.showError( + `${this.displayMemberName(member)} n'a pas pu être exclu de ${ + this.structure.structureName + }. Merci de réessayer plus tard.`, + "Echec de l'exclusion" + ); + } + ); + } + } + + public cancelAddTempUser(member: TempUser, shouldExclude: boolean): void { + this.cancelAddTempUserModalOpenned = false; + if (shouldExclude) { + this.structureService.removeTempUserFromStructure(member._id, this.structure._id).subscribe( + () => { + this.tempUsers = this.tempUsers.filter((obj) => obj._id !== member._id); + this.notificationService.showSuccess( + `La demande d'ajout de ${member.email} à ${this.structure.structureName} a bien été annulée`, + '' + ); + }, + () => { + this.notificationService.showError( + `La demande d'ajout de ${member.email} à ${this.structure.structureName} n'a pas pu être annulée. Merci de réessayer plus tard.`, + "Echec de l'annulation" + ); + } + ); + } + } + + public closeAddMemberModal(memberAddRequested: boolean): void { + this.addMemberModalOpenned = false; + if (memberAddRequested) { + this.ngOnInit(); + this.notificationService.showSuccess(`La demande d'ajout a bien été effectuée`, ''); + } + } +} diff --git a/src/app/resolvers/temp-user.resolver.ts b/src/app/resolvers/temp-user.resolver.ts index 2ae604aa6396df43cb2e23ce9837efde3439ba1b..0f2b61a67311064acb3ec208cfaca1402b50907e 100644 --- a/src/app/resolvers/temp-user.resolver.ts +++ b/src/app/resolvers/temp-user.resolver.ts @@ -10,7 +10,7 @@ export class TempUserResolver implements Resolve<TempUser> { constructor(private tempUserService: TempUserService, private router: Router) {} resolve(route: ActivatedRouteSnapshot): Observable<TempUser> { - const userId = route.queryParams.id; + const userId = route.params.id; return this.tempUserService.getUser(userId).pipe( map((res) => res), catchError(() => { diff --git a/src/app/services/structure.service.ts b/src/app/services/structure.service.ts index 4c41e148a2ea93792acc1d18b39c7036fb9ac577..662d69955b2277f25d0e6802ab5915929fff72d0 100644 --- a/src/app/services/structure.service.ts +++ b/src/app/services/structure.service.ts @@ -64,6 +64,9 @@ export class StructureService { return this.http.delete<Structure>(`${this.baseUrl}/${id}`); } + public removeTempUserFromStructure(idOwner: string, idStructure: string): Observable<any> { + return this.http.delete<any>(`${this.baseUrl}/${idStructure}/tempUser/${idOwner}`); + } public removeOwnerFromStructure(idOwner: string, idStructure: string): Observable<any> { return this.http.delete<any>(`${this.baseUrl}/${idStructure}/owner/${idOwner}`); } @@ -174,6 +177,10 @@ export class StructureService { return this.http.post<any>(`${this.baseUrl}/${structureId}/withOwners`, { emailUser: profile?.email }); } + public getTempUsers(structureId: string): Observable<TempUser[]> { + return this.http.get<TempUser[]>(`${this.baseUrl}/${structureId}/tempUsers`); + } + public sendMailOnStructureError(structureId: string, content: string): Observable<any> { return this.http.post<any>(`${this.baseUrl}/reportStructureError`, { structureId, diff --git a/src/app/shared/components/modal-options/modal-options.component.html b/src/app/shared/components/modal-options/modal-options.component.html index 0e45ca7cc1e91a5f82d66650fc0b615808afc922..b1f51492467791e75c2402909cca3fb58bd6e7c0 100644 --- a/src/app/shared/components/modal-options/modal-options.component.html +++ b/src/app/shared/components/modal-options/modal-options.component.html @@ -14,6 +14,10 @@ </div> </div> <div *ngIf="!isModalProfileOpts" class="modalContent" fxLayout="column" fxLayoutGap="10px"> + <div (click)="closeModal(functionType.manageOwners)" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="9px"> + <app-svg-icon [type]="'ico'" [iconColor]="'inherit'" [icon]="'edit'"></app-svg-icon> + <p>Gérer les membres</p> + </div> <div (click)="closeModal(functionType.addUser)" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="9px"> <app-svg-icon [type]="'ico'" [iconColor]="'inherit'" [icon]="'add'"></app-svg-icon> <p>Ajouter un compte</p> diff --git a/src/app/shared/components/structure-options-modal/structure-options-modal.component.ts b/src/app/shared/components/structure-options-modal/structure-options-modal.component.ts index 2ff02f91dbabe59b8258de79bb5c7c8a9e8ea59d..1d04371dbe96ef0e1c9b7c8df709d4cbe8c54529 100644 --- a/src/app/shared/components/structure-options-modal/structure-options-modal.component.ts +++ b/src/app/shared/components/structure-options-modal/structure-options-modal.component.ts @@ -84,6 +84,9 @@ export class StructureOptionsModalComponent implements OnInit { case FunctionTypeModalOptions.deleteAccount: this.toggleDeleteAccountModal(); break; + case FunctionTypeModalOptions.manageOwners: + this.router.navigateByUrl(`/profile/structure-members-management/${this.structure.structure._id}`); + break; case FunctionTypeModalOptions.addUser: this.editModal = TypeModalProfile.addAccount; this.ownerAlreadyLinked = false; diff --git a/src/app/shared/enum/functionTypeModalOptions.enum.ts b/src/app/shared/enum/functionTypeModalOptions.enum.ts index 2ec4677afa5704ded37b42bde9165524b78a5640..338457416bdcc41a934b49a715c84f5f44b490b6 100644 --- a/src/app/shared/enum/functionTypeModalOptions.enum.ts +++ b/src/app/shared/enum/functionTypeModalOptions.enum.ts @@ -2,6 +2,7 @@ export enum FunctionTypeModalOptions { changeEmail = 1, changePassword, deleteAccount, + manageOwners, addUser, removeUser, editStructure, diff --git a/src/app/structure-list/components/structure-details/structure-details.component.html b/src/app/structure-list/components/structure-details/structure-details.component.html index 785ae3639c958d1a26c6257ad5894fa496ee39df..f833357027916ae238a321489fe1f542b9da8416 100644 --- a/src/app/structure-list/components/structure-details/structure-details.component.html +++ b/src/app/structure-list/components/structure-details/structure-details.component.html @@ -268,7 +268,7 @@ [iconClass]="'icon-40'" ></app-svg-icon> <div class="info-member"> - <p class="member">{{ member.name | uppercase }} {{ member.surname | titlecase }}</p> + <p class="member">{{ member.name }} {{ member.surname | uppercase }}</p> <p class="job" *ngIf="displayJobEmployer(member)">{{ displayJobEmployer(member) }}</p> </div> </div> diff --git a/src/assets/ico/sprite.svg b/src/assets/ico/sprite.svg index f585b46dbcd9b1e7311546222e921d56ddd4d1fb..f28bb7a503f599a29e9036a95fab3fe0a24d584e 100644 --- a/src/assets/ico/sprite.svg +++ b/src/assets/ico/sprite.svg @@ -873,5 +873,8 @@ </symbol> +<symbol id="check" width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M18.9977 6.2526C19.4105 6.61952 19.4477 7.25159 19.0808 7.66437L10.9326 16.831C10.5785 17.2294 9.97428 17.28 9.55883 16.9462L4.37364 12.7795C3.94313 12.4336 3.87458 11.8041 4.22053 11.3736C4.56647 10.9431 5.19592 10.8746 5.62643 11.2205L10.0699 14.7912L17.586 6.33565C17.9529 5.92286 18.585 5.88568 18.9977 6.2526Z" fill="#696969" /> +</symbol> </svg>