From a0c1b4a72eefdb621cf4fa8ef735fd3d1059e55a Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Thu, 26 Sep 2024 12:49:02 +0200 Subject: [PATCH 1/4] feat: add taxonomy tab + status badges to species page --- .../core/gn_synthese/synthese_config.py | 4 + backend/geonature/utils/config_schema.py | 6 + .../GN2CommonModule/form/data-form.service.ts | 4 + .../form/taxonomy/taxonomy.component.ts | 2 +- .../synthese-info-obs.component.html | 83 +-- .../taxonomy/taxonomy.component.html | 86 ++++ .../taxonomy/taxonomy.component.scss | 23 + .../taxonomy/taxonomy.component.ts | 15 + .../synthese-shared.module.ts | 15 +- .../modal-info-obs.component.html | 477 ------------------ .../modal-info-obs.component.ts | 81 --- .../taxon-sheet/infos/infos.component.html | 16 +- .../taxon-sheet/infos/infos.component.scss | 16 +- .../taxon-sheet/infos/infos.component.ts | 5 +- .../infos/status/status.component.html | 13 + .../infos/status/status.component.scss | 37 ++ .../infos/status/status.component.ts | 115 +++++ .../infos/taxonomy/taxonomy.component.html | 8 + .../infos/taxonomy/taxonomy.component.scss | 12 + .../infos/taxonomy/taxonomy.component.ts | 14 + .../tab-taxonomy/tab-taxonomy.component.html | 3 + .../tab-taxonomy/tab-taxonomy.component.scss | 0 .../tab-taxonomy/tab-taxonomy.component.ts | 24 + .../taxon-sheet/taxon-sheet.route.service.ts | 12 + .../taxon-sheet/taxon-sheet.service.ts | 7 + 25 files changed, 405 insertions(+), 673 deletions(-) create mode 100644 frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.html create mode 100644 frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.scss create mode 100644 frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.ts delete mode 100644 frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.html delete mode 100644 frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.ts create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.html create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.scss create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.ts create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.html create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.scss create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.ts diff --git a/backend/geonature/core/gn_synthese/synthese_config.py b/backend/geonature/core/gn_synthese/synthese_config.py index af5dc42935..c862a2ce1c 100644 --- a/backend/geonature/core/gn_synthese/synthese_config.py +++ b/backend/geonature/core/gn_synthese/synthese_config.py @@ -134,6 +134,10 @@ class DefaultProfile: ] +class DefaultTaxonomy: + ENABLED = True + + class DefaultGeographicOverview: pass diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index fd420ffb08..76676465b8 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -23,6 +23,7 @@ DEFAULT_LIST_COLUMN, DefaultGeographicOverview, DefaultProfile, + DefaultTaxonomy, DefaultSpeciesSheet, ) from geonature.utils.env import GEONATURE_VERSION, BACKEND_DIR, ROOT_DIR @@ -277,6 +278,10 @@ class ExportObservationSchema(Schema): geojson_local_field = fields.String(load_default="geojson_local") +class SpeciesSheetTaxonomy(Schema): + ENABLED = fields.Boolean(load_default=DefaultTaxonomy.ENABLED) + + class SpeciesSheetProfile(Schema): ENABLED = fields.Boolean(load_default=DefaultProfile.ENABLED) LIST_INDICATORS = fields.List(fields.Dict, load_default=DefaultProfile.LIST_INDICATORS) @@ -295,6 +300,7 @@ class SpeciesSheet(Schema): load_default=SpeciesSheetGeographicOverview().load({}) ) # rename PROFILE = fields.Nested(SpeciesSheetProfile, load_default=SpeciesSheetProfile().load({})) + TAXONOMY = fields.Nested(SpeciesSheetTaxonomy, load_default=SpeciesSheetTaxonomy().load({})) class Synthese(Schema): diff --git a/frontend/src/app/GN2CommonModule/form/data-form.service.ts b/frontend/src/app/GN2CommonModule/form/data-form.service.ts index b1fe0e3637..5bc65d6205 100644 --- a/frontend/src/app/GN2CommonModule/form/data-form.service.ts +++ b/frontend/src/app/GN2CommonModule/form/data-form.service.ts @@ -152,6 +152,10 @@ export class DataFormService { }); } + fetchStatusSymbology() { + return this._http.get(`${this.config.API_TAXHUB}/bdc_statuts/status_symbologies`); + } + getTaxonAttributsAndMedia(cd_nom: number, id_attributs?: Array) { let query_string = new HttpParams(); if (id_attributs) { diff --git a/frontend/src/app/GN2CommonModule/form/taxonomy/taxonomy.component.ts b/frontend/src/app/GN2CommonModule/form/taxonomy/taxonomy.component.ts index cb722f31a4..0e81c08c48 100644 --- a/frontend/src/app/GN2CommonModule/form/taxonomy/taxonomy.component.ts +++ b/frontend/src/app/GN2CommonModule/form/taxonomy/taxonomy.component.ts @@ -35,7 +35,7 @@ export interface Taxon { nom_vern?: string; ordre?: string; phylum?: string; - statuts_protection?: any[]; + status?: any[]; synonymes?: any[]; } diff --git a/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html index 94ffedc4df..bd9a138364 100644 --- a/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html +++ b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html @@ -382,88 +382,7 @@

- - - - - - - - - - - - - -
- Groupe taxonomique - {{ selectedObsTaxonDetail?.classe }}
- Ordre - {{ selectedObsTaxonDetail?.ordre }}
- Famille - {{ selectedObsTaxonDetail?.famille }}
- -
Attribut(s) Taxonomique(s) locaux
- - - - - -
- {{ attr.label_attribut }} - {{ attr.valeur_attribut }}
- -
Statuts
- - - - - - - - - -
{{ status.value.display }}
-
    -
  • - - -
    - ({{ text.value.lb_adm_tr }} - {{ text.value.cd_sig }}) -
    - - Voir / Télécharger - - -
  • -
  • - - - {{ value.value.code_statut }} - - {{ value.value.label_statut }} - {{ value.value.rq_statut }} - -
  • -
-
-

Aucun

+
diff --git a/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.html b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.html new file mode 100644 index 0000000000..d3350df06b --- /dev/null +++ b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.html @@ -0,0 +1,86 @@ +
+
Classification
+ + + + + + + + + + + + + + + +
Groupe taxonomique{{ taxon?.classe }}
Ordre{{ taxon?.ordre }}
Famille + {{ taxon?.famille }} +
+ + + +
Statuts
+ + + + + + + + + +
{{ status.value.display }}
+
    +
  • + + +
    + ({{ text.value.lb_adm_tr }} - {{ text.value.cd_sig }}) +
    + + Voir / Télécharger + + +
  • +
  • + + + {{ value.value.code_statut }} + + {{ value.value.label_statut }} + {{ value.value.rq_statut }} + +
  • +
+
+

Aucun

