Skip to content

Commit d662cf1

Browse files
committed
refactor: simplify score completion presentation
1 parent da09df7 commit d662cf1

File tree

5 files changed

+87
-132
lines changed

5 files changed

+87
-132
lines changed

src/features/cartographie/components/lieux-mediation-numerique-details/score-completion/score-completion.component.html

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
<th scope="row" class="text-start fw-bold w-100">
2626
<small class="fw-bold" [ngClass]="field.presence ? 'text-primary' : 'text-dark'">{{ field.name }}</small>
2727
</th>
28-
<td class="text-end fw-bold">
29-
<small class="fw-bold" [ngClass]="field.presence ? 'text-primary' : 'text-dark'">
30-
<span *ngIf="field.presence" role="img" class="ri-check-line ri-xl text-primary" aria-hidden="true"></span>
31-
<span *ngIf="!field.presence" role="img" class="ri-close-line ri-xl text-dark" aria-hidden="true"></span>
28+
<td class="text-end">
29+
<small class="text-primary fw-bold">
30+
<span
31+
class="ri-xl"
32+
role="img"
33+
[ngClass]="field.presence ? 'ri-check-line text-primary' : 'ri-close-line text-dark'"
34+
aria-hidden="true"></span>
3235
</small>
3336
</td>
3437
</tr>
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2-
import { scoreCompletionPresence, scoreCompletion } from './score-completion.presenter';
2+
import { scoreCompletionPresence, scoreCompletionRate } from './score-completion.presenter';
33
import { LieuMediationNumeriqueDetailsPresentation } from '@features/cartographie/presenters';
4-
import { ScorePresence } from './score-completion.presentation';
4+
import { ScorePresenceField } from './score-completion.presentation';
55

