diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bd552650f263026a13f8ff214f99a50ea68eba..c4aed4b02c524c3e1a5e11a544fdb0474c028b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.15.0](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_client/compare/v1.14.0...v1.15.0) (2022-03-08) + + +### Features + +* **data-consent:** add data sharing consent when creating and editing a structure and at log-in if no consent was ever registered for at least one of the user's structures ([4214766](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_client/commit/42147665ecc287079acc03c169708b87daedcd2d)) + + +### Bug Fixes + +* **orientation-form:** set progression to 100 when clicking print ([e52fbf5](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_client/commit/e52fbf5657b0a41d638f153bd6af708ec9f5cd60)) +* **posts:** load more posts with tag ([a003d05](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_client/commit/a003d055cf594db61cdf4ca773194bd913aa4159)) +* typo and labels ([916739f](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_client/commit/916739f91d9fb14334278a44a953a7bcdf1f575b)) + ## [1.14.0](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_client/compare/v1.13.0...v1.14.0) (2022-02-21) diff --git a/package-lock.json b/package-lock.json index 17360dfd87dc1adc47011a52dd9e86f5c11553f7..fd2d00ca8b44fdb6992b0e53f5326b6768bd4073 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pamn", - "version": "1.14.0", + "version": "1.15.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e978d75bb56e522c587a00eefb1208b043aad48d..541783c6ea4a08ca8b99d08f69121032a7ac0d98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pamn", - "version": "1.14.0", + "version": "1.15.0", "scripts": { "ng": "ng", "start": "ng serve --configuration=fr --proxy-config proxy.conf.json", diff --git a/src/app/admin/components/manage-users/manage-users.component.ts b/src/app/admin/components/manage-users/manage-users.component.ts index 5e89e6f043864ff6a5403c400c4bac9b8ad9a316..d4713e661aa5d7013f2e30e5c1d3a2f3109d2bc1 100644 --- a/src/app/admin/components/manage-users/manage-users.component.ts +++ b/src/app/admin/components/manage-users/manage-users.component.ts @@ -63,6 +63,7 @@ export class ManageUsersComponent { }, { headerName: 'Actions', + editable: false, minWidth: 150, cellRenderer: 'deleteUserComponent', cellRendererParams: { diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 8718c963036cda112541a8e99581925e63e64e0b..ea7f55407f722e33d6a3a157a60b8710f15e55a4 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -102,7 +102,6 @@ const routes: Routes = [ path: 'newsletter', component: NewsletterSubscriptionComponent, }, - { path: 'newsletter-unsubscribe', component: NewsletterSubscriptionComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index eb67d95d7956a67be8b559beaaf6874c73cfc22e..4496fc8d943bb116b05e86507cc84157740ce9b8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -45,6 +45,7 @@ import { environment } from '../environments/environment'; import { StructureResolver } from './resolvers/structure.resolver'; import { RoleGuard } from './guards/role.guard'; import { UpdateService } from './services/update.service'; +import { DataShareConsentComponent } from './shared/components/data-share-consent/data-share-consent.component'; @NgModule({ declarations: [ @@ -72,6 +73,7 @@ import { UpdateService } from './services/update.service'; StructureDetailPrintComponent, StructureListPrintComponent, StructurePrintHeaderComponent, + DataShareConsentComponent, OrientationComponent, ], imports: [ diff --git a/src/app/carto/carto.component.scss b/src/app/carto/carto.component.scss index 3fcf8948f75926feca5fb9e426e93f6a9372ec45..b308746329a5622bfeff34e6b9aae048beefd70a 100644 --- a/src/app/carto/carto.component.scss +++ b/src/app/carto/carto.component.scss @@ -2,6 +2,12 @@ @import '../../assets/scss/layout'; @import '../../assets/scss/z-index'; +::ng-deep .footer { + @media #{$tablet} { + display: none !important; + } +} + .left-pane { width: 640px; min-width: 640px; diff --git a/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.html b/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.html index eb6736c0ea987b2c9256626c44e4dd2c79704ea0..1a4934a37ed0be251c0a00b44513ec0ad4ba2005 100644 --- a/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.html +++ b/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.html @@ -39,7 +39,7 @@ <!-- Opening Hours --> <div fxLayout="row" class="w-100 mobile-column"> <div *ngIf="structure.hours.hasData()" fxFlex="50%"> - <h3 class="subtitle">Horaires d’ouverture au public</h3> + <h3 class="subtitle">Horaires</h3> <div fxLayout="column"> <div *ngFor="let day of structure.hours | keyvalue: keepOriginalOrder"> <div *ngIf="day.value.open"> @@ -60,7 +60,7 @@ </div> <!-- accessModality --> <div *ngIf="structure.accessModality.length > 0" fxFlex="40%"> - <h3 class="subtitle">Accès transports en commun</h3> + <h3 class="subtitle">Accès</h3> <div fxLayout="column"> <div *ngFor="let tclStop of tclStopPoints"> <div fxLayout="row wrap" fxLayoutGap="5px"> diff --git a/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.ts b/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.ts index d1e0fb6ed26cd1c7ef6f62685486999cbc090a50..eb6dc63bb08246eb7416adf8232764332dd93e62 100644 --- a/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.ts +++ b/src/app/form/orientation-form/component/structure-detail-print/structure-detail-print.component.ts @@ -5,7 +5,6 @@ import { TclService } from '../../../../services/tcl.service'; import { TclStopPoint } from '../../../../models/tclStopPoint.model'; import { AuthService } from '../../../../services/auth.service'; import { AccessModality } from '../../../../structure-list/enum/access-modality.enum'; -import { PublicCategorie } from '../../../../structure-list/enum/public.enum'; @Component({ selector: 'app-structure-detail-print', templateUrl: './structure-detail-print.component.html', @@ -29,44 +28,12 @@ export class StructureDetailPrintComponent implements OnInit { } } + public keepOriginalOrder = (a, b) => a.key; + public userIsLoggedIn(): boolean { return this.authService.isLoggedIn(); } - public getAccessLabel(accessModality: AccessModality): string { - switch (accessModality) { - case AccessModality.free: - return 'Accès libre'; - case AccessModality.meeting: - return 'Sur rendez-vous'; - case AccessModality.meetingOnly: - return 'Uniquement sur RDV'; - case AccessModality.numeric: - return 'Téléphone / Visio'; - default: - return null; - } - } - - public getPublicLabel(tagetPublic: PublicCategorie): string { - switch (tagetPublic) { - case PublicCategorie.young: - return 'Jeunes (16 - 25 ans)'; - case PublicCategorie.adult: - return 'Adultes'; - case PublicCategorie.elderly: - return 'Séniors (+ de 65 ans)'; - case PublicCategorie.all: - return 'Tout public'; - case PublicCategorie.under16Years: - return 'Moins de 16 ans'; - case PublicCategorie.women: - return 'Uniquement femmes'; - default: - return null; - } - } - public getTclStopPoints(): void { this.tclService.getTclStopPointBycoord(this.structure.getLon(), this.structure.getLat()).subscribe((res) => { this.tclStopPoints = res; diff --git a/src/app/form/orientation-form/orientation-form.component.html b/src/app/form/orientation-form/orientation-form.component.html index 3174a45f00bed4ff424ec5da32265f60c05d7c32..c8c0679cf027f85e1ee264dd9712b68e01b3434d 100644 --- a/src/app/form/orientation-form/orientation-form.component.html +++ b/src/app/form/orientation-form/orientation-form.component.html @@ -293,7 +293,7 @@ <!-- ADDRESS SEARCH --> <div *ngIf="currentPage == pageTypeEnum.beneficiaryAddress" class="page"> <div class="title"> - <h3>Autour de quelle adresse chercher une structure ?</h3> + <h3>Autour de quelle adresse cherchez-vous une structure ?</h3> <p class="notRequired lg">facultatif</p> </div> <div class="form-group" fxLayout="column"> diff --git a/src/app/form/structure-form/form.component.html b/src/app/form/structure-form/form.component.html index 8c2aee1319362521bd2b20ac067668de7fbdd5bc..f943a843276058ddbeb8f5f81fa67ffa5d663692 100644 --- a/src/app/form/structure-form/form.component.html +++ b/src/app/form/structure-form/form.component.html @@ -85,7 +85,7 @@ <div class="summary" *ngFor="let page of pagesValidation; let index = index"> <div class="itemSummary" - [ngClass]="{ last: index == 22 }" + [ngClass]="{ last: index == lastPage }" fxLayout="row" fxLayoutAlign="space-between center" *ngIf="page.name && shouldDisplayPage(index)" @@ -408,7 +408,7 @@ > <div *ngIf="currentPage == pageTypeEnum.structureNameAndAddress" class="page"> <div class="title"> - <h3>Quelle structure voulez-vous réferencer ?</h3> + <h3>Quelle structure voulez-vous référencer ?</h3> </div> <p class="missing-information" @@ -1285,49 +1285,88 @@ </div> </div> </form> - <div *ngIf="currentPage == pageTypeEnum.cgu" class="page"> - <div class="section"> - <div class="title"> - <h3> - Acceptez-vous que les informations saisies soient enregistrées par la Métropole de Lyon<span - class="asterisk" - >*</span + <form> + <div *ngIf="currentPage == pageTypeEnum.cgu" class="page"> + <div class="section" *ngIf="!isEditMode"> + <div class="title"> + <h3> + Acceptez-vous que les informations saisies soient enregistrées par la Métropole de Lyon<span + class="asterisk" + >*</span + > + ? + </h3> + </div> + <app-checkbox-form + [isChecked]="userAcceptSavedDate" + [text]="'J\'accepte'" + (checkEvent)="acceptDataBeSaved($event)" + > + </app-checkbox-form> + </div> + <div class="section"> + <div class="title"> + <h3> + Acceptez-vous que les informations de votre structure soient mises à disposition sur la plateforme + data.grandlyon.com<span class="asterisk" *ngIf="!isEditMode">**</span + ><span class="asterisk" *ngIf="isEditMode">*</span> ? + </h3> + <p class="notRequired" *ngIf="!isEditMode">facultatif</p> + </div> + <app-checkbox-form + *ngIf="!isEditMode" + [text]="'J\'accepte'" + (checkEvent)="acceptOpenData($event)" + ></app-checkbox-form> + <div class="dataShareConsent"> + <app-radio-form + *ngIf="isEditMode" + name="{{ getStructureControl('structureName').value }}" + horizontal="true" + [selectedOption]="getStructureControl('dataShareConsentDate').value === null ? false : true" + (selectedEvent)="onRadioBtnChange('dataShareConsentDate', $event)" > - ? - </h3> + </app-radio-form> + </div> </div> - <app-checkbox-form - [isChecked]="userAcceptSavedDate" - [text]="'J\'accepte'" - (checkEvent)="acceptDataBeSaved($event)" - > - </app-checkbox-form> - </div> - <div *ngIf="!profile"> - <div class="title"> - <h3>Acceptez-vous de recevoir des mails d'informations de la part de Res'in ?</h3> + <div *ngIf="!profile"> + <div class="title"> + <h3>Souhaitez-vous vous abonner à la lettre d’information de Res'in ?</h3> + <p class="notRequired" *ngIf="!isEditMode">facultatif</p> + </div> + <app-checkbox-form + [isChecked]="userAcceptNewsletter" + [text]="'J\'accepte'" + (checkEvent)="acceptReceiveNewsletter($event)" + > + </app-checkbox-form> + </div> + <p *ngIf="!isEditMode" class="informationEndForm"> + <span class="asterisk">*</span> Les informations recueillies sont enregistrées dans un fichier par la + Métropole de Lyon en vue de l'animation du réseau des acteurs de la médiation numérique. Elles sont conservées + pendant 24 mois et sont destinées aux seuls intervenants habilités de la Métropole de Lyon. Vos données + personnelles sont traitées dans ce cadre aux fins de recensement des actions de médiation numérique sur le + territoire de la métropole. Conformément à la loi 78-17 du 6 janvier 1978 modifiée relative à l'information, + aux fichiers et aux libertés, et au Règlement Général européen à la Protection des Données, vous avez la + possibilité d’exercer vos droits d’accès, de rectification, d’effacement, d’opposition, de limitation du + traitement et de révocation de votre consentement. Afin d'exercer vos droits, vous pouvez vous adresser : par + courrier postal à : Métropole de Lyon - Direction des Affaires Juridiques et de la Commande Publique - 20, rue + du Lac - BP 33569 - 69505 Lyon Cedex par courrier électronique en remplissant le formulaire dédié sur Toodego, + le site des services et démarches en ligne dans la Métropole de Lyon + </p> + <div class="page" *ngIf="currentPage == pageTypeEnum.cgu"> + <p class="informationEndForm"> + <span class="asterisk" *ngIf="!isEditMode">**</span><span class="asterisk" *ngIf="isEditMode">*</span> La + Métropole de Lyon, engagée pour la transparence de l’action publique et la valorisation de ses partenaires, + encourage l’ouverture des données. Les données de votre structure seront publiées sur la plateforme + <a href="https://data.grandlyon.com/" target="_blank">https://data.grandlyon.com/</a> sous la licence + ouverte (open data) et seront donc librement accessibles et réutilisables. Vous pourrez modifier votre choix + à tout moment, exercer vos droits d’accès et de modification, en le signifiant, par tout moyen à votre + convenance, auprès de vos interlocuteurs de la Métropole de Lyon. + </p> </div> - <app-checkbox-form - [isChecked]="userAcceptNewsletter" - [text]="'J\'accepte'" - (checkEvent)="acceptReceiveNewsletter($event)" - > - </app-checkbox-form> </div> - <p class="informationEndForm"> - <span class="asterisk">*</span> Les informations recueillies sont enregistrées dans un fichier par la Métropole - de Lyon en vue de l'animation du réseau des acteurs de la médiation numérique. Elles sont conservées pendant 24 - mois et sont destinées aux seuls intervenants habilités de la Métropole de Lyon. Vos données personnelles sont - traitées dans ce cadre aux fins de recensement des actions de médiation numérique sur le territoire de la - métropole. Conformément à la loi 78-17 du 6 janvier 1978 modifiée relative à l'information, aux fichiers et aux - libertés, et au Règlement Général européen à la Protection des Données, vous avez la possibilité d’exercer vos - droits d’accès, de rectification, d’effacement, d’opposition, de limitation du traitement et de révocation de - votre consentement. Afin d'exercer vos droits, vous pouvez vous adresser : par courrier postal à : Métropole de - Lyon - Direction des Affaires Juridiques et de la Commande Publique - 20, rue du Lac - BP 33569 - 69505 Lyon - Cedex par courrier électronique en remplissant le formulaire dédié sur Toodego, le site des services et - démarches en ligne dans la Métropole de Lyon - </p> - </div> + </form> <div *ngIf="currentPage == nbPagesForm && !profile" class="page" @@ -1340,7 +1379,7 @@ </svg> <h3>Un courriel vous a été envoyé afin de valider votre inscription</h3> </div> - <div *ngIf="currentPage == nbPagesForm && profile" class="lastPage"> + <div *ngIf="currentPage == nbPagesForm && profile && !isEditMode" class="lastPage"> <div class="lastPage"> <div class="title"> <h3> @@ -1390,7 +1429,7 @@ Ok </button> <button - *ngIf="currentPage == nbPagesForm && profile" + *ngIf="currentPage == nbPagesForm && profile && !isEditMode" class="btn-primary unique" routerLink="/acteurs" [queryParams]="{ id: createdStructure._id }" diff --git a/src/app/form/structure-form/form.component.scss b/src/app/form/structure-form/form.component.scss index 1fabc2e2c2515cffee34567922f311566e0615b6..7ab8c6a5cdc98fd908a84acaca7b79c6ad6f6c3b 100644 --- a/src/app/form/structure-form/form.component.scss +++ b/src/app/form/structure-form/form.component.scss @@ -189,6 +189,14 @@ h4 { margin-top: 18px; color: $grey-2; @include cn-regular-14; + a { + color: $default-link-color; + text-decoration: underline; + font-weight: bold; + } + } + &.notRequired { + font-style: italic; } } .textareaBlock { @@ -560,7 +568,11 @@ img { .section { padding-bottom: 2rem; } - +.dataShareConsent { + ::ng-deep button p { + font-weight: normal !important; + } +} .missing-information { display: flex; color: $orange-warning; diff --git a/src/app/form/structure-form/form.component.ts b/src/app/form/structure-form/form.component.ts index d0b4d04ad4ca17c273a65e371e2b448155d36986..5de928c31fc24048bb27637eee1f61168ee684be 100644 --- a/src/app/form/structure-form/form.component.ts +++ b/src/app/form/structure-form/form.component.ts @@ -83,6 +83,8 @@ export class FormComponent implements OnInit { // Structure id for edit mode public structureId: string; + // last page for edit form + public lastPage = this.pageTypeEnum.cgu; constructor( private structureService: StructureService, @@ -320,6 +322,7 @@ export class FormComponent implements OnInit { Validators.min(0), ]), freeWorkShop: new FormControl(structure.freeWorkShop, [Validators.required]), + dataShareConsentDate: new FormControl(structure.dataShareConsentDate), }); } @@ -590,7 +593,14 @@ export class FormComponent implements OnInit { valid: this.getStructureControl('lockdownActivity').valid, name: 'Informations spécifiques à la période COVID', }; - this.pagesValidation[PageTypeEnum.cgu] = { valid: this.userAcceptSavedDate }; + if (this.isEditMode) { + this.pagesValidation[PageTypeEnum.cgu] = { + valid: this.getStructureControl('dataShareConsentDate').valid, + name: 'Partage de données sur data.grandlyon.com', + }; + } else { + this.pagesValidation[PageTypeEnum.cgu] = { valid: this.userAcceptSavedDate }; + } this.updatePageValid(); } } @@ -898,6 +908,12 @@ export class FormComponent implements OnInit { this.setValidationsForm(); } + public acceptOpenData(isAccepted: boolean): void { + let now = new Date().toString(); + this.getStructureControl('dataShareConsentDate').setValue(now); + this.setValidationsForm(); + } + public acceptReceiveNewsletter(isAccepted: boolean): void { this.userAcceptNewsletter = isAccepted; } diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 87949d944f159266ce125637ab1e35abf099715a..a44bb541e0d323f4f8119f1f7b29bd93184875eb 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -57,6 +57,10 @@ </div> <app-signup-modal *ngIf="displaySignUp" [openned]="isPopUpOpen" (closed)="closeSignUpModal($event)"></app-signup-modal> +<app-data-share-consent + *ngIf="isDisplayDataShare" + [dataConsentPendingStructures]="dataConsentPendingStructures" +></app-data-share-consent> <ng-template #customTitle> <img class="desktop-show logo-grand-lyon" width="108" height="37" src="/assets/logos/resin.svg" alt /> diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index ee446b18ff5ca333d21817772be67f5e96ecd839..e1cee9024d83ba768405d6162c1f44d16960c625 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { Structure } from '../models/structure.model'; import { ProfileService } from '../profile/services/profile.service'; import { AuthService } from '../services/auth.service'; @@ -15,6 +16,9 @@ export class HeaderComponent implements OnInit { public currentRoute = ''; public formRoute = '/create-structure'; public returnUrl = null; + public dataConsentPendingStructures: Structure[]; + private displayDataShare = false; + private loadingDataShare = false; constructor( private authService: AuthService, @@ -53,6 +57,24 @@ export class HeaderComponent implements OnInit { return this.authService.isLoggedIn(); } + public get isDisplayDataShare(): boolean { + if (this.displayDataShare) { + return this.displayDataShare; + } else { + if (this.isLoggedIn && !this.loadingDataShare) { + this.loadingDataShare = true; + this.profileService.getAllDataConsentPendingStructures().subscribe((dataConsentPendingStructures) => { + if (dataConsentPendingStructures.length) { + this.displayDataShare = true; + this.dataConsentPendingStructures = dataConsentPendingStructures; + return this.displayDataShare; + } + }); + } + } + return false; + } + public closeSignInModal(): void { this.isPopUpOpen = false; this.displaySignUp = true; diff --git a/src/app/models/structure.model.ts b/src/app/models/structure.model.ts index b6612172572afe1ce4090c0d90d3467680ea41c9..f8dc38fd709a66954563e18698d4e09ce612338f 100644 --- a/src/app/models/structure.model.ts +++ b/src/app/models/structure.model.ts @@ -50,6 +50,7 @@ export class Structure { public distance?: number; public coord?: number[] = []; + public dataShareConsentDate?: string; public accountVerified: boolean = false; diff --git a/src/app/post/components/post-details/post-details.component.scss b/src/app/post/components/post-details/post-details.component.scss index 56d471bb1debe7412cdbe32c6d26500d046e0aca..38f180a38320559e4f5fe146af6b66f7d33ba76d 100644 --- a/src/app/post/components/post-details/post-details.component.scss +++ b/src/app/post/components/post-details/post-details.component.scss @@ -22,9 +22,25 @@ } .description { + div { + height: fit-content; + } ::ng-deep img { height: 100%; } + ::ng-deep iframe { + width: 100% !important; + max-height: 400px; + height: 100vw; + } + ::ng-deep .kg-embed-card { + max-height: 400px; + iframe { + width: 100% !important; + max-height: 100%; + min-height: 100px; + } + } ::ng-deep figure { figcaption { margin-top: 1%; diff --git a/src/app/post/components/post-list/post-list.component.ts b/src/app/post/components/post-list/post-list.component.ts index 38cad37daf6d207eb936368d670172180e72a887..b5347b836010e4e086021289d777971eafbecb61 100644 --- a/src/app/post/components/post-list/post-list.component.ts +++ b/src/app/post/components/post-list/post-list.component.ts @@ -56,11 +56,14 @@ export class PostListComponent implements OnInit { ...this.selectedLocationTagSlug, ...this.selectedPublicTagsSlug, ]; + // Reset posts + this.resetPosts(); // Apply search - this.getPosts(this.filters); + this.getPosts(1, this.filters); } else { // Init default news list this.allPosts = []; + this.filters = []; this.postService.getPosts(1).subscribe((news) => { this.fillArticles(news); }); @@ -84,7 +87,7 @@ export class PostListComponent implements OnInit { this.allPosts = [...headLineTag, ..._.difference(this.allPosts, headLineTag)]; } - public getPosts(filters?: Tag[]): void { + public getPosts(page: number, filters?: Tag[]): void { // Parse filter let parsedFilters = null; if (filters) { @@ -97,11 +100,8 @@ export class PostListComponent implements OnInit { } } - // Reset posts - this.resetPosts(); - this.isLoading = true; - this.postService.getPosts(1, parsedFilters).subscribe((news) => { + this.postService.getPosts(page, parsedFilters).subscribe((news) => { this.fillArticles(news); }); } @@ -135,9 +135,13 @@ export class PostListComponent implements OnInit { private loadMore(): void { if (this.pagination && this.pagination.page < this.pagination.pages) { this.isLoading = true; - this.postService.getPosts(this.pagination.next).subscribe((news) => { - this.fillArticles(news); - }); + if (this.filters) { + this.getPosts(this.pagination.next, this.filters); + } else { + this.postService.getPosts(this.pagination.next).subscribe((news) => { + this.fillArticles(news); + }); + } } } diff --git a/src/app/post/services/post.service.ts b/src/app/post/services/post.service.ts index 89020397d8274a59afb75c72d212dd211ba20f0d..e7f51cfd9178b10a31df187d2a2b8706cbe9f8f1 100644 --- a/src/app/post/services/post.service.ts +++ b/src/app/post/services/post.service.ts @@ -23,22 +23,21 @@ export class PostService { } public getPosts(page: number, tags?: string[]): Observable<PostWithMeta> { - if (!tags) { - return this.http - .get<PostWithMeta>(`${this.baseUrl}?page=${page}&include=tags,authors`) - .pipe(map((item: PostWithMeta) => new PostWithMeta(item))); + let tagsFilter = ''; + + if (tags) { + let tagsString = ''; + // Transform tab filters to string filters + tags.forEach((tag, index) => { + tagsString += tag; + if (index != tags.length - 1) { + tagsString += '+tags:'; + } + }); + tagsFilter = `&filter=tags:${encodeURIComponent(tagsString)}`; } - let tagsString = ''; - // Transform tab filters to string filters - tags.forEach((tag, index) => { - tagsString += tag; - if (index != tags.length - 1) { - tagsString += '+tags:'; - } - }); - return this.http - .get<PostWithMeta>(`${this.baseUrl}?include=tags,authors&filter=tags:${encodeURIComponent(tagsString)}`) - .pipe(map((item: PostWithMeta) => new PostWithMeta(item))); + + return this.http.get<PostWithMeta>(`${this.baseUrl}?page=${page}&include=tags,authors${tagsFilter}`); } public getTags(): Observable<TagWithMeta> { diff --git a/src/app/profile/services/profile.service.ts b/src/app/profile/services/profile.service.ts index 2f044a5f52502d5d275524387de0ec6d6bacf647..74158fb4be741ee3eb771ca7b453560b9fc924a3 100644 --- a/src/app/profile/services/profile.service.ts +++ b/src/app/profile/services/profile.service.ts @@ -5,6 +5,8 @@ import { User } from '../../models/user.model'; import decode from 'jwt-decode'; import { UserRole } from '../../shared/enum/userRole.enum'; import { AuthService } from '../../services/auth.service'; +import { Structure } from '../../models/structure.model'; +import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -81,4 +83,8 @@ export class ProfileService { public isEmailAlreadyUsed(newMail: string): Observable<boolean> { return this.http.post<boolean>(`${this.baseUrl}/verify-exist-user`, { newMail }); } + + public getAllDataConsentPendingStructures(): Observable<Structure[]> { + return this.http.get<Structure[]>(`${this.baseUrl}/dataConsentValidation`); + } } diff --git a/src/app/services/structure.service.ts b/src/app/services/structure.service.ts index 71f6417cc109db5f21c4ee221613a4c0328d78a1..6135dd776dd37022895e69a680be7cd039767247 100644 --- a/src/app/services/structure.service.ts +++ b/src/app/services/structure.service.ts @@ -36,6 +36,11 @@ export class StructureService { public editStructure(structure: Structure): Observable<Structure> { structure.updatedAt = new Date().toString(); + if (structure.dataShareConsentDate) { + structure.dataShareConsentDate = new Date().toString(); + } else { + structure.dataShareConsentDate = null; + } const id = structure._id; delete structure._id; // id should not be provided for update return this.http.put(`${this.baseUrl}/${id}`, structure).pipe(map((item: Structure) => new Structure(item))); diff --git a/src/app/shared/components/data-share-consent/data-share-consent.component.html b/src/app/shared/components/data-share-consent/data-share-consent.component.html new file mode 100644 index 0000000000000000000000000000000000000000..1fc7aafb6f576fbd1245ce897d66ce3031f06a3c --- /dev/null +++ b/src/app/shared/components/data-share-consent/data-share-consent.component.html @@ -0,0 +1,62 @@ +<div *ngIf="openned" class="modalBackground"> + <div class="modal"> + <div class="contentModal" fxLayout="column" fxLayoutAlign="space-around start"> + <div class="form"> + <div class="modalTitle"> + <h3> + Acceptez-vous que les informations de vos structures soient mises à disposition sur la plateforme + data.grandlyon.com* ? + </h3> + </div> + <form [formGroup]="consentForm" class="dataShareConsent"> + <app-radio-form + *ngIf="dataConsentPendingStructures && dataConsentPendingStructures.length > 1" + name="Toutes les structures" + horizontal="true" + (selectedEvent)="onRadioBtnChangeAll($event)" + [events]="eventsSubject.asObservable()" + layoutGap="8px" + class="firstLine" + ></app-radio-form> + + <div *ngFor="let structure of dataConsentPendingStructures"> + <app-radio-form + name="{{ structure.structureName }}" + horizontal="true" + [selectedOption]=" + structure.dataShareConsentDate === undefined + ? null + : structure.dataShareConsentDate === null + ? false + : true + " + (selectedEvent)="onRadioBtnChangeStructure(structure._id, $event)" + layoutGap="8px" + ></app-radio-form> + </div> + + <p class="informationEndForm"> + <span class="asterisk">*</span> La Métropole de Lyon, engagée pour la transparence de l’action publique et + la valorisation de ses partenaires, encourage l’ouverture des données. Les données de votre structure seront + publiées sur la plateforme + <a href="https://data.grandlyon.com/" target="_blank">https://data.grandlyon.com/</a> sous la licence + ouverte (open data) et seront donc librement accessibles et réutilisables. Vous pourrez modifier votre choix + à tout moment, exercer vos droits d’accès et de modification, en le signifiant, par tout moyen à votre + convenance, auprès de vos interlocuteurs de la Métropole de Lyon. + </p> + + <div class="footerModal" fxLayout="row" fxLayoutAlign="space-around center"> + <button + (click)="onSubmit()" + class="btn-primary" + [disabled]="!isPageValid" + [ngClass]="{ invalid: !isPageValid }" + > + Valider + </button> + </div> + </form> + </div> + </div> + </div> +</div> diff --git a/src/app/shared/components/data-share-consent/data-share-consent.component.scss b/src/app/shared/components/data-share-consent/data-share-consent.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..f7bbdfa9426ca6c19944d0178c69644d84fb9016 --- /dev/null +++ b/src/app/shared/components/data-share-consent/data-share-consent.component.scss @@ -0,0 +1,80 @@ +@import '../../../../assets/scss/typography'; +@import '../../../../assets/scss/breakpoint'; +@import '../../../../assets/scss/color'; +@import '../../../../assets/scss/buttons'; +@import '../../../../assets/scss/z-index'; +@import '../../../../assets/scss/hyperlink'; +@import '../radio-form/radio-form.component.scss'; + +.modalBackground .modal { + max-width: 700px; + @media #{$large-phone} { + max-width: 95%; + } +} +.modalTitle { + display: flex; + h3 { + margin-top: 6%; + width: 90%; + } +} +h3 { + @include cn-bold-26; + color: $black; + margin-top: 0; +} +.form { + max-width: 90%; + margin: 0 32px; + margin-bottom: 8%; +} +.footerModal { + button { + &.invalid { + opacity: 0.4; + } + } +} +.dataShareConsent { + ::ng-deep button, + ::ng-deep .name { + font-size: $font-size-small; + font-weight: normal; + margin: 4px 0; + height: 40px; + ::ng-deep p { + font-weight: normal !important; + } + @media #{$phone} { + height: auto; + } + } + ::ng-deep .name { + padding: 0 10px; + } + ::ng-deep button { + width: 162px; + height: 40px; + } + ::ng-deep .firstLine { + .name, + button { + background-color: $grey-4; + &.selected { + p { + color: $black; + } + } + } + } +} +.informationEndForm { + color: $grey-1; + font-size: $font-size-xsmall; + a { + color: $blue; + text-decoration: underline; + font-weight: bold; + } +} diff --git a/src/app/shared/components/data-share-consent/data-share-consent.component.spec.ts b/src/app/shared/components/data-share-consent/data-share-consent.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a617845c6f4df074b2c8f11c21d5888b5fd84627 --- /dev/null +++ b/src/app/shared/components/data-share-consent/data-share-consent.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataShareConsentComponent } from './data-share-consent.component'; + +describe('DataShareConsentComponent', () => { + let component: DataShareConsentComponent; + let fixture: ComponentFixture<DataShareConsentComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DataShareConsentComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataShareConsentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/data-share-consent/data-share-consent.component.ts b/src/app/shared/components/data-share-consent/data-share-consent.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..26785ea77a6617f0f2fc7ca903643e1acb7809b6 --- /dev/null +++ b/src/app/shared/components/data-share-consent/data-share-consent.component.ts @@ -0,0 +1,86 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { Structure } from '../../../models/structure.model'; +import { StructureService } from '../../../services/structure.service'; + +@Component({ + selector: 'app-data-share-consent', + templateUrl: './data-share-consent.component.html', + styleUrls: ['./data-share-consent.component.scss'], +}) +export class DataShareConsentComponent implements OnInit { + public consentForm: FormGroup; + public isPageValid: boolean; + public loading = false; + public submitted = false; + public eventsSubject: Subject<Object> = new Subject<Object>(); + + constructor(private structureService: StructureService) {} + + @Input() public openned: boolean = true; + @Input() public dataConsentPendingStructures: Structure[]; + + ngOnInit() { + this.consentForm = new FormGroup({}); + for (let structure of this.dataConsentPendingStructures) { + this.consentForm.addControl( + structure._id, + new FormControl(structure.dataShareConsentDate, [Validators.required]) + ); + } + } + + public getFormControl(nameControl: string): AbstractControl { + return this.consentForm.get(nameControl); + } + + public getPendingStructure(id: string): Structure { + var result = this.dataConsentPendingStructures.filter(function (o) { + return o._id == id; + }); + return result ? result[0] : null; + } + + public onRadioBtnChangeAll(bool: boolean): void { + for (let structure of this.dataConsentPendingStructures) { + structure.dataShareConsentDate = bool ? new Date().toString() : null; + this.getFormControl(structure._id).setValue(bool); + } + this.setValidationsForm(); + } + + public onRadioBtnChangeStructure(controlName: string, bool: boolean): void { + this.getPendingStructure(controlName).dataShareConsentDate = bool ? new Date().toString() : null; + this.getFormControl(controlName).setValue(bool); + + // select or unselect "all structures" radio button + let isAllYes: boolean = true; + let isAllNo: boolean = true; + for (let structure of this.dataConsentPendingStructures) { + isAllYes = isAllYes && this.getFormControl(structure._id).value === true; + isAllNo = isAllNo && this.getFormControl(structure._id).value === false; + } + this.eventsSubject.next(isAllYes ? true : isAllNo ? false : null); + + this.setValidationsForm(); + } + + public setValidationsForm(): void { + let isPageValid: boolean = true; + for (let structure of this.dataConsentPendingStructures) { + isPageValid = isPageValid && this.getFormControl(structure._id).valid; + } + this.isPageValid = isPageValid; + } + + public onSubmit(): void { + this.submitted = true; + this.loading = true; + for (let structure of this.dataConsentPendingStructures) { + this.structureService.editStructure(structure).subscribe((s: Structure) => {}); + } + this.loading = false; + this.openned = false; + } +} diff --git a/src/app/shared/components/radio-form/radio-form.component.html b/src/app/shared/components/radio-form/radio-form.component.html index b120dde996625149911f38e0edf33c54482ad4e8..a80aa999f27b76ec4dc0a8769b54e5fb259839bd 100644 --- a/src/app/shared/components/radio-form/radio-form.component.html +++ b/src/app/shared/components/radio-form/radio-form.component.html @@ -1,10 +1,13 @@ -<div [fxLayout]="horizontal ? 'row' : 'column'" [fxLayoutGap]="horizontal ? '17px' : ''"> +<div [fxLayout]="horizontal ? 'row' : 'column'" [fxLayoutGap]="horizontal ? (layoutGap ? layoutGap : '17px') : ''"> + <div *ngIf="name" fxLayout="row" fxLayoutAlign=" center" [fxLayoutGap]="layoutGap ? layoutGap : '17px'" class="name"> + {{ name }} + </div> <button (click)="clicked(true)" [ngClass]="{ selected: selectedOption && selectedOption != null }" fxLayout="row" fxLayoutAlign=" center" - fxLayoutGap="17px" + [fxLayoutGap]="layoutGap ? layoutGap : '17px'" > <div class="checkmark"> <svg class="validate" aria-hidden="true"> @@ -18,7 +21,7 @@ [ngClass]="{ selected: !selectedOption && selectedOption != null }" fxLayout="row" fxLayoutAlign=" center" - fxLayoutGap="17px" + [fxLayoutGap]="layoutGap ? layoutGap : '17px'" > <div class="checkmark"> <svg class="validate" aria-hidden="true"> diff --git a/src/app/shared/components/radio-form/radio-form.component.scss b/src/app/shared/components/radio-form/radio-form.component.scss index 81d1c661ef98a4f03c06514ca36a64aaee5877e3..75ae988dc658ebdc9037985118b37b522f964fac 100644 --- a/src/app/shared/components/radio-form/radio-form.component.scss +++ b/src/app/shared/components/radio-form/radio-form.component.scss @@ -49,3 +49,14 @@ button { border-radius: 10px; } } + +.name { + width: 310px; + background: $grey-6; + border-radius: 4px; + padding: 0 16px; + font-size: $font-size-small; + outline: none; + border: none; + margin: 8px 0; +} diff --git a/src/app/shared/components/radio-form/radio-form.component.ts b/src/app/shared/components/radio-form/radio-form.component.ts index 84075f64f44017d666e4867ff124654b35ec927e..e0fbd385b17e1cad76a5aefa36b064914c57d8b1 100644 --- a/src/app/shared/components/radio-form/radio-form.component.ts +++ b/src/app/shared/components/radio-form/radio-form.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; @Component({ selector: 'app-radio-form', @@ -10,8 +11,20 @@ export class RadioFormComponent implements OnInit { @Input() public selectedOption: boolean; @Input() public horizontal: boolean; + @Input() public layoutGap: string; + @Input() public name: string; + @Input() events: Observable<Object>; @Output() selectedEvent: EventEmitter<boolean> = new EventEmitter<boolean>(); - ngOnInit(): void {} + + private eventsSubscription: Subscription; + + ngOnInit(): void { + if (this.events) this.eventsSubscription = this.events.subscribe((data: boolean) => (this.selectedOption = data)); + } + + ngOnDestroy() { + if (this.eventsSubscription) this.eventsSubscription.unsubscribe(); + } public clicked(bool: boolean): void { this.selectedOption = bool;