+
diff --git a/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.scss b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.scss new file mode 100644 index 0000000000..2ad0c2cd00 --- /dev/null +++ b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.scss @@ -0,0 +1,23 @@ +.Taxonomy { + display: flex; + flex-flow: column; + justify-content: flex-start; + row-gap: 0.5rem; + + &__subtitle { + text-decoration: underline; + text-decoration-color: currentColor; + text-underline-offset: 0.4rem; + text-decoration-thickness: 2px; + } + .Classification { + &__name { + font-weight: bold; + white-space: nowrap; + } + &__value { + width: 100%; + padding-left: 1rem; + } + } +} diff --git a/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.ts b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.ts new file mode 100644 index 0000000000..52a5bc2c12 --- /dev/null +++ b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/taxonomy/taxonomy.component.ts @@ -0,0 +1,15 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; +import { CommonModule } from '@angular/common'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; + +@Component({ + selector: 'pnx-synthese-taxonomy', + templateUrl: 'taxonomy.component.html', + styleUrls: ['taxonomy.component.scss'], +}) +export class TaxonomyComponent { + @Input() + taxon: Taxon | null = null; + constructor() {} +} diff --git a/frontend/src/app/shared/syntheseSharedModule/synthese-shared.module.ts b/frontend/src/app/shared/syntheseSharedModule/synthese-shared.module.ts index 4065730a24..3cfbc72764 100644 --- a/frontend/src/app/shared/syntheseSharedModule/synthese-shared.module.ts +++ b/frontend/src/app/shared/syntheseSharedModule/synthese-shared.module.ts @@ -8,11 +8,22 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; import { SyntheseInfoObsComponent } from './synthese-info-obs/synthese-info-obs.component'; import { DiscussionCardComponent } from '../discussionCardModule/discussion-card.component'; import { AlertInfoComponent } from '../alertInfoModule/alert-Info.component'; +import { TaxonomyComponent } from './synthese-info-obs/taxonomy/taxonomy.component'; @NgModule({ imports: [CommonModule, GN2CommonModule, NgChartsModule, RouterModule, ClipboardModule], - exports: [SyntheseInfoObsComponent, DiscussionCardComponent, AlertInfoComponent], - declarations: [SyntheseInfoObsComponent, DiscussionCardComponent, AlertInfoComponent], + exports: [ + SyntheseInfoObsComponent, + DiscussionCardComponent, + AlertInfoComponent, + TaxonomyComponent, + ], + declarations: [ + SyntheseInfoObsComponent, + DiscussionCardComponent, + AlertInfoComponent, + TaxonomyComponent, + ], providers: [], }) export class SharedSyntheseModule {} diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.html b/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.html deleted file mode 100644 index 3326574484..0000000000 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.html +++ /dev/null @@ -1,477 +0,0 @@ -
- - - diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.ts b/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.ts deleted file mode 100644 index 563bb7037a..0000000000 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { SyntheseDataService } from '@geonature_common/form/synthese-form/synthese-data.service'; -import { DataFormService } from '@geonature_common/form/data-form.service'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MediaService } from '@geonature_common/service/media.service'; -import { finalize } from 'rxjs/operators'; -import { ConfigService } from '@geonature/services/config.service'; - -@Component({ - selector: 'pnx-synthese-modal-info-obs', - templateUrl: 'modal-info-obs.component.html', -}) -export class ModalInfoObsComponent implements OnInit { - @Input() syntheseObs: any; - public selectObsTaxonInfo; - public selectedObs; - public selectedObsTaxonDetail; - public formatedAreas = []; - public SYNTHESE_CONFIG = null; - public isLoading = false; - constructor( - private _gnDataService: DataFormService, - private _dataService: SyntheseDataService, - public activeModal: NgbActiveModal, - public mediaService: MediaService, - public config: ConfigService - ) { - this.SYNTHESE_CONFIG = this.config.SYNTHESE; - } - - ngOnInit() { - this.loadOneSyntheseReleve(this.syntheseObs); - } - - loadOneSyntheseReleve(syntheseObs) { - this.isLoading = true; - this._dataService - .getOneSyntheseObservation(syntheseObs.id) - .pipe( - finalize(() => { - this.isLoading = false; - }) - ) - .subscribe((data) => { - this.selectedObs = data; - this.selectedObs['municipalities'] = []; - this.selectedObs['other_areas'] = []; - this.selectedObs['actors'] = this.selectedObs['actors'].split('|'); - const areaDict = {}; - // for each area type we want all the areas: we build an dict of array - this.selectedObs.areas.forEach((area) => { - if (!areaDict[area.area_type.type_name]) { - areaDict[area.area_type.type_name] = [area]; - } else { - areaDict[area.area_type.type_name].push(area); - } - }); - // for angular tempate we need to convert it into a aray - for (let key in areaDict) { - this.formatedAreas.push({ area_type: key, areas: areaDict[key] }); - } - - // this.inpnMapUrl = `https://inpn.mnhn.fr/cartosvg/couchegeo/repartition/atlas/${ - // this.selectedObs['cd_nom'] - // }/fr_light_l93,fr_light_mer_l93,fr_lit_l93)`; - }); - this._gnDataService - .getTaxonAttributsAndMedia(syntheseObs.cd_nom, this.SYNTHESE_CONFIG.ID_ATTRIBUT_TAXHUB) - .subscribe((data) => { - this.selectObsTaxonInfo = data; - }); - - this._gnDataService.getTaxonInfo(syntheseObs.cd_nom).subscribe((data) => { - this.selectedObsTaxonDetail = data; - }); - } - - backToModule(url_source, id_pk_source) { - window.open(url_source + '/' + id_pk_source, '_blank'); - } -} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html b/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html index 9608bc40c2..b2d722d056 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html @@ -1,18 +1,6 @@
-
-
- {{ taxon?.nom_complet }} -
-
- {{ taxon?.nom_vern }} -
-
+ +
+
Statuts
+
+ + {{ status.badge }} + +
+
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss new file mode 100644 index 0000000000..c77e17171b --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss @@ -0,0 +1,37 @@ +// //////////////////////////////////////////////////////////////////////////// +// +// //////////////////////////////////////////////////////////////////////////// + +.Status { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: center; + $gap: 0.5rem; + column-gap: $gap; + line-height: 1; + + &__header { + vertical-align: middle; + } + &__badges { + display: flex; + flex-flow: row wrap; + width: 100%; + gap: $gap; + line-height: inherit; + --bgColor: #ffffff; // Default value + --textColor: #444; // Default value + .badge { + display: flex; + flex-flow: row nowrap; + white-space: nowrap; + gap: $gap / 2; + text-transform: uppercase; + font-weight: bold; + background-color: var(--bgColor); + border: 2px solid color-mix(in srgb, var(--bgColor) 80%, black); + color: var(--textColor); + } + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts new file mode 100644 index 0000000000..7638b3c0ec --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts @@ -0,0 +1,115 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, OnInit } from '@angular/core'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; +import { TaxonSheetService } from '../../taxon-sheet.service'; + +interface Status { + badge: string; + tooltip: string; + symbologyAsCSS: string; +} + +function computeContrastColor(backgroundColor: string) { + // Convertir la couleur en un format RGB + const r = parseInt(backgroundColor.slice(1, 3), 16); + const g = parseInt(backgroundColor.slice(3, 5), 16); + const b = parseInt(backgroundColor.slice(5, 7), 16); + + // Calculer la luminosité + const luminance = 0.299 * r + 0.587 * g + 0.114 * b; + + // Retourner une couleur claire ou foncée selon la luminosité + return luminance < 128 ? '#ffffff' : '#444'; +} + +function colorToCSS(color: string) { + return `--bgColor: ${color}; --textColor: ${computeContrastColor(color)};`; +} + +@Component({ + standalone: true, + selector: 'status', + templateUrl: 'status.component.html', + styleUrls: ['status.component.scss'], + imports: [CommonModule, GN2CommonModule], +}) +export class StatusComponent implements OnInit { + _taxon: Taxon | null; + _symbology: Array<{ + types: Array; + values: Record< + string, + { + symbologyAsCSSStyle: string; + } + >; + }>; + status: Array = []; + + constructor(private _tss: TaxonSheetService) {} + + ngOnInit() { + this._tss.symbology.subscribe((symbology) => { + this._symbology = []; + if (!symbology || !symbology.symbologies) { + return; + } + for (const symbologyItem of symbology.symbologies) { + const values = {}; + for (const key of Object.keys(symbologyItem.values)) { + values[key] = { + symbologyAsCSSStyle: colorToCSS(symbologyItem.values[key].color), + }; + } + this._symbology.push({ + types: symbologyItem.types, + values: values, + }); + } + this.computeStatus(); + }); + this._tss.fetchStatusSymbology(); + } + + _getSymbologyAsCSSStyle(type: string, value: string): string { + if (!this._symbology) { + return ''; + } + const symbologieItem = this._symbology.find((item) => item.types.includes(type)); + if (!symbologieItem) { + return ''; + } + + return symbologieItem.values[value]?.symbologyAsCSSStyle ?? ''; + } + + @Input() + set taxon(taxon: Taxon | null) { + this._taxon = taxon; + this.computeStatus(); + } + + computeStatus() { + this.status = []; + if (!this._taxon) { + return; + } + + for (const status of Object.values(this._taxon.status)) { + for (const text of Object.values(status.text)) { + for (const value of Object.values(text.values)) { + const badgeValue = ['true', 'false'].includes(value.code_statut) + ? `${status.cd_type_statut}` + : `${status.cd_type_statut}: ${value.code_statut}`; + + this.status.push({ + badge: badgeValue, + tooltip: `${status.cd_type_statut} : ${value.display} - ${text.full_citation}`, + symbologyAsCSS: this._getSymbologyAsCSSStyle(status.cd_type_statut, value.code_statut), + }); + } + } + } + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.html b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.html new file mode 100644 index 0000000000..c74e9f299b --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.html @@ -0,0 +1,8 @@ +
+
+ {{ taxon?.nom_complet }} +
+
+ {{ taxon?.nom_vern }} +
+
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.scss new file mode 100644 index 0000000000..4ac109a575 --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.scss @@ -0,0 +1,12 @@ +.Taxonomy { + display: flex; + flex-flow: column; + &__completeName { + font-weight: lighter; + font-style: italic; + } + &__vernacularName { + font-size: 1.5rem; + font-weight: bold; + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.ts new file mode 100644 index 0000000000..27e3cbb63c --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +@Component({ + standalone: true, + selector: 'taxonomy', + templateUrl: 'taxonomy.component.html', + styleUrls: ['taxonomy.component.scss'], + imports: [CommonModule], +}) +export class TaxonomyComponent { + @Input() + taxon: Taxon | null = null; +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.html b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.html new file mode 100644 index 0000000000..94da0dd149 --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.ts new file mode 100644 index 0000000000..e5767049de --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; +import { CommonModule } from '@angular/common'; +import { TaxonSheetService } from '../taxon-sheet.service'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +import { SharedSyntheseModule } from '@geonature/shared/syntheseSharedModule/synthese-shared.module'; + +@Component({ + standalone: true, + selector: 'tab-taxonomy', + templateUrl: 'tab-taxonomy.component.html', + styleUrls: ['tab-taxonomy.component.scss'], + imports: [GN2CommonModule, CommonModule, SharedSyntheseModule], +}) +export class TabTaxonomyComponent implements OnInit { + taxon: Taxon | null = null; + constructor(private _tss: TaxonSheetService) {} + + ngOnInit() { + this._tss.taxon.subscribe((taxon: Taxon | null) => { + this.taxon = taxon; + }); + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts index 5f73b07b46..c584288d27 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts @@ -11,6 +11,7 @@ import { ConfigService } from '@geonature/services/config.service'; import { Observable } from 'rxjs'; import { TabGeographicOverviewComponent } from './tab-geographic-overview/tab-geographic-overview.component'; import { TabProfileComponent } from './tab-profile/tab-profile.component'; +import { TabTaxonomyComponent } from './tab-taxonomy/tab-taxonomy.component'; interface Tab { label: string; @@ -28,6 +29,13 @@ const ROUTE_GEOGRAPHIC_OVERVIEW: Tab = { export const ROUTE_MANDATORY = ROUTE_GEOGRAPHIC_OVERVIEW; +const ROUTE_TAXONOMY: Tab = { + label: 'Taxonomie', + path: 'taxonomy', + configEntry: 'TAXONOMY', + component: TabTaxonomyComponent, +}; + const ROUTE_PROFILE: Tab = { label: 'Profil', path: 'profile', @@ -37,6 +45,7 @@ const ROUTE_PROFILE: Tab = { export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ ROUTE_GEOGRAPHIC_OVERVIEW, + ROUTE_TAXONOMY, ROUTE_PROFILE, ]; @@ -53,6 +62,9 @@ export class RouteService implements CanActivateChild { this.TAB_LINKS.push(ROUTE_MANDATORY); if (this._config && this._config['SYNTHESE'] && this._config['SYNTHESE']['SPECIES_SHEET']) { const config = this._config['SYNTHESE']['SPECIES_SHEET']; + if (config['TAXONOMY'] && config['TAXONOMY']['ENABLED']) { + this.TAB_LINKS.push(ROUTE_TAXONOMY); + } if (config['PROFILE'] && config['PROFILE']['ENABLED']) { this.TAB_LINKS.push(ROUTE_PROFILE); } diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts index 097afe39dc..e5dcc94917 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts @@ -6,9 +6,16 @@ import { BehaviorSubject } from 'rxjs'; @Injectable() export class TaxonSheetService { taxon: BehaviorSubject = new BehaviorSubject(null); + symbology: BehaviorSubject = new BehaviorSubject(null); constructor(private _ds: DataFormService) {} + fetchStatusSymbology() { + this._ds.fetchStatusSymbology().subscribe((symbology) => { + this.symbology.next(symbology); + }); + } + updateTaxonByCdRef(cd_ref: number) { const taxon = this.taxon.getValue(); if (taxon && taxon.cd_ref == cd_ref) { From ed91512956c02f585f3364401aba9363d64f2bbe Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Thu, 26 Sep 2024 17:50:14 +0200 Subject: [PATCH 2/4] refact: move badge to a separate component --- .../app/GN2CommonModule/GN2Common.module.ts | 3 + .../others/badge/badge.component.html | 7 ++ .../others/badge/badge.component.scss | 15 ++++ .../others/badge/badge.component.ts | 68 +++++++++++++++++++ .../infos/status/status.component.html | 14 ++-- .../infos/status/status.component.scss | 13 ---- .../infos/status/status.component.ts | 58 ++++------------ 7 files changed, 114 insertions(+), 64 deletions(-) create mode 100644 frontend/src/app/GN2CommonModule/others/badge/badge.component.html create mode 100644 frontend/src/app/GN2CommonModule/others/badge/badge.component.scss create mode 100644 frontend/src/app/GN2CommonModule/others/badge/badge.component.ts diff --git a/frontend/src/app/GN2CommonModule/GN2Common.module.ts b/frontend/src/app/GN2CommonModule/GN2Common.module.ts index 6cd8392c2d..3eb4649c98 100644 --- a/frontend/src/app/GN2CommonModule/GN2Common.module.ts +++ b/frontend/src/app/GN2CommonModule/GN2Common.module.ts @@ -40,6 +40,7 @@ import { AreasIntersectedComponent } from './form/areas-intersected/areas-inters import { AutoCompleteComponent } from '@geonature_common/form/autocomplete/autocomplete.component'; import { ConfirmationDialog } from '@geonature_common/others/modal-confirmation/confirmation.dialog'; import { DatalistComponent } from '@geonature_common/form/datalist/datalist.component'; +import { BadgeComponent } from '@geonature_common/others/badge/badge.component'; import { BreadcrumbsComponent } from '@geonature_common/others/breadcrumbs/breadcrumbs.component'; import { DatasetsComponent } from './form/datasets/datasets.component'; import { DateComponent } from './form/date/date.component'; @@ -141,6 +142,7 @@ import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component'; AreasComponent, NomenclatureComponent, ObserversComponent, + BadgeComponent, BreadcrumbsComponent, DateComponent, TaxonomyComponent, @@ -208,6 +210,7 @@ import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component'; AcquisitionFrameworksComponent, AreasComponent, MunicipalitiesComponent, + BadgeComponent, BreadcrumbsComponent, DynamicFormComponent, NomenclatureComponent, diff --git a/frontend/src/app/GN2CommonModule/others/badge/badge.component.html b/frontend/src/app/GN2CommonModule/others/badge/badge.component.html new file mode 100644 index 0000000000..7d5af3e2c3 --- /dev/null +++ b/frontend/src/app/GN2CommonModule/others/badge/badge.component.html @@ -0,0 +1,7 @@ + + {{ text }} + diff --git a/frontend/src/app/GN2CommonModule/others/badge/badge.component.scss b/frontend/src/app/GN2CommonModule/others/badge/badge.component.scss new file mode 100644 index 0000000000..2a48c04202 --- /dev/null +++ b/frontend/src/app/GN2CommonModule/others/badge/badge.component.scss @@ -0,0 +1,15 @@ +.badge { + --bgColor: #ffffff; // Default value + --textColor: #444; // Default value + + display: flex; + flex-flow: row nowrap; + white-space: nowrap; + gap: 0.25rem; + text-transform: uppercase; + font-weight: bold; + + background-color: var(--bgColor); + border: 2px solid color-mix(in srgb, var(--bgColor) 80%, black); + color: var(--textColor); +} diff --git a/frontend/src/app/GN2CommonModule/others/badge/badge.component.ts b/frontend/src/app/GN2CommonModule/others/badge/badge.component.ts new file mode 100644 index 0000000000..d395ce19e1 --- /dev/null +++ b/frontend/src/app/GN2CommonModule/others/badge/badge.component.ts @@ -0,0 +1,68 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, OnInit } from '@angular/core'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; + +// //////////////////////////////////////////////////////////////////////////// +// helper method +// //////////////////////////////////////////////////////////////////////////// + +function isHexadecimalColor(color: string) { + return /^#[0-9A-F]{6}$/i.test(color); +} + +function computeContrastColor(backgroundColor: string) { + // Convertir la couleur en un format RGB + const r = parseInt(backgroundColor.slice(1, 3), 16); + const g = parseInt(backgroundColor.slice(3, 5), 16); + const b = parseInt(backgroundColor.slice(5, 7), 16); + + // Calculer la luminosité + const luminance = 0.299 * r + 0.587 * g + 0.114 * b; + + // Retourner une couleur claire ou foncée selon la luminosité + return luminance < 128 ? '#ffffff' : '#444'; +} + +function colorToCSS(color: string) { + return `--bgColor: ${color}; --textColor: ${computeContrastColor(color)};`; +} + +// //////////////////////////////////////////////////////////////////////////// +// Badge parameters +// //////////////////////////////////////////////////////////////////////////// + +export interface BadgeSymbology { + color?: string; +} + +// //////////////////////////////////////////////////////////////////////////// +// helper method +// //////////////////////////////////////////////////////////////////////////// + +@Component({ + selector: 'gn-badge', + templateUrl: 'badge.component.html', + styleUrls: ['badge.component.scss'], +}) +export class BadgeComponent { + @Input() + text: string; + + @Input() + tooltip: string; + + symbologyAsCSS: string; + + @Input() + set symbology(symbology: BadgeSymbology | null) { + this.symbologyAsCSS = ''; + if (!symbology) { + return; + } + if (!isHexadecimalColor(symbology.color)) { + console.warn(`[badge] ${symbology.color} is not a valid hexadecimal color`); + return; + } + this.symbologyAsCSS = colorToCSS(symbology.color); + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html index 3623d6205d..f7a4de80b6 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html @@ -1,13 +1,11 @@
Statuts
- - {{ status.badge }} - +
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss index c77e17171b..871f7ecef0 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss @@ -20,18 +20,5 @@ width: 100%; gap: $gap; line-height: inherit; - --bgColor: #ffffff; // Default value - --textColor: #444; // Default value - .badge { - display: flex; - flex-flow: row nowrap; - white-space: nowrap; - gap: $gap / 2; - text-transform: uppercase; - font-weight: bold; - background-color: var(--bgColor); - border: 2px solid color-mix(in srgb, var(--bgColor) 80%, black); - color: var(--textColor); - } } } diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts index 7638b3c0ec..f8ca16252a 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts @@ -3,30 +3,13 @@ import { Component, Input, OnInit } from '@angular/core'; import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; import { GN2CommonModule } from '@geonature_common/GN2Common.module'; import { TaxonSheetService } from '../../taxon-sheet.service'; +import { BadgeSymbology } from '@geonature_common/others/badge/badge.component'; interface Status { badge: string; tooltip: string; - symbologyAsCSS: string; + symbology: BadgeSymbology | null; } - -function computeContrastColor(backgroundColor: string) { - // Convertir la couleur en un format RGB - const r = parseInt(backgroundColor.slice(1, 3), 16); - const g = parseInt(backgroundColor.slice(3, 5), 16); - const b = parseInt(backgroundColor.slice(5, 7), 16); - - // Calculer la luminosité - const luminance = 0.299 * r + 0.587 * g + 0.114 * b; - - // Retourner une couleur claire ou foncée selon la luminosité - return luminance < 128 ? '#ffffff' : '#444'; -} - -function colorToCSS(color: string) { - return `--bgColor: ${color}; --textColor: ${computeContrastColor(color)};`; -} - @Component({ standalone: true, selector: 'status', @@ -38,12 +21,7 @@ export class StatusComponent implements OnInit { _taxon: Taxon | null; _symbology: Array<{ types: Array; - values: Record< - string, - { - symbologyAsCSSStyle: string; - } - >; + values: Record; }>; status: Array = []; @@ -55,33 +33,27 @@ export class StatusComponent implements OnInit { if (!symbology || !symbology.symbologies) { return; } - for (const symbologyItem of symbology.symbologies) { - const values = {}; - for (const key of Object.keys(symbologyItem.values)) { - values[key] = { - symbologyAsCSSStyle: colorToCSS(symbologyItem.values[key].color), - }; - } - this._symbology.push({ - types: symbologyItem.types, - values: values, - }); - } + this._symbology = symbology.symbologies; + this.computeStatus(); }); this._tss.fetchStatusSymbology(); } - _getSymbologyAsCSSStyle(type: string, value: string): string { + _getSymbologyAsBadgeSymbology(type: string, value: string): BadgeSymbology | null { if (!this._symbology) { - return ''; + return null; } const symbologieItem = this._symbology.find((item) => item.types.includes(type)); if (!symbologieItem) { - return ''; + return null; } - - return symbologieItem.values[value]?.symbologyAsCSSStyle ?? ''; + if (!('color' in symbologieItem.values[value])) { + return null; + } + return { + color: symbologieItem.values[value].color, + }; } @Input() @@ -106,7 +78,7 @@ export class StatusComponent implements OnInit { this.status.push({ badge: badgeValue, tooltip: `${status.cd_type_statut} : ${value.display} - ${text.full_citation}`, - symbologyAsCSS: this._getSymbologyAsCSSStyle(status.cd_type_statut, value.code_statut), + symbology: this._getSymbologyAsBadgeSymbology(status.cd_type_statut, value.code_statut), }); } } From 4c31a70adde1acbb25a41bb811074e892223e2d5 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Fri, 27 Sep 2024 13:13:32 +0200 Subject: [PATCH 3/4] feat: add status-badges component in GN2CommonModule --- .../app/GN2CommonModule/GN2Common.module.ts | 3 + .../others/badge/badge.component.ts | 4 +- .../status-badges.component.html | 8 ++ .../status-badges.component.scss | 7 ++ .../status-badges/status-badges.component.ts | 83 +++++++++++++++++++ .../infos/status/status.component.html | 7 +- .../infos/status/status.component.scss | 10 +-- .../infos/status/status.component.ts | 77 +---------------- .../taxon-sheet/taxon-sheet.service.ts | 7 -- 9 files changed, 108 insertions(+), 98 deletions(-) create mode 100644 frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.html create mode 100644 frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.scss create mode 100644 frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.ts diff --git a/frontend/src/app/GN2CommonModule/GN2Common.module.ts b/frontend/src/app/GN2CommonModule/GN2Common.module.ts index 3eb4649c98..a3d13c0a1c 100644 --- a/frontend/src/app/GN2CommonModule/GN2Common.module.ts +++ b/frontend/src/app/GN2CommonModule/GN2Common.module.ts @@ -75,6 +75,7 @@ import { ObserversTextComponent } from '@geonature_common/form/observers-text/ob import { PeriodComponent } from '@geonature_common/form/date/period.component'; import { PlacesComponent } from './map/places/places.component'; import { PlacesListComponent } from './map/placesList/placesList.component'; +import { StatusBadgesComponent } from '@geonature_common/others/status-badges/status-badges.component'; import { SyntheseSearchComponent } from '@geonature_common/form/synthese-form/synthese-form.component'; import { TaxaComponent } from '@geonature_common/form/taxa/taxa.component'; import { TaxonAdvancedModalComponent } from '@geonature_common/form/synthese-form/advanced-form/synthese-advanced-form-component'; @@ -187,6 +188,7 @@ import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component'; SafeHtmlPipe, SyntheseSearchComponent, SafeStripHtmlPipe, + StatusBadgesComponent, StripHtmlPipe, TaxaComponent, TaxonAdvancedModalComponent, @@ -291,6 +293,7 @@ import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component'; ReactiveFormsModule, ReadablePropertiePipe, SafeHtmlPipe, + StatusBadgesComponent, TaxaComponent, TaxonAdvancedModalComponent, TaxonomyComponent, diff --git a/frontend/src/app/GN2CommonModule/others/badge/badge.component.ts b/frontend/src/app/GN2CommonModule/others/badge/badge.component.ts index d395ce19e1..1138911743 100644 --- a/frontend/src/app/GN2CommonModule/others/badge/badge.component.ts +++ b/frontend/src/app/GN2CommonModule/others/badge/badge.component.ts @@ -1,6 +1,4 @@ -import { CommonModule } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; -import { GN2CommonModule } from '@geonature_common/GN2Common.module'; +import { Component, Input } from '@angular/core'; // //////////////////////////////////////////////////////////////////////////// // helper method diff --git a/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.html b/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.html new file mode 100644 index 0000000000..4221d6f4a6 --- /dev/null +++ b/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.scss b/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.scss new file mode 100644 index 0000000000..9b42867b35 --- /dev/null +++ b/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.scss @@ -0,0 +1,7 @@ +.StatusBadges { + display: flex; + flex-flow: row wrap; + width: 100%; + gap: 0.5rem; + line-height: inherit; +} diff --git a/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.ts b/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.ts new file mode 100644 index 0000000000..d9c6ec47c5 --- /dev/null +++ b/frontend/src/app/GN2CommonModule/others/status-badges/status-badges.component.ts @@ -0,0 +1,83 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TaxonSheetService } from '@geonature/syntheseModule/taxon-sheet/taxon-sheet.service'; +import { DataFormService } from '@geonature_common/form/data-form.service'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +import { BadgeSymbology } from '@geonature_common/others/badge/badge.component'; + +interface Status { + badge: string; + tooltip: string; + symbology: BadgeSymbology | null; +} +@Component({ + selector: 'gn-status-badges', + templateUrl: 'status-badges.component.html', + styleUrls: ['status-badges.component.scss'], +}) +export class StatusBadgesComponent implements OnInit { + _taxon: Taxon | null; + _symbology: Array<{ + types: Array; + values: Record; + }>; + status: Array = []; + + constructor(private _ds: DataFormService) {} + + ngOnInit() { + this._ds.fetchStatusSymbology().subscribe((symbology) => { + this._symbology = []; + if (!symbology || !symbology.symbologies) { + return; + } + this._symbology = symbology.symbologies; + + this.computeStatus(); + }); + } + + _getSymbologyAsBadgeSymbology(type: string, value: string): BadgeSymbology | null { + if (!this._symbology) { + return null; + } + const symbologieItem = this._symbology.find((item) => item.types.includes(type)); + if (!symbologieItem) { + return null; + } + if (!('color' in symbologieItem.values[value])) { + return null; + } + return { + color: symbologieItem.values[value].color, + }; + } + + @Input() + set taxon(taxon: Taxon | null) { + this._taxon = taxon; + this.computeStatus(); + } + + computeStatus() { + this.status = []; + if (!this._taxon) { + return; + } + + for (const status of Object.values(this._taxon.status)) { + for (const text of Object.values(status.text)) { + for (const value of Object.values(text.values)) { + const badgeValue = ['true', 'false'].includes(value.code_statut) + ? `${status.cd_type_statut}` + : `${status.cd_type_statut}: ${value.code_statut}`; + + this.status.push({ + badge: badgeValue, + tooltip: `${status.cd_type_statut} : ${value.display} - ${text.full_citation}`, + symbology: this._getSymbologyAsBadgeSymbology(status.cd_type_statut, value.code_statut), + }); + } + } + } + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html index f7a4de80b6..75d914d880 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.html @@ -1,11 +1,6 @@
Statuts
- +
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss index 871f7ecef0..8d2326bd7c 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss @@ -7,18 +7,10 @@ flex-flow: row nowrap; justify-content: flex-start; align-items: center; - $gap: 0.5rem; - column-gap: $gap; + column-gap: 0.5rem; line-height: 1; &__header { vertical-align: middle; } - &__badges { - display: flex; - flex-flow: row wrap; - width: 100%; - gap: $gap; - line-height: inherit; - } } diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts index f8ca16252a..0cc562647b 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts @@ -1,15 +1,8 @@ import { CommonModule } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; import { GN2CommonModule } from '@geonature_common/GN2Common.module'; -import { TaxonSheetService } from '../../taxon-sheet.service'; -import { BadgeSymbology } from '@geonature_common/others/badge/badge.component'; -interface Status { - badge: string; - tooltip: string; - symbology: BadgeSymbology | null; -} @Component({ standalone: true, selector: 'status', @@ -17,71 +10,9 @@ interface Status { styleUrls: ['status.component.scss'], imports: [CommonModule, GN2CommonModule], }) -export class StatusComponent implements OnInit { - _taxon: Taxon | null; - _symbology: Array<{ - types: Array; - values: Record; - }>; - status: Array = []; - - constructor(private _tss: TaxonSheetService) {} - - ngOnInit() { - this._tss.symbology.subscribe((symbology) => { - this._symbology = []; - if (!symbology || !symbology.symbologies) { - return; - } - this._symbology = symbology.symbologies; - - this.computeStatus(); - }); - this._tss.fetchStatusSymbology(); - } - - _getSymbologyAsBadgeSymbology(type: string, value: string): BadgeSymbology | null { - if (!this._symbology) { - return null; - } - const symbologieItem = this._symbology.find((item) => item.types.includes(type)); - if (!symbologieItem) { - return null; - } - if (!('color' in symbologieItem.values[value])) { - return null; - } - return { - color: symbologieItem.values[value].color, - }; - } +export class StatusComponent { + constructor() {} @Input() - set taxon(taxon: Taxon | null) { - this._taxon = taxon; - this.computeStatus(); - } - - computeStatus() { - this.status = []; - if (!this._taxon) { - return; - } - - for (const status of Object.values(this._taxon.status)) { - for (const text of Object.values(status.text)) { - for (const value of Object.values(text.values)) { - const badgeValue = ['true', 'false'].includes(value.code_statut) - ? `${status.cd_type_statut}` - : `${status.cd_type_statut}: ${value.code_statut}`; - - this.status.push({ - badge: badgeValue, - tooltip: `${status.cd_type_statut} : ${value.display} - ${text.full_citation}`, - symbology: this._getSymbologyAsBadgeSymbology(status.cd_type_statut, value.code_statut), - }); - } - } - } - } + taxon: Taxon | null; } diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts index e5dcc94917..097afe39dc 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.service.ts @@ -6,16 +6,9 @@ import { BehaviorSubject } from 'rxjs'; @Injectable() export class TaxonSheetService { taxon: BehaviorSubject = new BehaviorSubject(null); - symbology: BehaviorSubject = new BehaviorSubject(null); constructor(private _ds: DataFormService) {} - fetchStatusSymbology() { - this._ds.fetchStatusSymbology().subscribe((symbology) => { - this.symbology.next(symbology); - }); - } - updateTaxonByCdRef(cd_ref: number) { const taxon = this.taxon.getValue(); if (taxon && taxon.cd_ref == cd_ref) { From e97914b47e61fdc3f13cb3dc656e49c826884072 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Fri, 27 Sep 2024 12:51:05 +0200 Subject: [PATCH 4/4] feat: adjust species-sheet config structure --- .../core/gn_synthese/synthese_config.py | 78 ------------------- backend/geonature/utils/config_schema.py | 31 +------- config/default_config.toml.example | 15 +++- .../src/app/syntheseModule/synthese.module.ts | 3 +- .../taxon-sheet/indicator/indicator.ts | 35 +++++---- .../tab-profile/tab-profile.component.ts | 53 +++++++++---- .../taxon-sheet/taxon-sheet.component.ts | 58 ++++++++++---- .../taxon-sheet/taxon-sheet.route.service.ts | 72 ++++++----------- 8 files changed, 136 insertions(+), 209 deletions(-) diff --git a/backend/geonature/core/gn_synthese/synthese_config.py b/backend/geonature/core/gn_synthese/synthese_config.py index c862a2ce1c..a3129d8d1b 100644 --- a/backend/geonature/core/gn_synthese/synthese_config.py +++ b/backend/geonature/core/gn_synthese/synthese_config.py @@ -100,81 +100,3 @@ {"prop": "dataset_name", "name": "JDD", "max_width": 200}, {"prop": "observers", "name": "observateur", "max_width": 200}, ] - - -class DefaultProfile: - ENABLED = True - ## DEFAULT PROFILE INDICATORS - LIST_INDICATORS = [ - { - "name": "observation(s) valide(s)", - "matIcon": "search", - "field": "count_valid_data", - "type": "number", - }, - { - "name": "Première observation", - "matIcon": "schedule", - "field": "first_valid_data", - "type": "date", - }, - { - "name": "Dernière observation", - "matIcon": "search", - "field": "last_valid_data", - "type": "date", - }, - { - "name": "Plage d'altitude(s)", - "matIcon": "terrain", - "field": ["altitude_min", "altitude_max"], - "unit": "m", - "type": "number", - }, - ] - - -class DefaultTaxonomy: - ENABLED = True - - -class DefaultGeographicOverview: - pass - - -class DefaultSpeciesSheet: - ## DEFAULT SPECIES SHEET INDICATORS - LIST_INDICATORS = [ - { - "name": "observation(s)", - "matIcon": "search", - "field": "observation_count", - "type": "number", - }, - { - "name": "observateur(s)", - "matIcon": "people", - "field": "observer_count", - "type": "number", - }, - { - "name": "commune(s)", - "matIcon": "location_on", - "field": "area_count", - "type": "number", - }, - { - "name": "Plage d'altitude(s)", - "matIcon": "terrain", - "unit": "m", - "type": "number", - "field": ["altitude_min", "altitude_max"], - }, - { - "name": "Plage d'observation(s)", - "matIcon": "date_range", - "type": "date", - "field": ["date_min", "date_max"], - "separator": "-", - }, - ] diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 76676465b8..021f98da7b 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -18,14 +18,7 @@ ) from marshmallow.validate import OneOf, Regexp, Email, Length -from geonature.core.gn_synthese.synthese_config import ( - DEFAULT_EXPORT_COLUMNS, - DEFAULT_LIST_COLUMN, - DefaultGeographicOverview, - DefaultProfile, - DefaultTaxonomy, - DefaultSpeciesSheet, -) +from geonature.core.gn_synthese.synthese_config import DEFAULT_EXPORT_COLUMNS, DEFAULT_LIST_COLUMN from geonature.utils.env import GEONATURE_VERSION, BACKEND_DIR, ROOT_DIR from geonature.utils.module import iter_modules_dist, get_module_config from geonature.utils.utilsmails import clean_recipients @@ -278,29 +271,11 @@ class ExportObservationSchema(Schema): geojson_local_field = fields.String(load_default="geojson_local") -class SpeciesSheetTaxonomy(Schema): - ENABLED = fields.Boolean(load_default=DefaultTaxonomy.ENABLED) - - -class SpeciesSheetProfile(Schema): - ENABLED = fields.Boolean(load_default=DefaultProfile.ENABLED) - LIST_INDICATORS = fields.List(fields.Dict, load_default=DefaultProfile.LIST_INDICATORS) - - -class SpeciesSheetGeographicOverview(Schema): - pass - - class SpeciesSheet(Schema): # -------------------------------------------------------------------- # SYNTHESE - SPECIES_SHEET - LIST_INDICATORS = fields.List(fields.Dict, load_default=DefaultSpeciesSheet.LIST_INDICATORS) - - GEOGRAPHIC_OVERVIEW = fields.Dict( - load_default=SpeciesSheetGeographicOverview().load({}) - ) # rename - PROFILE = fields.Nested(SpeciesSheetProfile, load_default=SpeciesSheetProfile().load({})) - TAXONOMY = fields.Nested(SpeciesSheetTaxonomy, load_default=SpeciesSheetTaxonomy().load({})) + ENABLE_PROFILE = fields.Boolean(load_default=True) + ENABLE_TAXONOMY = fields.Boolean(load_default=True) class Synthese(Schema): diff --git a/config/default_config.toml.example b/config/default_config.toml.example index 05fa5a6a83..36f75ac331 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -307,9 +307,9 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Vues d'export personnalisées EXPORT_OBSERVATIONS_CUSTOM_VIEWS = [ { - label = "format personnalisé", + label = "format personnalisé", view_name = "schema_name.view_name" - } + } ] # Noms des colonnes obligatoires de la vue ``gn_synthese.v_metadata_for_export`` EXPORT_METADATA_ID_DATASET_COL = "jdd_id" @@ -441,6 +441,13 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Seulement les données de présence cd_nomenclature_observation_status = ['Pr'] + [SYNTHESE.SPECIES_SHEET] + # Options dédiées à la fiche espèce + #Permet d'activer ou non la section "Profile" de la fiche espèce + ENABLE_PROFILE = True + # Permet d'activer ou non la section "Taxonomy" de la fiche espèce + ENABLE_TAXONOMY = True + # Gestion des demandes d'inscription [ACCOUNT_MANAGEMENT] # Activer l'affichage du lien vers le formulaire d'inscription @@ -601,7 +608,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" [[AUTHENTICATION.PROVIDERS]] module="pypnusershub.auth.providers.default.DefaultConfiguration" id_provider="local_provider" - + [[AUTHENTICATION.PROVIDERS]] module="pypnusershub.auth.providers.openid_provider.OpenIDConnectProvider" id_provider = "google" @@ -609,4 +616,4 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" label = "Google" ISSUER = "https://accounts.google.com/" CLIENT_ID = "ID" # API key - CLIENT_SECRET = "SECRET" # API Secret \ No newline at end of file + CLIENT_SECRET = "SECRET" # API Secret diff --git a/frontend/src/app/syntheseModule/synthese.module.ts b/frontend/src/app/syntheseModule/synthese.module.ts index 993b8f5cdd..a8310b63f7 100644 --- a/frontend/src/app/syntheseModule/synthese.module.ts +++ b/frontend/src/app/syntheseModule/synthese.module.ts @@ -19,7 +19,6 @@ import { TaxonSheetComponent } from './taxon-sheet/taxon-sheet.component'; import { RouteService, ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES, - ROUTE_MANDATORY, } from './taxon-sheet/taxon-sheet.route.service'; const routes: Routes = [ @@ -32,7 +31,7 @@ const routes: Routes = [ children: [ { path: '', - redirectTo: ROUTE_MANDATORY.path, + redirectTo: ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES[0].path, pathMatch: 'prefix', }, ...ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES.map((tab) => { diff --git a/frontend/src/app/syntheseModule/taxon-sheet/indicator/indicator.ts b/frontend/src/app/syntheseModule/taxon-sheet/indicator/indicator.ts index 728a454f50..441fb02c80 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/indicator/indicator.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/indicator/indicator.ts @@ -1,16 +1,17 @@ -type IndicatorRawType = 'number' | 'string' | 'date'; -export interface IndicatorRaw { +export interface Indicator { name: string; matIcon: string; - field: string | Array; - unit?: string; - type: IndicatorRawType; + value: string | null; } -export interface Indicator { +type IndicatorRawType = 'number' | 'string' | 'date'; +export interface IndicatorDescription { name: string; matIcon: string; - value: string | null; + field: string | Array; + unit?: string; + separator?: string; + type: IndicatorRawType; } type Stats = Record; @@ -18,7 +19,7 @@ type Stats = Record; const DEFAULT_VALUE = '-'; const DEFAULT_SEPARATOR = '-'; -function getValue(field: string, indicatorConfig: IndicatorRaw, stats?: Stats) { +function getValue(field: string, indicatorConfig: IndicatorDescription, stats?: Stats) { if (stats && stats[field]) { let valueAsString = ''; switch (indicatorConfig.type) { @@ -37,24 +38,24 @@ function getValue(field: string, indicatorConfig: IndicatorRaw, stats?: Stats) { return DEFAULT_VALUE; } -export function computeIndicatorFromConfig( - indicatorConfig: IndicatorRaw, +export function computeIndicatorFromDecsription( + indicatorDescription: IndicatorDescription, stats?: Stats ): Indicator { let value = DEFAULT_VALUE; if (stats) { - if (Array.isArray(indicatorConfig.field)) { - const separator = indicatorConfig['separator'] ?? DEFAULT_SEPARATOR; - value = indicatorConfig.field - .map((field) => getValue(field, indicatorConfig, stats)) + if (Array.isArray(indicatorDescription.field)) { + const separator = indicatorDescription.separator ?? DEFAULT_SEPARATOR; + value = indicatorDescription.field + .map((field) => getValue(field, indicatorDescription, stats)) .join(' ' + separator + ' '); } else { - value = getValue(indicatorConfig.field, indicatorConfig, stats); + value = getValue(indicatorDescription.field, indicatorDescription, stats); } } return { - name: indicatorConfig.name, - matIcon: indicatorConfig.matIcon, + name: indicatorDescription.name, + matIcon: indicatorDescription.matIcon, value: value, }; } diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts index a0897604bf..0e0f51d908 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts @@ -1,14 +1,45 @@ import { Component, OnInit } from '@angular/core'; import { GN2CommonModule } from '@geonature_common/GN2Common.module'; import { CommonModule } from '@angular/common'; -import { ConfigService } from '@geonature/services/config.service'; import { DataFormService, Profile } from '@geonature_common/form/data-form.service'; import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; import { CommonService } from '@geonature_common/service/common.service'; -import { computeIndicatorFromConfig, Indicator, IndicatorRaw } from '../indicator/indicator'; +import { + computeIndicatorFromDecsription, + Indicator, + IndicatorDescription, +} from '../indicator/indicator'; import { IndicatorComponent } from '../indicator/indicator.component'; import { TaxonSheetService } from '../taxon-sheet.service'; +const INDICATORS: Array = [ + { + name: 'observation(s) valide(s)', + matIcon: 'search', + field: 'count_valid_data', + type: 'number', + }, + { + name: 'Première observation', + matIcon: 'schedule', + field: 'first_valid_data', + type: 'date', + }, + { + name: 'Dernière observation', + matIcon: 'search', + field: 'last_valid_data', + type: 'date', + }, + { + name: "Plage d'altitude(s)", + matIcon: 'terrain', + field: ['altitude_min', 'altitude_max'], + unit: 'm', + type: 'number', + }, +]; + @Component({ standalone: true, selector: 'tab-profile', @@ -21,7 +52,6 @@ export class TabProfileComponent implements OnInit { _profile: Profile | null; constructor( - private _config: ConfigService, private _ds: DataFormService, private _commonService: CommonService, private _tss: TaxonSheetService @@ -54,19 +84,8 @@ export class TabProfileComponent implements OnInit { set profile(profile: Profile | null) { this._profile = profile; - - if ( - this._config && - this._config['SYNTHESE'] && - this._config['SYNTHESE']['SPECIES_SHEET'] && - this._config['SYNTHESE']['SPECIES_SHEET']['PROFILE'] - ) { - this.indicators = this._config['SYNTHESE']['SPECIES_SHEET']['PROFILE']['LIST_INDICATORS'].map( - (indicatorConfig: IndicatorRaw) => - computeIndicatorFromConfig(indicatorConfig, profile?.properties) - ); - } else { - this.indicators = []; - } + this.indicators = INDICATORS.map((indicatorRaw: IndicatorDescription) => + computeIndicatorFromDecsription(indicatorRaw, profile?.properties) + ); } } diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.component.ts index d2fa308d7e..a17055c723 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.component.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.component.ts @@ -6,17 +6,55 @@ import { RouterLinkActive, RouterOutlet, } from '@angular/router'; -import { ConfigService } from '@geonature/services/config.service'; import { GN2CommonModule } from '@geonature_common/GN2Common.module'; import { InfosComponent } from './infos/infos.component'; import { LayoutComponent } from './layout/layout.component'; -import { computeIndicatorFromConfig, Indicator, IndicatorRaw } from './indicator/indicator'; +import { + computeIndicatorFromDecsription, + Indicator, + IndicatorDescription, +} from './indicator/indicator'; import { IndicatorComponent } from './indicator/indicator.component'; import { CommonModule } from '@angular/common'; import { SyntheseDataService } from '@geonature_common/form/synthese-form/synthese-data.service'; import { TaxonSheetService } from './taxon-sheet.service'; import { RouteService } from './taxon-sheet.route.service'; +const INDICATORS: Array = [ + { + name: 'observation(s)', + matIcon: 'search', + field: 'observation_count', + type: 'number', + }, + { + name: 'observateur(s)', + matIcon: 'people', + field: 'observer_count', + type: 'number', + }, + { + name: 'commune(s)', + matIcon: 'location_on', + field: 'area_count', + type: 'number', + }, + { + name: "Plage d'altitude(s)", + matIcon: 'terrain', + unit: 'm', + type: 'number', + field: ['altitude_min', 'altitude_max'], + }, + { + name: "Plage d'observation(s)", + matIcon: 'date_range', + type: 'date', + field: ['date_min', 'date_max'], + separator: '-', + }, +]; + @Component({ standalone: true, selector: 'pnx-taxon-sheet', @@ -44,7 +82,6 @@ export class TaxonSheetComponent implements OnInit { private _route: ActivatedRoute, private _tss: TaxonSheetService, private _syntheseDataService: SyntheseDataService, - private _config: ConfigService, private _routes: RouteService ) { this.TAB_LINKS = this._routes.TAB_LINKS; @@ -62,18 +99,9 @@ export class TaxonSheetComponent implements OnInit { } setIndicators(stats: any) { - if ( - this._config && - this._config['SYNTHESE'] && - this._config['SYNTHESE']['SPECIES_SHEET'] && - this._config['SYNTHESE']['SPECIES_SHEET']['LIST_INDICATORS'] - ) { - this.indicators = this._config['SYNTHESE']['SPECIES_SHEET']['LIST_INDICATORS'].map( - (indicatorConfig: IndicatorRaw) => computeIndicatorFromConfig(indicatorConfig, stats) - ); - } else { - this.indicators = []; - } + this.indicators = INDICATORS.map((indicatorConfig: IndicatorDescription) => + computeIndicatorFromDecsription(indicatorConfig, stats) + ); } goToPath(path: string) { diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts index c584288d27..054cf2c944 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts @@ -1,10 +1,8 @@ import { Injectable } from '@angular/core'; import { - CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, - ActivatedRoute, CanActivateChild, } from '@angular/router'; import { ConfigService } from '@geonature/services/config.service'; @@ -16,37 +14,29 @@ import { TabTaxonomyComponent } from './tab-taxonomy/tab-taxonomy.component'; interface Tab { label: string; path: string; - configEntry: string; + configEnabledField?: string; component: any; } -const ROUTE_GEOGRAPHIC_OVERVIEW: Tab = { - label: 'Synthèse Géographique', - path: 'geographic_overview', - configEntry: 'GEOGRAPHIC_OVERVIEW', - component: TabGeographicOverviewComponent, -}; - -export const ROUTE_MANDATORY = ROUTE_GEOGRAPHIC_OVERVIEW; - -const ROUTE_TAXONOMY: Tab = { - label: 'Taxonomie', - path: 'taxonomy', - configEntry: 'TAXONOMY', - component: TabTaxonomyComponent, -}; - -const ROUTE_PROFILE: Tab = { - label: 'Profil', - path: 'profile', - configEntry: 'PROFILE', - component: TabProfileComponent, -}; - export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ - ROUTE_GEOGRAPHIC_OVERVIEW, - ROUTE_TAXONOMY, - ROUTE_PROFILE, + { + label: 'Synthèse Géographique', + path: 'geographic_overview', + component: TabGeographicOverviewComponent, + configEnabledField: null, // make it always available ! + }, + { + label: 'Taxonomie', + path: 'taxonomy', + configEnabledField: 'ENABLE_TAXONOMY', + component: TabTaxonomyComponent, + }, + { + label: 'Profil', + path: 'profile', + configEnabledField: 'ENABLE_PROFILE', + component: TabProfileComponent, + }, ]; @Injectable({ @@ -54,20 +44,15 @@ export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ }) export class RouteService implements CanActivateChild { readonly TAB_LINKS = []; - constructor( private _config: ConfigService, private _router: Router ) { - this.TAB_LINKS.push(ROUTE_MANDATORY); - if (this._config && this._config['SYNTHESE'] && this._config['SYNTHESE']['SPECIES_SHEET']) { + if (this._config['SYNTHESE']?.['SPECIES_SHEET']) { const config = this._config['SYNTHESE']['SPECIES_SHEET']; - if (config['TAXONOMY'] && config['TAXONOMY']['ENABLED']) { - this.TAB_LINKS.push(ROUTE_TAXONOMY); - } - if (config['PROFILE'] && config['PROFILE']['ENABLED']) { - this.TAB_LINKS.push(ROUTE_PROFILE); - } + this.TAB_LINKS = ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES.filter( + (tab) => !tab.configEnabledField || config[tab.configEnabledField] + ); } } @@ -76,18 +61,9 @@ export class RouteService implements CanActivateChild { state: RouterStateSnapshot ): Observable | Promise | boolean { const targetedPath = childRoute.routeConfig.path; - if (ROUTE_MANDATORY.path == targetedPath) { + if (this.TAB_LINKS.map((tab) => tab.path).includes(targetedPath)) { return true; } - const targetedTab = ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES.find( - (tab) => tab.path === targetedPath - ); - if (this._config && this._config['SYNTHESE'] && this._config['SYNTHESE']['SPECIES_SHEET']) { - const config = this._config['SYNTHESE']['SPECIES_SHEET']; - if (config[targetedTab.configEntry] && config[targetedTab.configEntry]['ENABLED']) { - return true; - } - } this._router.navigate(['/404'], { skipLocationChange: true }); return false;