66
@Component({
77
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -11,10 +11,10 @@ import { ScorePresence } from './score-completion.presentation';
1111
export class ScoreCompletionComponent {
1212
@Input() public lieuMediationNumerique!: LieuMediationNumeriqueDetailsPresentation;
1313

14-
public scoreCompletion = scoreCompletion;
14+
public scoreCompletion = scoreCompletionRate;
1515
public scoreCompletionPresence = scoreCompletionPresence;
1616

17-
public sortScoreCompletionPresence(scorePresence: ScorePresence[]): ScorePresence[] {
18-
return scorePresence.sort((a, b) => Number(b.presence) - Number(a.presence));
17+
public sortScoreCompletionPresence(scorePresence: ScorePresenceField[]): ScorePresenceField[] {
18+
return scorePresence.sort((a, b) => Number(a.presence) - Number(b.presence));
1919
}
2020
}
Lines changed: 42 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,49 @@
1-
export const TOTAL_SCORE_COMPLETION: number = 41;
1+
import { LieuMediationNumeriqueDetailsPresentation } from '../../../presenters';
22

3-
export type ScoreDetail = {
4-
score: number;
5-
name: string;
6-
};
7-
8-
export type ScorePresence = {
9-
name: string;
10-
presence: boolean;
11-
};
3+
type Join<K, P> = K extends string | number ? (P extends string | number ? `${K}.${P}` : never) : never;
124

13-
type ScoreCompletion = {
14-
[key: string]: ScoreDetail | ScoreContact | ScorePresentation | ScoreLocalisation;
15-
};
5+
type Paths<T> = T extends object
6+
? {
7+
[K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Paths<T[K]>> : never;
8+
}[keyof T]
9+
: never;
1610

17-
type ScoreContact = {
18-
telephone: ScoreDetail;
19-
courriel: ScoreDetail;
20-
site_web: ScoreDetail;
11+
export type ScoreCoefficientField = {
12+
coefficient: number;
13+
name: string;
14+
field: Paths<LieuMediationNumeriqueDetailsPresentation>;
2115
};
2216

23-
type ScorePresentation = {
24-
presentation_detail: ScoreDetail;
25-
presentation_resume: ScoreDetail;
26-
};
17+
export type ScorePresenceField = { presence: boolean; name: string; field: Paths<LieuMediationNumeriqueDetailsPresentation> };
2718

28-
type ScoreLocalisation = {
29-
latitude: ScoreDetail;
30-
longitude: ScoreDetail;
31-
};
19+
export const SCORE_FIELDS: ScoreCoefficientField[] = [
20+
{ coefficient: 2, name: 'Nom', field: 'adresse' },
21+
{ coefficient: 2, name: 'Adresse', field: 'adresse' },
22+
{ coefficient: 2, name: 'Commune', field: 'commune' },
23+
{ coefficient: 2, name: 'Code postal', field: 'code_postal' },
24+
{ coefficient: 2, name: 'Services', field: 'services' },
25+
{ coefficient: 2, name: 'Horaires', field: 'horaires' },
26+
{ coefficient: 2, name: 'Typologie', field: 'typologies' },
27+
{ coefficient: 2, name: 'Téléphone', field: 'contact.telephone' },
28+
{ coefficient: 2, name: 'Courriel', field: 'contact.courriel' },
29+
{ coefficient: 2, name: 'Site web', field: 'contact.site_web' },
30+
{ coefficient: 2, name: 'Présentation détaillée', field: 'presentation.detail' },
31+
{ coefficient: 2, name: 'Présentation résumée', field: 'presentation.resume' },
32+
{ coefficient: 2, name: 'Date de mise à jour', field: 'date_maj' },
33+
{ coefficient: 2, name: 'Publics accueillis', field: 'publics_accueillis' },
34+
{ coefficient: 2, name: 'Conditions d’accès', field: 'conditions_acces' },
35+
{ coefficient: 2, name: 'Label nationaux', field: 'labels_nationaux' },
36+
{ coefficient: 2, name: 'Autres labels', field: 'labels_autres' },
37+
{ coefficient: 2, name: 'Modalités d’accompagnement', field: 'modalites_accompagnement' },
38+
{ coefficient: 1, name: 'Accessibilité', field: 'accessibilite' },
39+
{ coefficient: 1, name: 'Latitude', field: 'localisation.latitude' },
40+
{ coefficient: 1, name: 'Longitude', field: 'localisation.longitude' },
41+
{ coefficient: 1, name: 'Prise de RDV', field: 'prise_rdv' },
42+
{ coefficient: 1, name: 'Source', field: 'source' }
43+
// todo: ajouter le pivot
44+
];
3245

33-
export const scoreCompletionTable: ScoreCompletion = {
34-
nom: { score: 2, name: 'Nom' },
35-
adresse: { score: 2, name: 'Adresse' },
36-
commune: { score: 2, name: 'Commune' },
37-
code_postal: { score: 2, name: 'Code postal' },
38-
services: { score: 2, name: 'Services' },
39-
horaires: { score: 2, name: 'Horaires' },
40-
typologies: { score: 2, name: 'Typologie' },
41-
contact: {
42-
telephone: { score: 2, name: 'Téléphone' },
43-
courriel: { score: 2, name: 'Courriel' },
44-
site_web: { score: 2, name: 'Site web' }
45-
},
46-
presentation: {
47-
presentation_detail: { score: 2, name: 'Présentation détaillée' },
48-
presentation_resume: { score: 2, name: 'Présentation résumée' }
49-
},
50-
date_maj: { score: 2, name: 'Date de mise à jour' },
51-
publics_accueillis: { score: 2, name: 'Publics accueillis' },
52-
conditions_acces: { score: 2, name: 'Conditions d’accès' },
53-
labels_nationaux: { score: 2, name: 'Label nationaux' },
54-
autres_labels: { score: 2, name: 'Autres labels' },
55-
modalites_accompagnement: { score: 2, name: 'Modalités d’accompagnement' },
56-
accessibilite: { score: 1, name: 'Accessibilité' },
57-
localisation: {
58-
latitude: { score: 1, name: 'Latitude' },
59-
longitude: { score: 1, name: 'Longitude' }
60-
},
61-
prise_rdv: { score: 1, name: 'Prise de RDV' },
62-
source: { score: 1, name: 'Source' },
63-
pivot: { score: 2, name: 'Pivot' }
64-
};
46+
export const TOTAL_SCORE_COEFFICIENTS: number = SCORE_FIELDS.reduce(
47+
(totalCoefficients: number, { coefficient }: ScoreCoefficientField) => totalCoefficients + coefficient,
48+
0
49+
);

src/features/cartographie/components/lieux-mediation-numerique-details/score-completion/score-completion.presenter.spec.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { scoreCompletion } from './score-completion.presenter';
1+
import { scoreCompletionRate } from './score-completion.presenter';
22
import {
33
Adresse,
44
ConditionAcces,
@@ -22,10 +22,7 @@ import {
2222
} from '@gouvfr-anct/lieux-de-mediation-numerique';
2323
import { firstValueFrom, of } from 'rxjs';
2424
import { ParamMap } from '@angular/router';
25-
import {
26-
LieuMediationNumeriqueDetailsPresentation,
27-
LieuxMediationNumeriqueDetailsPresenter
28-
} from '../../../../cartographie/presenters';
25+
import { LieuMediationNumeriqueDetailsPresentation, LieuxMediationNumeriqueDetailsPresenter } from '../../../presenters';
2926
import { LieuxMediationNumeriqueRepository } from '../../../../core/repositories';
3027
import { NO_LOCALISATION } from '../../../../core/models';
3128

@@ -85,9 +82,9 @@ describe('score completion presenter', (): void => {
8582
)
8683
);
8784

88-
const scoreCompletionTotal: number = scoreCompletion(structure);
85+
const scoreCompletionTotal: number = scoreCompletionRate(structure);
8986

90-
expect(scoreCompletionTotal).toStrictEqual(80);
87+
expect(scoreCompletionTotal).toStrictEqual(95);
9188
});
9289

9390
it('should return low score completion', async (): Promise<void> => {
@@ -126,7 +123,7 @@ describe('score completion presenter', (): void => {
126123
)
127124
);
128125

129-
const scoreCompletionTotal: number = scoreCompletion(structure);
126+
const scoreCompletionTotal: number = scoreCompletionRate(structure);
130127

131128
expect(scoreCompletionTotal).toStrictEqual(39);
132129
});
Lines changed: 28 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,34 @@
1-
import { LieuMediationNumeriqueDetailsPresentation } from '../../../../cartographie/presenters';
2-
import { ScoreDetail, ScorePresence, TOTAL_SCORE_COMPLETION, scoreCompletionTable } from './score-completion.presentation';
1+
import { LieuMediationNumeriqueDetailsPresentation } from '../../../presenters';
2+
import {
3+
SCORE_FIELDS,
4+
ScoreCoefficientField,
5+
ScorePresenceField,
6+
TOTAL_SCORE_COEFFICIENTS
7+
} from './score-completion.presentation';
38

4-
const NESTED_FIELDS: string[] = ['contact', 'presentation', 'localisation'];
9+
type NestedRecord = { [k: string]: NestedRecord } | undefined;
510

6-
const calculateNestedFieldScore = (
7-
nestedFieldName: string,
8-
lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation
9-
): number => {
10-
let nestedScore = 0;
11-
const nestedFieldValue = lieuMediationNumerique[nestedFieldName as keyof LieuMediationNumeriqueDetailsPresentation];
12-
Object.entries(scoreCompletionTable[nestedFieldName]).forEach(([key, value]) => {
13-
if (nestedFieldValue && nestedFieldValue.hasOwnProperty(key)) {
14-
nestedScore += value.score;
15-
}
16-
});
17-
return nestedScore;
18-
};
19-
20-
export const scoreCompletion = (lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation): number => {
21-
const scoreSimpleField: number = Object.keys(scoreCompletionTable)
22-
.filter((field) => !NESTED_FIELDS.includes(field) && lieuMediationNumerique.hasOwnProperty(field))
23-
.reduce((score, curr) => score + (scoreCompletionTable[curr] as ScoreDetail).score, 0);
24-
25-
const scoreNestedField: number = NESTED_FIELDS.reduce(
26-
(score, nestedField) => score + calculateNestedFieldScore(nestedField, lieuMediationNumerique),
27-
0
28-
);
11+
const toNestedProperty = (property: NestedRecord, nestedField: string): NestedRecord =>
12+
property && property.hasOwnProperty(nestedField) ? property[nestedField] : undefined;
2913

30-
const scoreCompletionPercent: number = ((scoreSimpleField + scoreNestedField) / TOTAL_SCORE_COMPLETION) * 100;
31-
return Math.round(scoreCompletionPercent);
32-
};
14+
const hasField = (field: string) => (lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation) =>
15+
field.split('.').reduce(toNestedProperty, lieuMediationNumerique as unknown as NestedRecord) !== undefined;
3316

34-
export const scoreCompletionPresence = (lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation): ScorePresence[] => {
35-
const fieldPresence: ScorePresence[] = [];
17+
const computeScore =
18+
(lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation) =>
19+
(score: number, { field, coefficient }: ScoreCoefficientField): number =>
20+
hasField(field)(lieuMediationNumerique) ? score + coefficient : score;
3621

37-
Object.keys(scoreCompletionTable).forEach((field) => {
38-
if (!NESTED_FIELDS.includes(field)) {
39-
fieldPresence.push({
40-
name: (scoreCompletionTable[field] as ScoreDetail).name,
41-
presence: lieuMediationNumerique.hasOwnProperty(field)
42-
});
43-
}
44-
});
22+
export const scoreCompletionRate = (lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation): number =>
23+
Math.round((SCORE_FIELDS.reduce(computeScore(lieuMediationNumerique), 0) / TOTAL_SCORE_COEFFICIENTS) * 100);
4524

46-
const nestedFieldsPresence = (
47-
nestedFieldName: string,
48-
lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation
49-
): void => {
50-
const nestedFieldValue = lieuMediationNumerique[nestedFieldName as keyof LieuMediationNumeriqueDetailsPresentation] || {};
51-
Object.keys(scoreCompletionTable[nestedFieldName]).forEach((key) => {
52-
fieldPresence.push({
53-
name: (scoreCompletionTable[nestedFieldName] as Record<string, ScoreDetail>)[key].name,
54-
presence: nestedFieldValue.hasOwnProperty(key)
55-
});
56-
});
57-
};
58-
59-
NESTED_FIELDS.forEach((nestedField) => {
60-
nestedFieldsPresence(nestedField, lieuMediationNumerique);
61-
});
62-
63-
return fieldPresence;
64-
};
25+
export const scoreCompletionPresence = (
26+
lieuMediationNumerique: LieuMediationNumeriqueDetailsPresentation
27+
): ScorePresenceField[] =>
28+
SCORE_FIELDS.map(
29+
({ field, name }: ScoreCoefficientField): ScorePresenceField => ({
30+
presence: hasField(field)(lieuMediationNumerique),
31+
field,
32+
name
33+
})
34+
);

0 commit comments

Comments
 (0)