From 609a027262a37c274c40b5d8ef1d752baa274212 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 14:11:52 +0200 Subject: [PATCH 01/52] introduced search by type vaccin --- src/views/vmd-rdv.view.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index d8547152c..8116c0329 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -110,9 +110,20 @@ export abstract class AbstractVmdRdvView extends LitElement { + /> + +
+ +
+ +
- ${this.renderAdditionnalSearchCriteria()} +
+ ${this.renderAdditionnalSearchCriteria()}
From e54cfcb46a03ac18ce9ad4c7847b9c41e97325c1 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 14:15:31 +0200 Subject: [PATCH 02/52] introduced upcoming day selector --- src/views/vmd-rdv.view.ts | 106 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 8116c0329..4bd2dd95f 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -54,6 +54,36 @@ export abstract class AbstractVmdRdvView extends LitElement { CSS_Global, css`${unsafeCSS(rdvViewCss)}`, css` + .list-group-horizontal.days li.list-group-item.selected { + border: 4px solid #5561d9; + padding: 5px; + } + .list-group-horizontal.days li.list-group-item:not(.selected) { + padding: 8px; + } + + .cpt-rdv { + border-radius: 8px; + padding: 4px 6px; + white-space: nowrap; + background-color: #5561d9; color: white; + font-weight: bold; + } + + ul.days { + width: 100%; + overflow: scroll; + margin-top: 10px; + margin-bottom: 30px; + font-size: 2rem; + } + .days li { + text-align: center; + } + .days .day { + font-weight: bold; + white-space: nowrap; + } ` ]; @@ -128,6 +158,82 @@ export abstract class AbstractVmdRdvView extends LitElement {
+
    +
  • +
    Ven 21
    + 1 créneau +
  • +
  • +
    Sam 22
    +
  • +
  • +
    Dim 23
    +
  • +
  • +
    Lun 24
    +
  • +
  • +
    Mar 25
    + 20 créneaux +
  • +
  • +
    Mer 26
    + 25 créneaux +
  • +
  • +
    Jeu 27
    + 22 créneaux +
  • +
  • +
    Ven 28
    + 30 créneaux +
  • + + + + +
  • +
    Dim 30
    + 28 créneaux +
  • +
  • +
    Lun 31
    + 35 créneau +
  • +
  • +
    Mar 01/06
    + 42 créneaux +
  • +
  • +
    Mer 02/06
    + 42 créneaux +
  • +
  • +
    Jeu 03/06
    + 42 créneaux +
  • +
  • +
    Ven 04/06
    + 42 créneaux +
  • +
  • +
    Sam 05/06
    + 42 créneaux +
  • +
  • +
    Dim 06/06
    + 42 créneaux +
  • +
  • +
    Lun 07/06
    + 42 créneaux +
  • +
  • +
    Mar 08/06
    + 42 créneaux +
  • +
+ ${this.searchInProgress?html`
From 2564f029fa8935c25ee6a3805924284a65ea7be5 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 14:35:07 +0200 Subject: [PATCH 03/52] getting rid of critereTri property, this is now hardcoded to 'date' (for department search) or 'distance' (for commune search) --- src/components/vmd-search.component.ts | 2 +- src/state/State.ts | 19 ++---- src/utils/Analytics.ts | 6 +- src/views/vmd-rdv.view.ts | 81 ++++---------------------- 4 files changed, 20 insertions(+), 88 deletions(-) diff --git a/src/components/vmd-search.component.ts b/src/components/vmd-search.component.ts index cb31d144e..d528098c0 100644 --- a/src/components/vmd-search.component.ts +++ b/src/components/vmd-search.component.ts @@ -57,7 +57,7 @@ export class VmdSearchComponent extends LitElement { private onCommuneSelected (commune: Commune) { this.currentSelection = commune this.dispatchEvent(new CustomEvent('on-search', { - detail: SearchRequest.ByCommune(commune, 'distance', this.currentSearchType || 'standard') + detail: SearchRequest.ByCommune(commune, this.currentSearchType || 'standard') })) } diff --git a/src/state/State.ts b/src/state/State.ts index ace4cbb7f..1dca50a35 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -17,10 +17,11 @@ export namespace SearchRequest { export type ByDepartement = { type: SearchType, par: 'departement', - departement: Departement + departement: Departement, + tri: 'date' } export function ByDepartement (departement: Departement, type: SearchType): ByDepartement { - return { type, par: 'departement', departement } + return { type, par: 'departement', departement, tri: 'date' } } export function isByDepartement (searchRequest: SearchRequest): searchRequest is ByDepartement { return searchRequest.par === 'departement' @@ -30,10 +31,10 @@ export namespace SearchRequest { type: SearchType, par: 'commune', commune: Commune, - tri: CodeTriCentre + tri: 'distance' } - export function ByCommune (commune: Commune, tri: CodeTriCentre, type: SearchType): ByCommune { - return { type, par: 'commune', commune, tri } + export function ByCommune (commune: Commune, type: SearchType): ByCommune { + return { type, par: 'commune', commune, tri: 'distance' } } export function isByCommune (searchRequest: SearchRequest): searchRequest is ByCommune { return searchRequest.par === 'commune' @@ -45,14 +46,6 @@ export namespace SearchRequest { } export type CodeTriCentre = 'date' | 'distance'; -export type TriCentre = { - codeTriCentre: CodeTriCentre; - libelle: string; -}; -export const TRIS_CENTRE: Map = new Map([ - ['distance', { codeTriCentre: 'distance', libelle: "Au plus proche" }], - ['date', { codeTriCentre: 'date', libelle: "Disponible au plus vite" }], -]); const USE_RAW_GITHUB = false const VMD_BASE_URL = USE_RAW_GITHUB diff --git a/src/utils/Analytics.ts b/src/utils/Analytics.ts index 5aea6dec3..d79d280ff 100644 --- a/src/utils/Analytics.ts +++ b/src/utils/Analytics.ts @@ -30,7 +30,7 @@ export class Analytics { }); } - clickSurRdv(lieu: Lieu, triCentre: CodeTriCentre, searchType: SearchType, commune: Commune|undefined) { + clickSurRdv(lieu: Lieu, triCentre: CodeTriCentre|'unknown', searchType: SearchType, commune: Commune|undefined) { window.dataLayer.push({ 'event': 'rdv_click', 'rdv_departement' : lieu.departement, @@ -44,7 +44,7 @@ export class Analytics { }); } - clickSurVerifRdv(lieu: Lieu, triCentre: CodeTriCentre, searchType: SearchType, commune: Commune|undefined) { + clickSurVerifRdv(lieu: Lieu, triCentre: CodeTriCentre|'unknown', searchType: SearchType, commune: Commune|undefined) { window.dataLayer.push({ 'event': 'rdv_verify', 'rdv_departement' : lieu.departement, @@ -58,7 +58,7 @@ export class Analytics { }); } - rechercheLieuEffectuee(codeDepartement: CodeDepartement, triCentre: CodeTriCentre, searchType: SearchType, commune: Commune|undefined, resultats: LieuxAvecDistanceParDepartement|undefined) { + rechercheLieuEffectuee(codeDepartement: CodeDepartement, triCentre: CodeTriCentre|'unknown', searchType: SearchType, commune: Commune|undefined, resultats: LieuxAvecDistanceParDepartement|undefined) { window.dataLayer.push({ 'event': commune?'search_by_commune':'search_by_departement', 'search_departement': codeDepartement, diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 4bd2dd95f..6d5b69893 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -15,7 +15,6 @@ import rdvViewCss from "./vmd-rdv.view.scss"; import distanceEntreDeuxPoints from "../distance" import { CodeDepartement, - CodeTriCentre, Commune, libelleUrlPathDeCommune, libelleUrlPathDuDepartement, @@ -25,13 +24,11 @@ import { LieuxParDepartement, SearchRequest, SearchType, - State, - TRIS_CENTRE + State, CodeTriCentre } from "../state/State"; import { formatDistanceToNow, parseISO } from 'date-fns' import { fr } from 'date-fns/locale' import {Strings} from "../utils/Strings"; -import {ValueStrCustomEvent,} from "../components/vmd-commune-or-departement-selector.component"; import {DEPARTEMENTS_LIMITROPHES} from "../utils/Departements"; import {TemplateResult} from "lit-html"; import {Analytics} from "../utils/Analytics"; @@ -109,13 +106,9 @@ export abstract class AbstractVmdRdvView extends LitElement { .reduce((total, lieu) => total+lieu.appointment_count, 0); } - protected beforeNewSearchFromLocation (search: SearchRequest): SearchRequest { - return search - } - async onSearchSelected (event: CustomEvent) { const search = event.detail - this.goToNewSearch(this.beforeNewSearchFromLocation(search)) + this.goToNewSearch(search) } protected async goToNewSearch (search: SearchRequest) { @@ -153,7 +146,6 @@ export abstract class AbstractVmdRdvView extends LitElement {
- ${this.renderAdditionnalSearchCriteria()}
@@ -427,7 +419,7 @@ export abstract class AbstractVmdRdvView extends LitElement { const commune = SearchRequest.isByCommune(currentSearch) ? currentSearch.commune : undefined Analytics.INSTANCE.rechercheLieuEffectuee( codeDepartement, - this.currentCritereTri(), + this.currentTri(), currentSearch.type, commune, this.lieuxParDepartementAffiches); @@ -442,20 +434,20 @@ export abstract class AbstractVmdRdvView extends LitElement { private prendreRdv(lieu: Lieu) { if(this.currentSearch && SearchRequest.isByCommune(this.currentSearch) && lieu.url) { - Analytics.INSTANCE.clickSurRdv(lieu, this.currentCritereTri(), this.currentSearch.type, this.currentSearch.commune); + Analytics.INSTANCE.clickSurRdv(lieu, this.currentTri(), this.currentSearch.type, this.currentSearch.commune); } Router.navigateToUrlIfPossible(lieu.url); } private verifierRdv(lieu: Lieu) { if(this.currentSearch && SearchRequest.isByCommune(this.currentSearch) && lieu.url) { - Analytics.INSTANCE.clickSurVerifRdv(lieu, this.currentCritereTri(), this.currentSearch.type, this.currentSearch.commune); + Analytics.INSTANCE.clickSurVerifRdv(lieu, this.currentTri(), this.currentSearch.type, this.currentSearch.commune); } Router.navigateToUrlIfPossible(lieu.url); } - renderAdditionnalSearchCriteria(): TemplateResult { - return html``; + private currentTri(): CodeTriCentre|"unknown" { + return this.currentSearch?this.currentSearch.tri:'unknown'; } // FIXME move me to testable files @@ -502,7 +494,6 @@ export abstract class AbstractVmdRdvView extends LitElement { return lieu; } - abstract currentCritereTri(): CodeTriCentre; abstract libelleLieuSelectionne(): TemplateResult; // FIXME move me to a testable file abstract afficherLieuxParDepartement(lieuxParDepartement: LieuxParDepartement, search: SearchRequest): LieuxAvecDistanceParDepartement; @@ -523,26 +514,21 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { this._codePostalSelectionne = code this.updateCurrentSearch() } - @property({type: String}) set critèreDeTri (critèreDeTri: 'date' | 'distance') { - this._critèreDeTri = critèreDeTri - this.updateCurrentSearch() - } @internalProperty() private _searchType: SearchType | undefined = undefined; @internalProperty() private _codeCommuneSelectionne: string | undefined = undefined; @internalProperty() private _codePostalSelectionne: string | undefined = undefined; - @internalProperty() private _critèreDeTri: CodeTriCentre = 'distance' private currentSearchMarker = {} private async updateCurrentSearch() { - if (this._codeCommuneSelectionne && this._codePostalSelectionne && this._critèreDeTri && this._searchType) { + if (this._codeCommuneSelectionne && this._codePostalSelectionne && this._searchType) { const marker = {} this.currentSearchMarker = marker await delay(20) if (this.currentSearchMarker !== marker) { return } const commune = await State.current.autocomplete.findCommune(this._codePostalSelectionne, this._codeCommuneSelectionne) if (commune) { - this.currentSearch = SearchRequest.ByCommune(commune, this._critèreDeTri, this._searchType) + this.currentSearch = SearchRequest.ByCommune(commune, this._searchType) this.refreshLieux() } } @@ -578,57 +564,10 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { .map(l => ({...l, distance: distanceAvec(l) })) .map(l => this.transformLieuEnFonctionDuTypeDeRecherche(l)) .filter(l => !l.distance || l.distance < MAX_DISTANCE_CENTRE_IN_KM) - .sortBy(l => this.extraireFormuleDeTri(l, search.tri)) + .sortBy(l => this.extraireFormuleDeTri(l, 'distance')) .build() }; } - - // FIXME move me to vmd-search) - critereTriUpdated(triCentre: CodeTriCentre) { - Analytics.INSTANCE.critereTriCentresMisAJour(triCentre); - if (this.currentSearch) { - const nextSearch = { - ...this.currentSearch, - tri: triCentre - } - this.goToNewSearch(nextSearch) - } - } - protected beforeNewSearchFromLocation (search: SearchRequest): SearchRequest { - if (SearchRequest.isByCommune(search) && this.currentSearch) { - return { - ...search, - tri: this.currentSearch.tri - } - } - return search - } - - // FIXME move me to vmd-search - renderAdditionnalSearchCriteria(): TemplateResult { - if(SearchRequest.isStandardType(this.currentSearch)) { - return html` -
- -
- - -
-
- `; - } else { - return html``; - } - } - - currentCritereTri(): CodeTriCentre { - return this.critèreDeTri; - } } @customElement('vmd-rdv-par-departement') From 39be1cc8c51c914656e9ed9931a80753fde8822e Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 14:37:37 +0200 Subject: [PATCH 04/52] introduced distance search field --- ...md-input-range-with-tooltip.component.scss | 121 ++++++++++++++++ .../vmd-input-range-with-tooltip.component.ts | 79 +++++++++++ src/index.ts | 1 + src/views/vmd-rdv.view.ts | 133 ++++++++++++++++++ 4 files changed, 334 insertions(+) create mode 100644 src/components/vmd-input-range-with-tooltip.component.scss create mode 100644 src/components/vmd-input-range-with-tooltip.component.ts diff --git a/src/components/vmd-input-range-with-tooltip.component.scss b/src/components/vmd-input-range-with-tooltip.component.scss new file mode 100644 index 000000000..528661799 --- /dev/null +++ b/src/components/vmd-input-range-with-tooltip.component.scss @@ -0,0 +1,121 @@ +@import "bootstrap/scss/functions"; +@import "bootstrap/scss/mixins/breakpoints"; +@import "../styles/bootstrap-variables"; +@import "../styles/global-variables"; + +/* see https://css-tricks.com/value-bubbles-for-range-inputs/ */ +.range-wrap { + position: relative; + margin: 3rem auto 3rem; +} +.bubble { + background: $primary; + color: white; + padding: 4px 12px; + position: absolute; + border-radius: 4px; + left: 50%; + top: 40px; + transform: translateX(-50%); +} +.bubble::after { + content: ""; + position: absolute; + width: 2px; + height: 2px; + background: $primary; + top: -1px; + left: 50%; +} + +/* see https://www.cssportal.com/style-input-range/ */ +input[type=range] { + height: 26px; + background-color: transparent; + -webkit-appearance: none; + margin: 10px 0; + width: 100%; +} +input[type=range]:focus { + outline: none; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 14px; + cursor: pointer; + animate: 0.2s; + box-shadow: 1px 1px 1px $primary; + background: $primary; + border-radius: 14px; + border: 0px solid #000000; +} +input[type=range]::-webkit-slider-thumb { + box-shadow: 0px 0px 0px #000000; + border: 0px solid #000000; + height: 20px; + width: 40px; + border-radius: 12px; + background: white; + cursor: pointer; + -webkit-appearance: none; + margin-top: -3px; +} +input[type=range]:focus::-webkit-slider-runnable-track { + background: $primary; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 14px; + cursor: pointer; + animate: 0.2s; + box-shadow: 1px 1px 1px $primary; + background: $primary; + border-radius: 14px; + border: 0px solid #000000; +} +input[type=range]::-moz-range-thumb { + box-shadow: 0px 0px 0px #000000; + border: 0px solid #000000; + height: 20px; + width: 40px; + border-radius: 12px; + background: white; + cursor: pointer; +} +input[type=range]::-ms-track { + width: 100%; + height: 14px; + cursor: pointer; + animate: 0.2s; + background: transparent; + border-color: transparent; + color: transparent; +} +input[type=range]::-ms-fill-lower { + background: $primary; + border: 0px solid #000000; + border-radius: 28px; + box-shadow: 1px 1px 1px $primary; +} +input[type=range]::-ms-fill-upper { + background: $primary; + border: 0px solid #000000; + border-radius: 28px; + box-shadow: 1px 1px 1px $primary; +} +input[type=range]::-ms-thumb { + margin-top: 1px; + box-shadow: 0px 0px 0px #000000; + border: 0px solid #000000; + height: 20px; + width: 40px; + border-radius: 12px; + background: white; + cursor: pointer; +} +input[type=range]:focus::-ms-fill-lower { + background: $primary; +} +input[type=range]:focus::-ms-fill-upper { + background: $primary; +} diff --git a/src/components/vmd-input-range-with-tooltip.component.ts b/src/components/vmd-input-range-with-tooltip.component.ts new file mode 100644 index 000000000..f41d060cf --- /dev/null +++ b/src/components/vmd-input-range-with-tooltip.component.ts @@ -0,0 +1,79 @@ +import { + LitElement, + html, + customElement, + property, + css, + unsafeCSS, + internalProperty, query +} from 'lit-element'; +import inputRangeWithTooltipCss from "./vmd-input-range-with-tooltip.component.scss"; +import {styleMap} from "lit-html/directives/style-map"; + +export type Options = {code: string|number, libelle: string}; + +@customElement('vmd-input-range-with-tooltip') +export class VmdInputRangeWithTooltipComponent extends LitElement { + + //language=css + static styles = [ + css`${unsafeCSS(inputRangeWithTooltipCss)}`, + css` + ` + ]; + + @property({attribute: false}) options: Options[] = []; + + @internalProperty() indexOptionSelectionnee: number = 0; + @internalProperty() get libelleAffiche() { + return this.options[this.indexOptionSelectionnee]?.libelle || ""; + } + @internalProperty() get max() { + return this.options.length-1; + } + @internalProperty() get bubbleLeft() { + const percentageValue = Math.round(this.indexOptionSelectionnee*10000/this.max)/100; + const leftShiftRatio = 0.5 - percentageValue/100; + return `calc(${percentageValue}% + (${leftShiftRatio*8*this.libelleAffiche.length}px))`; + } + + @query(".bubble") $bubble!: HTMLOutputElement; + + constructor() { + super(); + } + + render() { + return html` +
+ + ${this.libelleAffiche} +
+ `; + } + + indexUpdated(indexStr: string) { + this.indexOptionSelectionnee = Number(indexStr); + this.dispatchEvent(new CustomEvent<{value: string|number|undefined}>('option-selected', { + detail: { + value: this.options[this.indexOptionSelectionnee]?.code + } + })); + } + + connectedCallback() { + super.connectedCallback(); + + if(this.getAttribute("codeSelectionne")) { + this.indexOptionSelectionnee = this.options + .map((o, index) => ({...o, index})) + .find(o => ""+o.code === this.getAttribute("codeSelectionne")) + ?.index || 0; + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + // console.log("disconnected callback") + } +} diff --git a/src/index.ts b/src/index.ts index 1a53312b3..19f386395 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,4 @@ import './components/vmd-appointment-card.component' import './components/vmd-appointment-metadata.component' import './components/vmd-commune-or-departement-selector.component' import './components/vmd-button-switch.component' +import './components/vmd-input-range-with-tooltip.component' diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 6d5b69893..8e8eea93e 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -81,6 +81,123 @@ export abstract class AbstractVmdRdvView extends LitElement { font-weight: bold; white-space: nowrap; } + + /* see https://css-tricks.com/value-bubbles-for-range-inputs/ */ + .range-wrap { + position: relative; + margin: 3rem auto 3rem; + } + .bubble { + background: #5561d9; + color: white; + padding: 4px 12px; + position: absolute; + border-radius: 4px; + left: 50%; + top: 40px; + transform: translateX(-50%); + } + .bubble::after { + content: ""; + position: absolute; + width: 2px; + height: 2px; + background: #5561d9; + top: -1px; + left: 50%; + } + + /* see https://www.cssportal.com/style-input-range/ */ + input[type=range] { + height: 26px; + background-color: transparent; + -webkit-appearance: none; + margin: 10px 0; + width: 100%; + } + input[type=range]:focus { + outline: none; + } + input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 14px; + cursor: pointer; + animate: 0.2s; + box-shadow: 1px 1px 1px #5561d9; + background: #5561d9; + border-radius: 14px; + border: 0px solid #000000; + } + input[type=range]::-webkit-slider-thumb { + box-shadow: 0px 0px 0px #000000; + border: 0px solid #000000; + height: 20px; + width: 40px; + border-radius: 12px; + background: white; + cursor: pointer; + -webkit-appearance: none; + margin-top: -3px; + } + input[type=range]:focus::-webkit-slider-runnable-track { + background: #5561d9; + } + input[type=range]::-moz-range-track { + width: 100%; + height: 14px; + cursor: pointer; + animate: 0.2s; + box-shadow: 1px 1px 1px #5561d9; + background: #5561d9; + border-radius: 14px; + border: 0px solid #000000; + } + input[type=range]::-moz-range-thumb { + box-shadow: 0px 0px 0px #000000; + border: 0px solid #000000; + height: 20px; + width: 40px; + border-radius: 12px; + background: white; + cursor: pointer; + } + input[type=range]::-ms-track { + width: 100%; + height: 14px; + cursor: pointer; + animate: 0.2s; + background: transparent; + border-color: transparent; + color: transparent; + } + input[type=range]::-ms-fill-lower { + background: #5561d9; + border: 0px solid #000000; + border-radius: 28px; + box-shadow: 1px 1px 1px #5561d9; + } + input[type=range]::-ms-fill-upper { + background: #5561d9; + border: 0px solid #000000; + border-radius: 28px; + box-shadow: 1px 1px 1px #5561d9; + } + input[type=range]::-ms-thumb { + margin-top: 1px; + box-shadow: 0px 0px 0px #000000; + border: 0px solid #000000; + height: 20px; + width: 40px; + border-radius: 12px; + background: white; + cursor: pointer; + } + input[type=range]:focus::-ms-fill-lower { + background: #5561d9; + } + input[type=range]:focus::-ms-fill-upper { + background: #5561d9; + } ` ]; @@ -146,6 +263,22 @@ export abstract class AbstractVmdRdvView extends LitElement { +
+ +
+ +
+
From 762e8514da3d1cda21128498af3ba4f34c3568d3 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 14:48:20 +0200 Subject: [PATCH 05/52] introduced search by horaires criteria --- src/views/vmd-rdv.view.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 8e8eea93e..add9de686 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -82,6 +82,20 @@ export abstract class AbstractVmdRdvView extends LitElement { white-space: nowrap; } + input[type=time] { + line-height: 20px; + width: 80px; + font-size: 1.6rem; + } + + .time-range { + width: auto; + display: inline-block; + background-color: white; + padding: 6px; + border: 1px solid grey; + } + /* see https://css-tricks.com/value-bubbles-for-range-inputs/ */ .range-wrap { position: relative; @@ -279,6 +293,20 @@ export abstract class AbstractVmdRdvView extends LitElement { > +
+ +
+ + +
-
+
-
+ +
+
From 17de5b301ecf3a265f5e649e995b238d448c2a9b Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 20:10:06 +0200 Subject: [PATCH 06/52] handling multiple departments complexity in State.lieuxPour() instead of directly in vmd-rdv --- src/state/State.ts | 29 ++++++++++++++++++++--------- src/views/vmd-rdv.view.ts | 19 ++----------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/state/State.ts b/src/state/State.ts index 1dca50a35..37bae2cc5 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -260,15 +260,26 @@ export class State { this.autocomplete = new Autocomplete(webBaseUrl, () => this.departementsDisponibles()) } - async lieuxPour(codeDepartement: CodeDepartement): Promise { - const resp = await fetch(`${VMD_BASE_URL}/${codeDepartement}.json`, { cache: 'no-cache' }) - const results = await resp.json() - const lieuxParDepartement = { - lieuxDisponibles: results.centres_disponibles.map(transformLieu), - lieuxIndisponibles: results.centres_indisponibles.map(transformLieu), - codeDepartements: [codeDepartement], - derniereMiseAJour: results.last_updated - }; + async lieuxPour(codesDepartements: CodeDepartement[]): Promise { + const [principalLieuxDepartement, ...lieuxDepartementsAditionnels] = await Promise.all( + codesDepartements.map(codeDept => fetch(`${VMD_BASE_URL}/${codeDept}.json`, { cache: 'no-cache' }) + .then(resp => resp.json()) + .then((statsDept: LieuxParDepartement_JSON) => ({...statsDept, codeDepartement: codeDept})) + ) + ); + + const lieuxParDepartement = [principalLieuxDepartement].concat(lieuxDepartementsAditionnels).reduce((mergedLieuxParDepartement, lieuxParDepartement) => ({ + codeDepartements: mergedLieuxParDepartement.codeDepartements.concat(lieuxParDepartement.codeDepartement), + derniereMiseAJour: mergedLieuxParDepartement.derniereMiseAJour, + lieuxDisponibles: mergedLieuxParDepartement.lieuxDisponibles.concat(lieuxParDepartement.centres_disponibles.map(transformLieu)), + lieuxIndisponibles: mergedLieuxParDepartement.lieuxIndisponibles.concat(lieuxParDepartement.centres_indisponibles.map(transformLieu)), + }), { + codeDepartements: [], + derniereMiseAJour: principalLieuxDepartement.last_updated, + lieuxDisponibles: [], + lieuxIndisponibles: [] + } as LieuxParDepartement); + return lieuxParDepartement; } diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index add9de686..a206062ef 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -531,7 +531,7 @@ export abstract class AbstractVmdRdvView extends LitElement { ? currentSearch.departement.code_departement : currentSearch.commune.codeDepartement const derniereMiseAJour = this.lieuxParDepartementAffiches?.derniereMiseAJour - const lieuxAJourPourDepartement = await State.current.lieuxPour(codeDepartement) + const lieuxAJourPourDepartement = await State.current.lieuxPour([codeDepartement]) this.miseAJourDisponible = (derniereMiseAJour !== lieuxAJourPourDepartement.derniereMiseAJour); // we stop the update check if there has been one @@ -557,22 +557,7 @@ export abstract class AbstractVmdRdvView extends LitElement { try { this.searchInProgress = true; await delay(1) // give some time (one tick) to render loader before doing the heavy lifting - const [lieuxDepartement, ...lieuxDepartementsLimitrophes] = await Promise.all([ - State.current.lieuxPour(codeDepartement), - ...this.codeDepartementAdditionnels(codeDepartement).map(dept => State.current.lieuxPour(dept)) - ]); - - const lieuxParDepartement = [lieuxDepartement].concat(lieuxDepartementsLimitrophes).reduce((mergedLieuxParDepartement, lieuxParDepartement) => ({ - codeDepartements: mergedLieuxParDepartement.codeDepartements.concat(lieuxParDepartement.codeDepartements), - derniereMiseAJour: mergedLieuxParDepartement.derniereMiseAJour, - lieuxDisponibles: mergedLieuxParDepartement.lieuxDisponibles.concat(lieuxParDepartement.lieuxDisponibles), - lieuxIndisponibles: mergedLieuxParDepartement.lieuxIndisponibles.concat(lieuxParDepartement.lieuxIndisponibles), - }), { - codeDepartements: [], - derniereMiseAJour: lieuxDepartement.derniereMiseAJour, - lieuxDisponibles: [], - lieuxIndisponibles: [] - } as LieuxParDepartement); + const lieuxParDepartement = await State.current.lieuxPour([codeDepartement].concat(this.codeDepartementAdditionnels(codeDepartement))); this.lieuxParDepartementAffiches = this.afficherLieuxParDepartement(lieuxParDepartement, currentSearch); this.cartesAffichees = this.infiniteScroll.ajouterCartesPaginees(this.lieuxParDepartementAffiches, []); From e4d7de418b25ae7e7655745ce3f3e6723d82f276 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 21:55:14 +0200 Subject: [PATCH 07/52] handling creneaux quotidiens in centres json file --- src/state/State.ts | 68 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/state/State.ts b/src/state/State.ts index 37bae2cc5..06d10b5f6 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -1,6 +1,7 @@ import {Strings} from "../utils/Strings"; import { Autocomplete } from './Autocomplete' import { Memoize } from 'typescript-memoize' +import {ArrayBuilder} from "../utils/Arrays"; export type CodeTrancheAge = 'plus75ans'; export type TrancheAge = { @@ -150,15 +151,32 @@ function transformLieu(rawLieu: any): Lieu { }; } export type Coordinates = { latitude: number, longitude: number } - +export type StatsCreneauxQuotidienParTag = { + tag: string; + total: number; +}; +export type StatsCreneauxQuotidien = { + date: string; // "2021-05-23" + total: number; + urls: string[]; + countByTag: StatsCreneauxQuotidienParTag[]; +} export type LieuxParDepartement = { lieuxDisponibles: Lieu[]; lieuxIndisponibles: Lieu[]; codeDepartements: CodeDepartement[]; + creneauxQuotidiens: StatsCreneauxQuotidien[]; derniereMiseAJour: ISODateString; }; export type LieuxParDepartements = Map; +export type LieuxParDepartement_JSON = { + centres_disponibles: Lieu[]; + centres_indisponibles: Lieu[]; + creneaux_quotidiens: {}[]; + last_updated: string; +}; + export type LieuAffichableAvecDistance = Lieu & { disponible: boolean, distance: number|undefined }; export type LieuxAvecDistanceParDepartement = { lieuxAffichables: LieuAffichableAvecDistance[]; @@ -268,18 +286,52 @@ export class State { ) ); - const lieuxParDepartement = [principalLieuxDepartement].concat(lieuxDepartementsAditionnels).reduce((mergedLieuxParDepartement, lieuxParDepartement) => ({ - codeDepartements: mergedLieuxParDepartement.codeDepartements.concat(lieuxParDepartement.codeDepartement), - derniereMiseAJour: mergedLieuxParDepartement.derniereMiseAJour, - lieuxDisponibles: mergedLieuxParDepartement.lieuxDisponibles.concat(lieuxParDepartement.centres_disponibles.map(transformLieu)), - lieuxIndisponibles: mergedLieuxParDepartement.lieuxIndisponibles.concat(lieuxParDepartement.centres_indisponibles.map(transformLieu)), - }), { + const lieuxParDepartement: LieuxParDepartement = [principalLieuxDepartement].concat(lieuxDepartementsAditionnels).reduce((mergedLieuxParDepartement: LieuxParDepartement, lieuxParDepartement: LieuxParDepartement_JSON & {codeDepartement: string}) => { + const creneauxQuotidiens: StatsCreneauxQuotidien[] = mergedLieuxParDepartement.creneauxQuotidiens; + (lieuxParDepartement.creneaux_quotidiens || []).forEach((creneauxQuotidien: any) => { + if(!creneauxQuotidiens.find(cq => cq.date === creneauxQuotidien.date)) { + creneauxQuotidiens.push({ + date: creneauxQuotidien.date, + total: 0, + countByTag: [], + urls: [] + }) + } + const creneauxQuotidienMatchingDate = creneauxQuotidiens.find(cq => cq.date === creneauxQuotidien.date)!; + + creneauxQuotidienMatchingDate.total += creneauxQuotidien.total; + creneauxQuotidienMatchingDate.urls.push(creneauxQuotidien.url); + creneauxQuotidien.countByTag.forEach((statsCreneauxQuotidienParTag: StatsCreneauxQuotidienParTag) => { + if(!creneauxQuotidienMatchingDate.countByTag.find(cbt => cbt.tag === statsCreneauxQuotidienParTag.tag)) { + creneauxQuotidienMatchingDate.countByTag.push({ + tag: statsCreneauxQuotidienParTag.tag, + total: 0 + }); + } + + creneauxQuotidienMatchingDate.countByTag.find(cbt => cbt.tag === statsCreneauxQuotidienParTag.tag)!.total += statsCreneauxQuotidienParTag.total; + }); + }); + + return { + codeDepartements: mergedLieuxParDepartement.codeDepartements.concat(lieuxParDepartement.codeDepartement), + derniereMiseAJour: mergedLieuxParDepartement.derniereMiseAJour, + lieuxDisponibles: mergedLieuxParDepartement.lieuxDisponibles.concat(lieuxParDepartement.centres_disponibles.map(transformLieu)), + lieuxIndisponibles: mergedLieuxParDepartement.lieuxIndisponibles.concat(lieuxParDepartement.centres_indisponibles.map(transformLieu)), + creneauxQuotidiens + }; + }, { codeDepartements: [], derniereMiseAJour: principalLieuxDepartement.last_updated, lieuxDisponibles: [], - lieuxIndisponibles: [] + lieuxIndisponibles: [], + creneauxQuotidiens: [] } as LieuxParDepartement); + lieuxParDepartement.creneauxQuotidiens = ArrayBuilder.from(lieuxParDepartement.creneauxQuotidiens) + .sortBy(cq => cq.date) + .build(); + return lieuxParDepartement; } From 20e5ad8dbf4a008d6fe78150127070f12a20769c Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 23 May 2021 23:56:10 +0200 Subject: [PATCH 08/52] using dedicated component for upcoming days selector --- .../vmd-upcoming-days-selector.component.scss | 43 ++++++ .../vmd-upcoming-days-selector.component.ts | 58 ++++++++ src/index.ts | 1 + src/utils/Strings.ts | 4 + src/views/vmd-rdv.view.ts | 126 ++---------------- 5 files changed, 119 insertions(+), 113 deletions(-) create mode 100644 src/components/vmd-upcoming-days-selector.component.scss create mode 100644 src/components/vmd-upcoming-days-selector.component.ts diff --git a/src/components/vmd-upcoming-days-selector.component.scss b/src/components/vmd-upcoming-days-selector.component.scss new file mode 100644 index 000000000..0caa60ca3 --- /dev/null +++ b/src/components/vmd-upcoming-days-selector.component.scss @@ -0,0 +1,43 @@ +@import "bootstrap/scss/functions"; +@import "bootstrap/scss/mixins/breakpoints"; +@import "../styles/bootstrap-variables"; +@import "../styles/global-variables"; + +.list-group-horizontal.days li.list-group-item { + text-align: center; + cursor: pointer; + + &.selected { + border: 5px solid $primary; + padding: 5px; + } + &:not(.selected) { + padding: 9px; + border-width: 1px; + &:hover { + border: 3px solid $primary; + border-style: dashed; + padding: 7px; + } + } +} + +.cpt-rdv { + border-radius: 8px; + padding: 4px 6px; + white-space: nowrap; + background-color: $primary; color: white; + font-weight: bold; +} + +ul.days { + width: 100%; + overflow: scroll; + margin-top: 10px; + margin-bottom: 30px; + font-size: 2rem; +} +.days .day { + font-weight: bold; + white-space: nowrap; +} diff --git a/src/components/vmd-upcoming-days-selector.component.ts b/src/components/vmd-upcoming-days-selector.component.ts new file mode 100644 index 000000000..ce44dcfcf --- /dev/null +++ b/src/components/vmd-upcoming-days-selector.component.ts @@ -0,0 +1,58 @@ +import {LitElement, html, customElement, property, css, unsafeCSS} from 'lit-element'; +import upcomingDaysSelectorCss from "./vmd-upcoming-days-selector.component.scss"; +import {StatsCreneauxQuotidien} from "../state/State"; +import {CSS_Global} from "../styles/ConstructibleStyleSheets"; +import {repeat} from "lit-html/directives/repeat"; +import {classMap} from "lit-html/directives/class-map"; +import {Strings} from "../utils/Strings"; +import {format, parse} from "date-fns"; +import {fr} from "date-fns/locale"; + +@customElement('vmd-upcoming-days-selector') +export class VmdUpcomingDaysSelectorComponent extends LitElement { + + //language=css + static styles = [ + CSS_Global, + css`${unsafeCSS(upcomingDaysSelectorCss)}`, + css` + ` + ]; + + @property() statsCreneauxQuotidien: StatsCreneauxQuotidien[] = []; + @property() dateSelectionnee: string|undefined = undefined; + + constructor() { + super(); + } + + render() { + return html` +
    + ${repeat(this.statsCreneauxQuotidien, date => date, stat => html` +
  • +
    ${Strings.upperFirst(format(parse(stat.date, 'yyyy-MM-dd', new Date("1970-01-01T00:00:00Z")), 'E dd/MM', {locale: fr})).replace(".","")}
    + ${stat.total?html`${stat.total} créneau${Strings.plural(stat.total, "x")}`:html``} +
  • + `)} +
+ `; + } + + connectedCallback() { + super.connectedCallback(); + // console.log("connected callback") + } + + disconnectedCallback() { + super.disconnectedCallback(); + // console.log("disconnected callback") + } + + private jourSelectionne(stat: StatsCreneauxQuotidien) { + this.dispatchEvent(new CustomEvent('jour-selectionne', { + detail: stat + })); + + } +} diff --git a/src/index.ts b/src/index.ts index 19f386395..336798d52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ import './components/vmd-appointment-metadata.component' import './components/vmd-commune-or-departement-selector.component' import './components/vmd-button-switch.component' import './components/vmd-input-range-with-tooltip.component' +import './components/vmd-upcoming-days-selector.component' diff --git a/src/utils/Strings.ts b/src/utils/Strings.ts index b6b410a1c..551889477 100644 --- a/src/utils/Strings.ts +++ b/src/utils/Strings.ts @@ -11,6 +11,10 @@ export class Strings { return (value && value>1)?pluralForm:''; } + static upperFirst(str: string) { + return str[0].toUpperCase()+str.substring(1); + } + // FIXME move to dedicated component static toNormalizedPhoneNumber(phoneNumber: string|undefined) { if(phoneNumber === undefined) { diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index a206062ef..380a83adc 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -24,9 +24,9 @@ import { LieuxParDepartement, SearchRequest, SearchType, - State, CodeTriCentre + State, CodeTriCentre, StatsCreneauxQuotidien } from "../state/State"; -import { formatDistanceToNow, parseISO } from 'date-fns' +import {formatDistanceToNow, parseISO, startOfDay} from 'date-fns' import { fr } from 'date-fns/locale' import {Strings} from "../utils/Strings"; import {DEPARTEMENTS_LIMITROPHES} from "../utils/Departements"; @@ -51,37 +51,6 @@ export abstract class AbstractVmdRdvView extends LitElement { CSS_Global, css`${unsafeCSS(rdvViewCss)}`, css` - .list-group-horizontal.days li.list-group-item.selected { - border: 4px solid #5561d9; - padding: 5px; - } - .list-group-horizontal.days li.list-group-item:not(.selected) { - padding: 8px; - } - - .cpt-rdv { - border-radius: 8px; - padding: 4px 6px; - white-space: nowrap; - background-color: #5561d9; color: white; - font-weight: bold; - } - - ul.days { - width: 100%; - overflow: scroll; - margin-top: 10px; - margin-bottom: 30px; - font-size: 2rem; - } - .days li { - text-align: center; - } - .days .day { - font-weight: bold; - white-space: nowrap; - } - input[type=time] { line-height: 20px; width: 80px; @@ -219,7 +188,7 @@ export abstract class AbstractVmdRdvView extends LitElement { @property({type: Boolean, attribute: false}) searchInProgress: boolean = false; @property({type: Boolean, attribute: false}) miseAJourDisponible: boolean = false; @property({type: Array, attribute: false}) cartesAffichees: LieuAffichableAvecDistance[] = []; - + @internalProperty() lieuxParDepartement: LieuxParDepartement|undefined = undefined; @internalProperty() protected currentSearch: SearchRequest | void = undefined protected derniereCommuneSelectionnee: Commune|undefined = undefined; @@ -310,89 +279,20 @@ export abstract class AbstractVmdRdvView extends LitElement {
- -
    -
  • -
    Ven 21
    - 1 créneau -
  • -
  • -
    Sam 22
    -
  • -
  • -
    Dim 23
    -
  • -
  • -
    Lun 24
    -
  • -
  • -
    Mar 25
    - 20 créneaux -
  • -
  • -
    Mer 26
    - 25 créneaux -
  • -
  • -
    Jeu 27
    - 22 créneaux -
  • -
  • -
    Ven 28
    - 30 créneaux -
  • - - - - -
  • -
    Dim 30
    - 28 créneaux -
  • -
  • -
    Lun 31
    - 35 créneau -
  • -
  • -
    Mar 01/06
    - 42 créneaux -
  • -
  • -
    Mer 02/06
    - 42 créneaux -
  • -
  • -
    Jeu 03/06
    - 42 créneaux -
  • -
  • -
    Ven 04/06
    - 42 créneaux -
  • -
  • -
    Sam 05/06
    - 42 créneaux -
  • -
  • -
    Dim 06/06
    - 42 créneaux -
  • -
  • -
    Lun 07/06
    - 42 créneaux -
  • -
  • -
    Mar 08/06
    - 42 créneaux -
  • -
- + ${this.searchInProgress?html`
`:html` + +

${this.totalCreneaux.toLocaleString()} créneau${Strings.plural(this.totalCreneaux, "x")} de vaccination trouvé${Strings.plural(this.totalCreneaux)} @@ -557,9 +457,9 @@ export abstract class AbstractVmdRdvView extends LitElement { try { this.searchInProgress = true; await delay(1) // give some time (one tick) to render loader before doing the heavy lifting - const lieuxParDepartement = await State.current.lieuxPour([codeDepartement].concat(this.codeDepartementAdditionnels(codeDepartement))); + this.lieuxParDepartement = await State.current.lieuxPour([codeDepartement].concat(this.codeDepartementAdditionnels(codeDepartement))); - this.lieuxParDepartementAffiches = this.afficherLieuxParDepartement(lieuxParDepartement, currentSearch); + this.lieuxParDepartementAffiches = this.afficherLieuxParDepartement(this.lieuxParDepartement, currentSearch); this.cartesAffichees = this.infiniteScroll.ajouterCartesPaginees(this.lieuxParDepartementAffiches, []); const commune = SearchRequest.isByCommune(currentSearch) ? currentSearch.commune : undefined From 8fb46779a89e78a0dbe3d4716041d5c1cf70a2de Mon Sep 17 00:00:00 2001 From: fcamblor Date: Mon, 24 May 2021 21:06:55 +0200 Subject: [PATCH 09/52] extracted search type specificities into a dedicated/centralized SearchTypeConfig map --- src/routing/Router.ts | 20 +++++++---- src/state/State.ts | 42 ++++++++++++++++++++++ src/views/vmd-rdv.view.ts | 76 +++++++++++++++++++++++++-------------- 3 files changed, 104 insertions(+), 34 deletions(-) diff --git a/src/routing/Router.ts b/src/routing/Router.ts index f480c3b61..9fee3e350 100644 --- a/src/routing/Router.ts +++ b/src/routing/Router.ts @@ -1,7 +1,13 @@ import page from "page"; import { TemplateResult } from "lit-html"; import {html} from "lit-element"; -import {CodeTriCentre, SearchType, State} from "../state/State"; +import { + CodeTriCentre, + SearchType, + searchTypeConfigFor, + searchTypeConfigFromPathParam, + State +} from "../state/State"; import {Analytics} from "../utils/Analytics"; // @ts-ignore import {rechercheDepartementDescriptor, rechercheCommuneDescriptor} from './DynamicURLs'; @@ -57,12 +63,12 @@ class Routing { `/centres-vaccination-covid-dpt:codeDpt-:nomDpt`, rechercheDepartementDescriptor.routerUrl ], - analyticsViewName: (_) => `search_results_by_department`, + analyticsViewName: (pathParams) => searchTypeConfigFromPathParam(pathParams).analytics.searchResultsByDepartement, viewContent: async (params) => { await import('../views/vmd-rdv.view'); return (subViewSlot) => html` ${subViewSlot} ` @@ -79,12 +85,12 @@ class Routing { `/centres-vaccination-covid-dpt:codeDpt-:nomDpt/commune:codeCommune-:codePostal-:nomCommune/en-triant-par-:codeTriCentre`, rechercheCommuneDescriptor.routerUrl ], - analyticsViewName: (_) => `search_results_by_city`, + analyticsViewName: (pathParams) => searchTypeConfigFromPathParam(pathParams).analytics.searchResultsByCity, viewContent: async (params) => { await import('../views/vmd-rdv.view'); return (subViewSlot) => html` @@ -193,11 +199,11 @@ class Routing { } public getLinkToRendezVousAvecDepartement(codeDepartement: string, pathLibelleDepartement: string, searchType: SearchType) { - return `${this.basePath}centres-vaccination-covid-dpt${codeDepartement}-${pathLibelleDepartement}/recherche-${searchType}`; + return `${this.basePath}centres-vaccination-covid-dpt${codeDepartement}-${pathLibelleDepartement}/recherche-${searchTypeConfigFor(searchType).pathParam}`; } public navigateToRendezVousAvecCommune(codeTriCentre: CodeTriCentre, codeDepartement: string, pathLibelleDepartement: string, codeCommune: string, codePostal: string, pathLibelleCommune: string, searchType: SearchType) { - page(`${this.basePath}centres-vaccination-covid-dpt${codeDepartement}-${pathLibelleDepartement}/commune${codeCommune}-${codePostal}-${pathLibelleCommune}/recherche-${searchType}/en-triant-par-${codeTriCentre}`); + page(`${this.basePath}centres-vaccination-covid-dpt${codeDepartement}-${pathLibelleDepartement}/commune${codeCommune}-${codePostal}-${pathLibelleCommune}/recherche-${searchTypeConfigFor(searchType).pathParam}/en-triant-par-${codeTriCentre}`); } navigateToHome() { diff --git a/src/state/State.ts b/src/state/State.ts index 06d10b5f6..c293b1871 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -247,6 +247,48 @@ export const libelleUrlPathDeCommune = (commune: Commune) => { } export type SearchType = "standard"; +export type SearchTypeConfig = { + dailyAppointmentsExtractor: (dailyStat: StatsCreneauxQuotidien) => number; + cardAppointmentsExtractor: (lieu: Lieu) => number; + filterLieuxDisponibles: (lieux: LieuAffichableAvecDistance[]) => LieuAffichableAvecDistance[]; + pathParam: string; + standardTabSelected: boolean; + excludeAppointmentByPhoneOnly: boolean; + theme: 'standard'|'highlighted'; + analytics: { + searchResultsByDepartement: string; + searchResultsByCity: string; + } +}; +const SEARCH_TYPE_CONFIGS: {[type in SearchType]: SearchTypeConfig & {type: type}} = { + 'standard': { + type: 'standard', + dailyAppointmentsExtractor: (dailyStat) => dailyStat.total, + cardAppointmentsExtractor: (lieu) => lieu.appointment_count, + filterLieuxDisponibles: (lieux) => lieux.filter(lieu => lieu.disponible), + pathParam: 'standard', + standardTabSelected: true, + excludeAppointmentByPhoneOnly: false, + theme: 'standard', + analytics: { + searchResultsByDepartement: 'search_results_by_department', + searchResultsByCity: 'search_results_by_city' + } + } +}; +export function searchTypeConfigFromPathParam(pathParams: Record) { + const config = Object.values(SEARCH_TYPE_CONFIGS).find(config => pathParams && config.pathParam === pathParams['typeRecherche']); + if(config) { + return config; + } + throw new Error(`No config found for path param: ${pathParams['typeRecherche']}`); +} +export function searchTypeConfigFromSearch(searchRequest: SearchRequest|void, fallback: SearchType) { + return searchTypeConfigFor(searchRequest ? searchRequest.type : fallback); +} +export function searchTypeConfigFor(searchType: SearchType) { + return SEARCH_TYPE_CONFIGS[searchType]; +} export class State { diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 380a83adc..04449c523 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -22,9 +22,12 @@ import { LieuAffichableAvecDistance, LieuxAvecDistanceParDepartement, LieuxParDepartement, - SearchRequest, - SearchType, - State, CodeTriCentre, StatsCreneauxQuotidien + SearchRequest, SearchType, + State, + CodeTriCentre, + StatsCreneauxQuotidien, + searchTypeConfigFor, + searchTypeConfigFromSearch } from "../state/State"; import {formatDistanceToNow, parseISO, startOfDay} from 'date-fns' import { fr } from 'date-fns/locale' @@ -224,8 +227,9 @@ export abstract class AbstractVmdRdvView extends LitElement { } render() { - const lieuxDisponibles = (this.lieuxParDepartementAffiches && this.lieuxParDepartementAffiches.lieuxAffichables)? - this.lieuxParDepartementAffiches.lieuxAffichables.filter(l => l.disponible):[]; + const countLieuxDisponibles = searchTypeConfigFromSearch(this.currentSearch, 'standard').filterLieuxDisponibles(this.lieuxParDepartementAffiches?.lieuxAffichables || []).length; + const searchTypeConfig = searchTypeConfigFromSearch(this.currentSearch, 'standard'); + const standardMode = searchTypeConfig.standardTabSelected; return html`
@@ -317,11 +321,11 @@ export abstract class AbstractVmdRdvView extends LitElement {
- ${lieuxDisponibles.length ? html` + ${countLieuxDisponibles ? html`

- ${lieuxDisponibles.length} Lieu${Strings.plural(lieuxDisponibles.length, 'x')} de vaccination avec des disponibilités + ${countLieuxDisponibles} Lieu${Strings.plural(countLieuxDisponibles, 'x')} de vaccination avec des disponibilités

` : html` @@ -350,14 +354,14 @@ export abstract class AbstractVmdRdvView extends LitElement { return html``; })}
- ${SearchRequest.isStandardType(this.currentSearch)?html` + ${standardMode?html`

Les critères d'éligibilité sont vérifiés lors de la prise de rendez-vous

`:html``} @@ -536,10 +540,6 @@ export abstract class AbstractVmdRdvView extends LitElement { } } - protected transformLieuEnFonctionDuTypeDeRecherche(lieu: LieuAffichableAvecDistance) { - return lieu; - } - abstract libelleLieuSelectionne(): TemplateResult; // FIXME move me to a testable file abstract afficherLieuxParDepartement(lieuxParDepartement: LieuxParDepartement, search: SearchRequest): LieuxAvecDistanceParDepartement; @@ -603,15 +603,26 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { const { lieuxDisponibles, lieuxIndisponibles } = lieuxParDepartement + + let lieuxAffichablesBuilder = ArrayBuilder.from([...lieuxDisponibles].map(l => ({...l, disponible: true}))) + .concat([...lieuxIndisponibles].map(l => ({...l, disponible: false}))) + .map(l => ({ + ...l, + distance: distanceAvec(l), + appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l) + })).filter(l => + (!l.distance || l.distance < MAX_DISTANCE_CENTRE_IN_KM) + ) + if(searchTypeConfigFromSearch(this.currentSearch, 'standard').excludeAppointmentByPhoneOnly) { + lieuxAffichablesBuilder.filter(l => !l.appointment_by_phone_only) + } + + lieuxAffichablesBuilder.sortBy(l => this.extraireFormuleDeTri(l, 'distance')) + + const lieuxAffichables = lieuxAffichablesBuilder.build(); return { ...lieuxParDepartement, - lieuxAffichables: ArrayBuilder.from([...lieuxDisponibles].map(l => ({...l, disponible: true}))) - .concat([...lieuxIndisponibles].map(l => ({...l, disponible: false}))) - .map(l => ({...l, distance: distanceAvec(l) })) - .map(l => this.transformLieuEnFonctionDuTypeDeRecherche(l)) - .filter(l => !l.distance || l.distance < MAX_DISTANCE_CENTRE_IN_KM) - .sortBy(l => this.extraireFormuleDeTri(l, 'distance')) - .build() + lieuxAffichables, }; } } @@ -661,17 +672,28 @@ export class VmdRdvParDepartementView extends AbstractVmdRdvView { } // FIXME move me to testable file - afficherLieuxParDepartement(lieuxParDepartement: LieuxParDepartement): LieuxAvecDistanceParDepartement { + afficherLieuxParDepartement(lieuxParDepartement: LieuxParDepartement, search: SearchRequest): LieuxAvecDistanceParDepartement { const { lieuxDisponibles, lieuxIndisponibles } = lieuxParDepartement + let lieuxAffichablesBuilder = ArrayBuilder.from([...lieuxDisponibles].map(l => ({...l, disponible: true}))) + .concat([...lieuxIndisponibles].map(l => ({...l, disponible: false}))) + .map(l => ({ + ...l, + distance: undefined, + appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l) + })) + + if(searchTypeConfigFromSearch(this.currentSearch, 'standard').excludeAppointmentByPhoneOnly) { + lieuxAffichablesBuilder.filter(l => !l.appointment_by_phone_only) + } + + lieuxAffichablesBuilder.sortBy(l => this.extraireFormuleDeTri(l, 'distance')) + + const lieuxAffichables = lieuxAffichablesBuilder.build(); + return { ...lieuxParDepartement, - lieuxAffichables: ArrayBuilder.from([...lieuxDisponibles].map(l => ({...l, disponible: true}))) - .concat([...lieuxIndisponibles].map(l => ({...l, disponible: false}))) - .map(l => ({...l, distance: undefined })) - .map(l => this.transformLieuEnFonctionDuTypeDeRecherche(l)) - .sortBy(l => this.extraireFormuleDeTri(l, 'date')) - .build() + lieuxAffichables, }; } From 8e3ba86c1559e97116f904dec69be1296c90d77e Mon Sep 17 00:00:00 2001 From: fcamblor Date: Mon, 24 May 2021 22:50:52 +0200 Subject: [PATCH 10/52] extracted lieuxDisponibles into afficherLieuxParDepartement() impl --- src/state/InfiniteScroll.spec.ts | 1 + src/state/State.ts | 1 + src/views/vmd-rdv.view.ts | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/state/InfiniteScroll.spec.ts b/src/state/InfiniteScroll.spec.ts index e6c07943f..4c79adc0e 100644 --- a/src/state/InfiniteScroll.spec.ts +++ b/src/state/InfiniteScroll.spec.ts @@ -35,6 +35,7 @@ describe('InfiniteScroll', () => { } lieuxParDepartementAffiches = { lieuxAffichables: lieuxAffichables, + lieuxDisponibles: [], codeDepartements: [], derniereMiseAJour: new Date().toISOString() }; diff --git a/src/state/State.ts b/src/state/State.ts index c293b1871..422a2d809 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -180,6 +180,7 @@ export type LieuxParDepartement_JSON = { export type LieuAffichableAvecDistance = Lieu & { disponible: boolean, distance: number|undefined }; export type LieuxAvecDistanceParDepartement = { lieuxAffichables: LieuAffichableAvecDistance[]; + lieuxDisponibles: LieuAffichableAvecDistance[]; codeDepartements: CodeDepartement[]; derniereMiseAJour: ISODateString; }; diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 04449c523..53001d689 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -227,7 +227,7 @@ export abstract class AbstractVmdRdvView extends LitElement { } render() { - const countLieuxDisponibles = searchTypeConfigFromSearch(this.currentSearch, 'standard').filterLieuxDisponibles(this.lieuxParDepartementAffiches?.lieuxAffichables || []).length; + const countLieuxDisponibles = (this.lieuxParDepartementAffiches?.lieuxDisponibles || []).length; const searchTypeConfig = searchTypeConfigFromSearch(this.currentSearch, 'standard'); const standardMode = searchTypeConfig.standardTabSelected; @@ -623,6 +623,7 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { return { ...lieuxParDepartement, lieuxAffichables, + lieuxDisponibles: searchTypeConfigFromSearch(this.currentSearch, 'standard').filterLieuxDisponibles(lieuxAffichables) }; } } @@ -694,6 +695,7 @@ export class VmdRdvParDepartementView extends AbstractVmdRdvView { return { ...lieuxParDepartement, lieuxAffichables, + lieuxDisponibles: searchTypeConfigFromSearch(this.currentSearch, 'standard').filterLieuxDisponibles(lieuxAffichables) }; } From a37a4dfbc41762d157021ec3548b39b744312456 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Mon, 24 May 2021 23:23:37 +0200 Subject: [PATCH 11/52] introduced day-level statistics through rdvsDuJourSelectionne property --- src/components/vmd-search.component.ts | 4 +- .../vmd-upcoming-days-selector.component.ts | 10 ++- src/state/InfiniteScroll.spec.ts | 1 + src/state/State.ts | 64 ++++++++++++++++--- src/views/vmd-rdv.view.ts | 42 +++++++++--- 5 files changed, 98 insertions(+), 23 deletions(-) diff --git a/src/components/vmd-search.component.ts b/src/components/vmd-search.component.ts index d528098c0..a1286fa01 100644 --- a/src/components/vmd-search.component.ts +++ b/src/components/vmd-search.component.ts @@ -57,14 +57,14 @@ export class VmdSearchComponent extends LitElement { private onCommuneSelected (commune: Commune) { this.currentSelection = commune this.dispatchEvent(new CustomEvent('on-search', { - detail: SearchRequest.ByCommune(commune, this.currentSearchType || 'standard') + detail: SearchRequest.ByCommune(commune, this.currentSearchType || 'standard', this.currentValue ? this.currentValue.date : undefined) })) } private onDepartementSelected (departement: Departement) { this.currentSelection = departement this.dispatchEvent(new CustomEvent('on-search', { - detail: SearchRequest.ByDepartement(departement, this.currentSearchType || 'standard') + detail: SearchRequest.ByDepartement(departement, this.currentSearchType || 'standard', this.currentValue ? this.currentValue.date : undefined) })) } } diff --git a/src/components/vmd-upcoming-days-selector.component.ts b/src/components/vmd-upcoming-days-selector.component.ts index ce44dcfcf..957afe4d7 100644 --- a/src/components/vmd-upcoming-days-selector.component.ts +++ b/src/components/vmd-upcoming-days-selector.component.ts @@ -21,6 +21,7 @@ export class VmdUpcomingDaysSelectorComponent extends LitElement { @property() statsCreneauxQuotidien: StatsCreneauxQuotidien[] = []; @property() dateSelectionnee: string|undefined = undefined; + @property({attribute: false}) dailyAppointmentExtractor: (stat: StatsCreneauxQuotidien) => number = () => 0; constructor() { super(); @@ -29,12 +30,15 @@ export class VmdUpcomingDaysSelectorComponent extends LitElement { render() { return html`
    - ${repeat(this.statsCreneauxQuotidien, date => date, stat => html` + ${repeat(this.statsCreneauxQuotidien, date => date, stat => { + const appointmentCount = this.dailyAppointmentExtractor(stat); + return html`
  • ${Strings.upperFirst(format(parse(stat.date, 'yyyy-MM-dd', new Date("1970-01-01T00:00:00Z")), 'E dd/MM', {locale: fr})).replace(".","")}
    - ${stat.total?html`${stat.total} créneau${Strings.plural(stat.total, "x")}`:html``} + ${appointmentCount?html`${appointmentCount} créneau${Strings.plural(appointmentCount, "x")}`:html``}
  • - `)} + `; + })}
`; } diff --git a/src/state/InfiniteScroll.spec.ts b/src/state/InfiniteScroll.spec.ts index 4c79adc0e..b787723f3 100644 --- a/src/state/InfiniteScroll.spec.ts +++ b/src/state/InfiniteScroll.spec.ts @@ -21,6 +21,7 @@ describe('InfiniteScroll', () => { appointment_by_phone_only: false, appointment_schedules: [], plateforme: 'Doctolib', + internal_id: 'doctolib12345', prochain_rdv: '2021-05-17T09:10:00.000+02:00', metadata: { address: '1 Place de la Concorde', diff --git a/src/state/State.ts b/src/state/State.ts index 422a2d809..67e6cc1a0 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -19,10 +19,11 @@ export namespace SearchRequest { type: SearchType, par: 'departement', departement: Departement, - tri: 'date' + tri: 'date', + date: string|undefined } - export function ByDepartement (departement: Departement, type: SearchType): ByDepartement { - return { type, par: 'departement', departement, tri: 'date' } + export function ByDepartement (departement: Departement, type: SearchType, date: string|undefined): ByDepartement { + return { type, par: 'departement', departement, tri: 'date', date } } export function isByDepartement (searchRequest: SearchRequest): searchRequest is ByDepartement { return searchRequest.par === 'departement' @@ -32,10 +33,11 @@ export namespace SearchRequest { type: SearchType, par: 'commune', commune: Commune, - tri: 'distance' + tri: 'distance', + date: string|undefined } - export function ByCommune (commune: Commune, type: SearchType): ByCommune { - return { type, par: 'commune', commune, tri: 'distance' } + export function ByCommune (commune: Commune, type: SearchType, date: string|undefined): ByCommune { + return { type, par: 'commune', commune, tri: 'distance', date } } export function isByCommune (searchRequest: SearchRequest): searchRequest is ByCommune { return searchRequest.par === 'commune' @@ -124,6 +126,7 @@ export type Lieu = { appointment_schedules: AppointmentSchedule[]|undefined; plateforme: TypePlateforme; prochain_rdv: ISODateString|null; + internal_id: string; metadata: { address: string; phone_number: string|undefined; @@ -247,14 +250,29 @@ export const libelleUrlPathDeCommune = (commune: Commune) => { return Strings.toReadableURLPathValue(commune.nom); } +export type Creneau = { + debut: ISODateString; + tags: string[]; +} +export type CreneauxPourLieu = { + id: string; + creneaux: Creneau[]; +} +export type RendezVoudDuJour = { + date: string; + timezone: string; + lieux: CreneauxPourLieu[]; +} + export type SearchType = "standard"; export type SearchTypeConfig = { dailyAppointmentsExtractor: (dailyStat: StatsCreneauxQuotidien) => number; - cardAppointmentsExtractor: (lieu: Lieu) => number; + cardAppointmentsExtractor: (lieu: Lieu, creneauxPourLieu: CreneauxPourLieu|undefined) => number; filterLieuxDisponibles: (lieux: LieuAffichableAvecDistance[]) => LieuAffichableAvecDistance[]; pathParam: string; standardTabSelected: boolean; excludeAppointmentByPhoneOnly: boolean; + jourSelectionnable: boolean; theme: 'standard'|'highlighted'; analytics: { searchResultsByDepartement: string; @@ -265,11 +283,12 @@ const SEARCH_TYPE_CONFIGS: {[type in SearchType]: SearchTypeConfig & {type: type 'standard': { type: 'standard', dailyAppointmentsExtractor: (dailyStat) => dailyStat.total, - cardAppointmentsExtractor: (lieu) => lieu.appointment_count, + cardAppointmentsExtractor: (_, creneauxPourLieu) => creneauxPourLieu?creneauxPourLieu.creneaux.length:-1, filterLieuxDisponibles: (lieux) => lieux.filter(lieu => lieu.disponible), pathParam: 'standard', standardTabSelected: true, excludeAppointmentByPhoneOnly: false, + jourSelectionnable: true, theme: 'standard', analytics: { searchResultsByDepartement: 'search_results_by_department', @@ -427,4 +446,33 @@ export class State { } }; } + + async rdvDuJour(stats: StatsCreneauxQuotidien) { + const rdvQuotidiensParDepartements: RendezVoudDuJour[] = await Promise.all( + stats.urls.map(urlRdvQuotidienParDpt => + fetch(`${VMD_BASE_URL}/${urlRdvQuotidienParDpt}`, {cache: 'no-cache'}) + .then(resp => resp.json())) + ); + + return rdvQuotidiensParDepartements.reduce((rdvQuotidiensAggreges, rdvQuotidiensParDepartement) => { + rdvQuotidiensParDepartement.lieux.forEach(lieu => { + if(!rdvQuotidiensAggreges.lieux.find(l => l.id === lieu.id)) { + rdvQuotidiensAggreges.lieux.push({ + id: lieu.id, + creneaux: [] + }) + } + + Array.prototype.push.apply(rdvQuotidiensAggreges.lieux.find(l => l.id === lieu.id)!.creneaux, lieu.creneaux); + }) + + return rdvQuotidiensAggreges; + }, { + date: stats.date, + lieux: [], + // Crossing fingers we can't query a list of departments not sharing the same timezone + // (spoiler alert: this is not possible) + timezone: rdvQuotidiensParDepartements[0]?.timezone + } as RendezVoudDuJour); + } } diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 53001d689..e1ac12c61 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -26,6 +26,7 @@ import { State, CodeTriCentre, StatsCreneauxQuotidien, + RendezVoudDuJour, searchTypeConfigFor, searchTypeConfigFromSearch } from "../state/State"; @@ -194,6 +195,8 @@ export abstract class AbstractVmdRdvView extends LitElement { @internalProperty() lieuxParDepartement: LieuxParDepartement|undefined = undefined; @internalProperty() protected currentSearch: SearchRequest | void = undefined + @internalProperty() rdvsDuJourSelectionne: RendezVoudDuJour | undefined = undefined; + protected derniereCommuneSelectionnee: Commune|undefined = undefined; protected lieuBackgroundRefreshIntervalId: ReturnType|undefined = undefined; @@ -290,12 +293,14 @@ export abstract class AbstractVmdRdvView extends LitElement {
`:html` + ${this.lieuxParDepartement?.creneauxQuotidiens && searchTypeConfig.jourSelectionnable?html` + dateSelectionnee="${this.rdvsDuJourSelectionne?.date || ""}" + .statsCreneauxQuotidien="${this.lieuxParDepartement.creneauxQuotidiens}" + .dailyAppointmentExtractor="${this.currentSearch?searchTypeConfigFor(this.currentSearch.type).dailyAppointmentsExtractor:()=>0}" + @jour-selectionne="${(event: CustomEvent) => this.jourSelectionne(event.detail) }" + >`:html``}

@@ -374,7 +379,10 @@ export abstract class AbstractVmdRdvView extends LitElement { this.registerInfiniteScroll(); } - + async jourSelectionne(statJourSelectionne: StatsCreneauxQuotidien) { + this.rdvsDuJourSelectionne = await State.current.rdvDuJour(statJourSelectionne); + await this.refreshLieux(); + } async connectedCallback() { super.connectedCallback(); @@ -463,6 +471,17 @@ export abstract class AbstractVmdRdvView extends LitElement { await delay(1) // give some time (one tick) to render loader before doing the heavy lifting this.lieuxParDepartement = await State.current.lieuxPour([codeDepartement].concat(this.codeDepartementAdditionnels(codeDepartement))); + if(!this.rdvsDuJourSelectionne) { + const dailyAppointmentsExtractor = searchTypeConfigFor(currentSearch.type).dailyAppointmentsExtractor; + // Pre-selecting first date having appointments + const statsPremierJourAvecCreneaux = this.lieuxParDepartement.creneauxQuotidiens.reduce((statTrouvee, stat) => { + return statTrouvee || (dailyAppointmentsExtractor(stat) !== 0? stat : undefined); + }, undefined as StatsCreneauxQuotidien|undefined); + if(statsPremierJourAvecCreneaux) { + this.rdvsDuJourSelectionne = await State.current.rdvDuJour(statsPremierJourAvecCreneaux); + } + } + this.lieuxParDepartementAffiches = this.afficherLieuxParDepartement(this.lieuxParDepartement, currentSearch); this.cartesAffichees = this.infiniteScroll.ajouterCartesPaginees(this.lieuxParDepartementAffiches, []); @@ -574,7 +593,7 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { if (this.currentSearchMarker !== marker) { return } const commune = await State.current.autocomplete.findCommune(this._codePostalSelectionne, this._codeCommuneSelectionne) if (commune) { - this.currentSearch = SearchRequest.ByCommune(commune, this._searchType) + this.currentSearch = SearchRequest.ByCommune(commune, this._searchType, this.rdvsDuJourSelectionne?.date) this.refreshLieux() } } @@ -609,9 +628,10 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { .map(l => ({ ...l, distance: distanceAvec(l), - appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l) + appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l, this.rdvsDuJourSelectionne?.lieux.find(cpl => cpl.id === l.internal_id)) })).filter(l => (!l.distance || l.distance < MAX_DISTANCE_CENTRE_IN_KM) + && (!this.rdvsDuJourSelectionne || this.rdvsDuJourSelectionne.lieux.map(_l => _l.id).includes(l.internal_id)) ) if(searchTypeConfigFromSearch(this.currentSearch, 'standard').excludeAppointmentByPhoneOnly) { lieuxAffichablesBuilder.filter(l => !l.appointment_by_phone_only) @@ -650,7 +670,7 @@ export class VmdRdvParDepartementView extends AbstractVmdRdvView { const departements = await State.current.departementsDisponibles() const departementSelectionne = departements.find(d => d.code_departement === code); if (departementSelectionne) { - this.currentSearch = SearchRequest.ByDepartement(departementSelectionne, this._searchType) + this.currentSearch = SearchRequest.ByDepartement(departementSelectionne, this._searchType, this.rdvsDuJourSelectionne?.date) this.refreshLieux() } } @@ -681,8 +701,10 @@ export class VmdRdvParDepartementView extends AbstractVmdRdvView { .map(l => ({ ...l, distance: undefined, - appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l) - })) + appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l, this.rdvsDuJourSelectionne?.lieux.find(cpl => cpl.id === l.internal_id)) + })).filter(l => + (!this.rdvsDuJourSelectionne || this.rdvsDuJourSelectionne.lieux.map(_l => _l.id).includes(l.internal_id)) + ) if(searchTypeConfigFromSearch(this.currentSearch, 'standard').excludeAppointmentByPhoneOnly) { lieuxAffichablesBuilder.filter(l => !l.appointment_by_phone_only) From bc6ff178336dd44278149c411401c8f156ed697d Mon Sep 17 00:00:00 2001 From: fcamblor Date: Tue, 25 May 2021 04:15:05 +0200 Subject: [PATCH 12/52] hiding type vaccin on chronodose tab --- src/views/vmd-rdv.view.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index e1ac12c61..40a0906af 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -242,6 +242,7 @@ export abstract class AbstractVmdRdvView extends LitElement { @on-search="${this.onSearchSelected}" /> + ${standardMode?html`
- + `:html``}
+ ${false?html`
- + `:html``}
From 0174b74dbca387c8f651f63f2586084f2e9b05b5 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Tue, 25 May 2021 04:08:24 +0200 Subject: [PATCH 14/52] better type safety on fetched JSON in State + added cache on State.rdvDuJour() --- src/state/State.ts | 50 ++++++++++++++++++++++++++++++--------- src/views/vmd-rdv.view.ts | 4 ++-- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/state/State.ts b/src/state/State.ts index 67e6cc1a0..8cc9effc0 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -2,6 +2,7 @@ import {Strings} from "../utils/Strings"; import { Autocomplete } from './Autocomplete' import { Memoize } from 'typescript-memoize' import {ArrayBuilder} from "../utils/Arrays"; +import {formatISO} from "date-fns"; export type CodeTrancheAge = 'plus75ans'; export type TrancheAge = { @@ -164,6 +165,12 @@ export type StatsCreneauxQuotidien = { urls: string[]; countByTag: StatsCreneauxQuotidienParTag[]; } +export type StatsCreneauxQuotidien_JSON = { + date: string; // "2021-05-23" + total: number; + url: string; + countByTag: StatsCreneauxQuotidienParTag[]; +} export type LieuxParDepartement = { lieuxDisponibles: Lieu[]; lieuxIndisponibles: Lieu[]; @@ -176,7 +183,7 @@ export type LieuxParDepartements = Map; export type LieuxParDepartement_JSON = { centres_disponibles: Lieu[]; centres_indisponibles: Lieu[]; - creneaux_quotidiens: {}[]; + creneaux_quotidiens: StatsCreneauxQuotidien_JSON[]; last_updated: string; }; @@ -258,8 +265,10 @@ export type CreneauxPourLieu = { id: string; creneaux: Creneau[]; } -export type RendezVoudDuJour = { +export type RendezVousDuJour = Omit; +export type RendezVousDuJour_JSON = { date: string; + codeDepartement: CodeDepartement; timezone: string; lieux: CreneauxPourLieu[]; } @@ -296,7 +305,7 @@ const SEARCH_TYPE_CONFIGS: {[type in SearchType]: SearchTypeConfig & {type: type } } }; -export function searchTypeConfigFromPathParam(pathParams: Record) { +export function searchTypeConfigFromPathParam(pathParams: Record): SearchTypeConfig & {type: SearchType} { const config = Object.values(SEARCH_TYPE_CONFIGS).find(config => pathParams && config.pathParam === pathParams['typeRecherche']); if(config) { return config; @@ -306,7 +315,7 @@ export function searchTypeConfigFromPathParam(pathParams: Record) export function searchTypeConfigFromSearch(searchRequest: SearchRequest|void, fallback: SearchType) { return searchTypeConfigFor(searchRequest ? searchRequest.type : fallback); } -export function searchTypeConfigFor(searchType: SearchType) { +export function searchTypeConfigFor(searchType: SearchType): SearchTypeConfig & {type: SearchType} { return SEARCH_TYPE_CONFIGS[searchType]; } @@ -350,7 +359,7 @@ export class State { const lieuxParDepartement: LieuxParDepartement = [principalLieuxDepartement].concat(lieuxDepartementsAditionnels).reduce((mergedLieuxParDepartement: LieuxParDepartement, lieuxParDepartement: LieuxParDepartement_JSON & {codeDepartement: string}) => { const creneauxQuotidiens: StatsCreneauxQuotidien[] = mergedLieuxParDepartement.creneauxQuotidiens; - (lieuxParDepartement.creneaux_quotidiens || []).forEach((creneauxQuotidien: any) => { + (lieuxParDepartement.creneaux_quotidiens || []).forEach((creneauxQuotidien) => { if(!creneauxQuotidiens.find(cq => cq.date === creneauxQuotidien.date)) { creneauxQuotidiens.push({ date: creneauxQuotidien.date, @@ -447,11 +456,30 @@ export class State { }; } - async rdvDuJour(stats: StatsCreneauxQuotidien) { - const rdvQuotidiensParDepartements: RendezVoudDuJour[] = await Promise.all( - stats.urls.map(urlRdvQuotidienParDpt => - fetch(`${VMD_BASE_URL}/${urlRdvQuotidienParDpt}`, {cache: 'no-cache'}) - .then(resp => resp.json())) + private _cacheRdvDuJour: {url: string, expires: string, rdvDuJour: RendezVousDuJour}[] = []; + async rdvDuJour(stats: StatsCreneauxQuotidien): Promise { + const rdvQuotidiensParDepartements: RendezVousDuJour[] = await Promise.all( + stats.urls.map(async urlRdvQuotidienParDpt => { + const cachedRdv = this._cacheRdvDuJour.find(cachedRdv => cachedRdv.url === urlRdvQuotidienParDpt); + if(cachedRdv && Date.parse(cachedRdv.expires) > Date.now()) { + return cachedRdv.rdvDuJour; + } else { + const rdvJSON: RendezVousDuJour_JSON = await fetch(`${VMD_BASE_URL}/${urlRdvQuotidienParDpt}`, {cache: 'no-cache'}).then(resp => resp.json()); + const {codeDepartement, ...rdv } = rdvJSON; + const expiration = formatISO(Date.now() + 1000 * 60 * 3); + if(cachedRdv) { + cachedRdv.rdvDuJour = rdv; + cachedRdv.expires = expiration; + } else { + this._cacheRdvDuJour.push({ + url: urlRdvQuotidienParDpt, + rdvDuJour: rdv, + expires: expiration + }) + } + return rdv; + } + }) ); return rdvQuotidiensParDepartements.reduce((rdvQuotidiensAggreges, rdvQuotidiensParDepartement) => { @@ -473,6 +501,6 @@ export class State { // Crossing fingers we can't query a list of departments not sharing the same timezone // (spoiler alert: this is not possible) timezone: rdvQuotidiensParDepartements[0]?.timezone - } as RendezVoudDuJour); + } as RendezVousDuJour); } } diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index bd26c4c21..ca3d36580 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -26,7 +26,7 @@ import { State, CodeTriCentre, StatsCreneauxQuotidien, - RendezVoudDuJour, + RendezVousDuJour, searchTypeConfigFor, searchTypeConfigFromSearch } from "../state/State"; @@ -195,7 +195,7 @@ export abstract class AbstractVmdRdvView extends LitElement { @internalProperty() lieuxParDepartement: LieuxParDepartement|undefined = undefined; @internalProperty() protected currentSearch: SearchRequest | void = undefined - @internalProperty() rdvsDuJourSelectionne: RendezVoudDuJour | undefined = undefined; + @internalProperty() rdvsDuJourSelectionne: RendezVousDuJour | undefined = undefined; protected derniereCommuneSelectionnee: Commune|undefined = undefined; From 3edc98c04a229f9cd096aa46e6f539d50a392826 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Tue, 25 May 2021 04:13:03 +0200 Subject: [PATCH 15/52] refactored search in order to take into consideration selected day. We now have 3 steps in the displayed data resolution : 1/ We resolve location that are matching level 1 search criteria (everything except selected day) 2/ We build the daily appointments calendar based on available locations calculated during 1/ 3/ We filter available location based on selected day --- .../vmd-upcoming-days-selector.component.ts | 19 +-- src/state/InfiniteScroll.spec.ts | 8 +- src/state/InfiniteScroll.ts | 6 +- src/state/State.ts | 24 ++- src/utils/Analytics.ts | 6 +- src/views/vmd-rdv.view.ts | 160 +++++++++++------- 6 files changed, 134 insertions(+), 89 deletions(-) diff --git a/src/components/vmd-upcoming-days-selector.component.ts b/src/components/vmd-upcoming-days-selector.component.ts index 957afe4d7..1183e862c 100644 --- a/src/components/vmd-upcoming-days-selector.component.ts +++ b/src/components/vmd-upcoming-days-selector.component.ts @@ -1,6 +1,6 @@ import {LitElement, html, customElement, property, css, unsafeCSS} from 'lit-element'; import upcomingDaysSelectorCss from "./vmd-upcoming-days-selector.component.scss"; -import {StatsCreneauxQuotidien} from "../state/State"; +import {countCreneauxFor, RendezVousDuJour} from "../state/State"; import {CSS_Global} from "../styles/ConstructibleStyleSheets"; import {repeat} from "lit-html/directives/repeat"; import {classMap} from "lit-html/directives/class-map"; @@ -19,9 +19,8 @@ export class VmdUpcomingDaysSelectorComponent extends LitElement { ` ]; - @property() statsCreneauxQuotidien: StatsCreneauxQuotidien[] = []; + @property() creneauxQuotidiens: RendezVousDuJour[] = []; @property() dateSelectionnee: string|undefined = undefined; - @property({attribute: false}) dailyAppointmentExtractor: (stat: StatsCreneauxQuotidien) => number = () => 0; constructor() { super(); @@ -30,11 +29,11 @@ export class VmdUpcomingDaysSelectorComponent extends LitElement { render() { return html`
    - ${repeat(this.statsCreneauxQuotidien, date => date, stat => { - const appointmentCount = this.dailyAppointmentExtractor(stat); + ${repeat(this.creneauxQuotidiens, cq => cq.date, cq => { + const appointmentCount = countCreneauxFor(cq); return html` -
  • -
    ${Strings.upperFirst(format(parse(stat.date, 'yyyy-MM-dd', new Date("1970-01-01T00:00:00Z")), 'E dd/MM', {locale: fr})).replace(".","")}
    +
  • +
    ${Strings.upperFirst(format(parse(cq.date, 'yyyy-MM-dd', new Date("1970-01-01T00:00:00Z")), 'E dd/MM', {locale: fr})).replace(".","")}
    ${appointmentCount?html`${appointmentCount} créneau${Strings.plural(appointmentCount, "x")}`:html``}
  • `; @@ -53,9 +52,9 @@ export class VmdUpcomingDaysSelectorComponent extends LitElement { // console.log("disconnected callback") } - private jourSelectionne(stat: StatsCreneauxQuotidien) { - this.dispatchEvent(new CustomEvent('jour-selectionne', { - detail: stat + private jourSelectionne(creneauxQuotidien: RendezVousDuJour) { + this.dispatchEvent(new CustomEvent('jour-selectionne', { + detail: creneauxQuotidien })); } diff --git a/src/state/InfiniteScroll.spec.ts b/src/state/InfiniteScroll.spec.ts index b787723f3..b3ae00729 100644 --- a/src/state/InfiniteScroll.spec.ts +++ b/src/state/InfiniteScroll.spec.ts @@ -35,7 +35,7 @@ describe('InfiniteScroll', () => { }); } lieuxParDepartementAffiches = { - lieuxAffichables: lieuxAffichables, + lieuxMatchantCriteres: lieuxAffichables, lieuxDisponibles: [], codeDepartements: [], derniereMiseAJour: new Date().toISOString() @@ -55,7 +55,7 @@ describe('InfiniteScroll', () => { it('should ajouterCartesPaginees add 20 more cards', () => { // Given - const cartesAffichees = lieuxParDepartementAffiches.lieuxAffichables.slice(0, 20); + const cartesAffichees = lieuxParDepartementAffiches.lieuxMatchantCriteres.slice(0, 20); // When const output = scroll.ajouterCartesPaginees(lieuxParDepartementAffiches, cartesAffichees); @@ -66,7 +66,7 @@ describe('InfiniteScroll', () => { it('should ajouterCartesPaginees add 5 more cards', () => { // Given - const cartesAffichees = lieuxParDepartementAffiches.lieuxAffichables.slice(0, 40); + const cartesAffichees = lieuxParDepartementAffiches.lieuxMatchantCriteres.slice(0, 40); // When const output = scroll.ajouterCartesPaginees(lieuxParDepartementAffiches, cartesAffichees); @@ -77,7 +77,7 @@ describe('InfiniteScroll', () => { it('should ajouterCartesPaginees not add more cards', () => { // Given - const cartesAffichees = lieuxParDepartementAffiches.lieuxAffichables.slice(0, 45); + const cartesAffichees = lieuxParDepartementAffiches.lieuxMatchantCriteres.slice(0, 45); // When const output = scroll.ajouterCartesPaginees(lieuxParDepartementAffiches, cartesAffichees); diff --git a/src/state/InfiniteScroll.ts b/src/state/InfiniteScroll.ts index 30195b920..a0b2d3d41 100644 --- a/src/state/InfiniteScroll.ts +++ b/src/state/InfiniteScroll.ts @@ -7,14 +7,14 @@ export class InfiniteScroll { ajouterCartesPaginees(lieuxParDepartementAffiches: LieuxAvecDistanceParDepartement | undefined = undefined, cartesAffichees: LieuAffichableAvecDistance[]) { - if (!lieuxParDepartementAffiches?.lieuxAffichables || - cartesAffichees.length >= lieuxParDepartementAffiches?.lieuxAffichables.length) { + if (!lieuxParDepartementAffiches?.lieuxMatchantCriteres || + cartesAffichees.length >= lieuxParDepartementAffiches?.lieuxMatchantCriteres.length) { return cartesAffichees; } const startIndex = cartesAffichees.length - let cartesAAjouter = lieuxParDepartementAffiches.lieuxAffichables + let cartesAAjouter = lieuxParDepartementAffiches.lieuxMatchantCriteres .slice(startIndex, startIndex + PAGINATION_SIZE); return cartesAffichees.concat(cartesAAjouter); diff --git a/src/state/State.ts b/src/state/State.ts index 8cc9effc0..984c95b7a 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -189,7 +189,7 @@ export type LieuxParDepartement_JSON = { export type LieuAffichableAvecDistance = Lieu & { disponible: boolean, distance: number|undefined }; export type LieuxAvecDistanceParDepartement = { - lieuxAffichables: LieuAffichableAvecDistance[]; + lieuxMatchantCriteres: LieuAffichableAvecDistance[]; lieuxDisponibles: LieuAffichableAvecDistance[]; codeDepartements: CodeDepartement[]; derniereMiseAJour: ISODateString; @@ -266,6 +266,9 @@ export type CreneauxPourLieu = { creneaux: Creneau[]; } export type RendezVousDuJour = Omit; +export function countCreneauxFor(dailyAppointments: RendezVousDuJour) { + return dailyAppointments.lieux.reduce((total, lieu) => total + lieu.creneaux.length, 0); +} export type RendezVousDuJour_JSON = { date: string; codeDepartement: CodeDepartement; @@ -275,9 +278,9 @@ export type RendezVousDuJour_JSON = { export type SearchType = "standard"; export type SearchTypeConfig = { - dailyAppointmentsExtractor: (dailyStat: StatsCreneauxQuotidien) => number; - cardAppointmentsExtractor: (lieu: Lieu, creneauxPourLieu: CreneauxPourLieu|undefined) => number; - filterLieuxDisponibles: (lieux: LieuAffichableAvecDistance[]) => LieuAffichableAvecDistance[]; + filtrerCreneauxCompatibles: (creneaux: Creneau[]) => Creneau[]; + cardAppointmentsExtractor: (lieu: Lieu, daySelectorDisponible: boolean, creneauxPourLieu: CreneauxPourLieu|undefined) => number; + lieuConsidereCommeDisponible: (lieu: LieuAffichableAvecDistance, lieuxIdsDuJourSelectionne: string[]|undefined) => boolean; pathParam: string; standardTabSelected: boolean; excludeAppointmentByPhoneOnly: boolean; @@ -291,9 +294,11 @@ export type SearchTypeConfig = { const SEARCH_TYPE_CONFIGS: {[type in SearchType]: SearchTypeConfig & {type: type}} = { 'standard': { type: 'standard', - dailyAppointmentsExtractor: (dailyStat) => dailyStat.total, - cardAppointmentsExtractor: (_, creneauxPourLieu) => creneauxPourLieu?creneauxPourLieu.creneaux.length:-1, - filterLieuxDisponibles: (lieux) => lieux.filter(lieu => lieu.disponible), + filtrerCreneauxCompatibles: (creneaux) => creneaux, + cardAppointmentsExtractor: (lieu, daySelectorDisponible, creneauxPourLieu) => daySelectorDisponible + ?creneauxPourLieu?creneauxPourLieu.creneaux.length:0 + :lieu.appointment_count, + lieuConsidereCommeDisponible: (lieu, lieuxIdsDuJourSelectionne) => lieu.appointment_by_phone_only || !lieuxIdsDuJourSelectionne || lieuxIdsDuJourSelectionne.includes(lieu.internal_id), pathParam: 'standard', standardTabSelected: true, excludeAppointmentByPhoneOnly: false, @@ -503,4 +508,9 @@ export class State { timezone: rdvQuotidiensParDepartements[0]?.timezone } as RendezVousDuJour); } + + async rdvDesJours(stats: StatsCreneauxQuotidien[]): Promise { + const rdvDesJours = await Promise.all(stats.map(stat => this.rdvDuJour(stat))); + return rdvDesJours; + } } diff --git a/src/utils/Analytics.ts b/src/utils/Analytics.ts index d79d280ff..721c61eda 100644 --- a/src/utils/Analytics.ts +++ b/src/utils/Analytics.ts @@ -63,11 +63,11 @@ export class Analytics { 'event': commune?'search_by_commune':'search_by_departement', 'search_departement': codeDepartement, 'search_commune' : commune?`${commune.codePostal} - ${commune.nom} (${commune.code})`:undefined, - 'search_nb_appointments' : resultats?resultats.lieuxAffichables.reduce((totalDoses, lieu) => totalDoses+lieu.appointment_count, 0):undefined, - 'search_nb_lieu_vaccination' : resultats?resultats.lieuxAffichables + 'search_nb_appointments' : resultats?resultats.lieuxDisponibles.reduce((totalDoses, lieu) => totalDoses+lieu.appointment_count, 0):undefined, + 'search_nb_lieu_vaccination' : resultats?resultats.lieuxDisponibles .filter(isLieuActif) .length:undefined, - 'search_nb_lieu_vaccination_inactive' : resultats?resultats.lieuxAffichables + 'search_nb_lieu_vaccination_inactive' : resultats?resultats.lieuxDisponibles .filter(l => !isLieuActif(l)) .length:undefined, 'search_sort_type': triCentre, diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index ca3d36580..5b21a92b9 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -25,10 +25,9 @@ import { SearchRequest, SearchType, State, CodeTriCentre, - StatsCreneauxQuotidien, RendezVousDuJour, searchTypeConfigFor, - searchTypeConfigFromSearch + searchTypeConfigFromSearch, SearchTypeConfig, countCreneauxFor } from "../state/State"; import {formatDistanceToNow, parseISO, startOfDay} from 'date-fns' import { fr } from 'date-fns/locale' @@ -188,14 +187,16 @@ export abstract class AbstractVmdRdvView extends LitElement { ` ]; - @property({type: Array, attribute: false}) lieuxParDepartementAffiches: LieuxAvecDistanceParDepartement | undefined = undefined; + @internalProperty() lieuxParDepartementAffiches: LieuxAvecDistanceParDepartement | undefined = undefined; + @internalProperty() creneauxQuotidiensDetaillesAffiches: RendezVousDuJour[] = []; @property({type: Boolean, attribute: false}) searchInProgress: boolean = false; @property({type: Boolean, attribute: false}) miseAJourDisponible: boolean = false; @property({type: Array, attribute: false}) cartesAffichees: LieuAffichableAvecDistance[] = []; @internalProperty() lieuxParDepartement: LieuxParDepartement|undefined = undefined; @internalProperty() protected currentSearch: SearchRequest | void = undefined - @internalProperty() rdvsDuJourSelectionne: RendezVousDuJour | undefined = undefined; + @internalProperty() creneauxQuotidiensDetailles: RendezVousDuJour[] = []; + @internalProperty() jourSelectionne: string|undefined = undefined; protected derniereCommuneSelectionnee: Commune|undefined = undefined; @@ -208,10 +209,14 @@ export abstract class AbstractVmdRdvView extends LitElement { return 0; } return this.lieuxParDepartementAffiches - .lieuxAffichables + .lieuxDisponibles .reduce((total, lieu) => total+lieu.appointment_count, 0); } + get daySelectorAvailable(): boolean { + return !!this.lieuxParDepartement?.creneauxQuotidiens.length && !!this.currentSearch && searchTypeConfigFor(this.currentSearch.type).jourSelectionnable; + } + async onSearchSelected (event: CustomEvent) { const search = event.detail this.goToNewSearch(search) @@ -242,7 +247,7 @@ export abstract class AbstractVmdRdvView extends LitElement { @on-search="${this.onSearchSelected}" /> - ${standardMode?html` + ${this.daySelectorAvailable?html`
    `:html` - ${this.lieuxParDepartement?.creneauxQuotidiens && searchTypeConfig.jourSelectionnable?html` + ${this.daySelectorAvailable?html` `:html``}

    { - return statTrouvee || (dailyAppointmentsExtractor(stat) !== 0? stat : undefined); - }, undefined as StatsCreneauxQuotidien|undefined); - if(statsPremierJourAvecCreneaux) { - this.rdvsDuJourSelectionne = await State.current.rdvDuJour(statsPremierJourAvecCreneaux); - } - } - - this.lieuxParDepartementAffiches = this.afficherLieuxParDepartement(this.lieuxParDepartement, currentSearch); + this.rafraichirDonneesAffichees(); this.cartesAffichees = this.infiniteScroll.ajouterCartesPaginees(this.lieuxParDepartementAffiches, []); const commune = SearchRequest.isByCommune(currentSearch) ? currentSearch.commune : undefined @@ -503,6 +492,75 @@ export abstract class AbstractVmdRdvView extends LitElement { } } + private rafraichirDonneesAffichees() { + if(this.currentSearch && this.lieuxParDepartement && this.creneauxQuotidiensDetailles) { + const searchTypeConfig = searchTypeConfigFor(this.currentSearch.type); + const lieuxMatchantCriteres = this.filtrerLieuxMatchantLesCriteres(this.lieuxParDepartement, this.currentSearch); + // On calcule les créneaux quotidiens en fonction des lieux matchant les critères + this.creneauxQuotidiensDetaillesAffiches = this.filtrerCreneauxQuotidiensEnFonctionDesLieuxMatchantLesCriteres(this.creneauxQuotidiensDetailles, lieuxMatchantCriteres, searchTypeConfig); + + let daySelectorAvailable = this.daySelectorAvailable; + if(daySelectorAvailable) { + // On voit quel jour selectionner: + // 1/ on essaie de conserver le même jour selectionné si possible + // 2/ si pas possible (pas de créneau) on prend le premier jour dispo avec des créneaux + // 3/ si pas possible (aucun jour avec des créneaux) aucun jour n'est sélectionné + if(this.jourSelectionne) { + const creneauxQuotidienSelectionnes = this.creneauxQuotidiensDetaillesAffiches.find(cq => cq.date === this.jourSelectionne); + if(!creneauxQuotidienSelectionnes || countCreneauxFor(creneauxQuotidienSelectionnes)===0) { + this.jourSelectionne = undefined; + } + } + if(!this.jourSelectionne) { + this.jourSelectionne = this.creneauxQuotidiensDetaillesAffiches.filter(dailyAppointments => countCreneauxFor(dailyAppointments) !== 0)[0]?.date; + } + } else { + this.jourSelectionne = undefined; + } + + // On calcule les lieux affichés en fonction du jour sélectionné + const creneauxQuotidienSelectionnes = this.creneauxQuotidiensDetaillesAffiches.find(cq => cq.date === this.jourSelectionne); + const lieuxIdsAvecCreneauxDuJourSelectionne = creneauxQuotidienSelectionnes + ?creneauxQuotidienSelectionnes.lieux.filter(l => l.creneaux.length).map(l => l.id) + :undefined; + const lieuxMatchantCriteresAvecCountRdvMAJ = lieuxMatchantCriteres.map(l => ({ + ...l, + appointment_count: searchTypeConfig.cardAppointmentsExtractor(l, daySelectorAvailable, creneauxQuotidienSelectionnes?.lieux.find(cpl => cpl.id === l.internal_id)) + })); + + let lieuxDisponiblesAffiches = lieuxMatchantCriteresAvecCountRdvMAJ + .filter(l => searchTypeConfig.lieuConsidereCommeDisponible(l, lieuxIdsAvecCreneauxDuJourSelectionne)) + .map(l => ({ + ...l, + disponible: true + })); + let lieuxIndisponiblesAffiches = lieuxMatchantCriteresAvecCountRdvMAJ + .filter(l => !searchTypeConfig.lieuConsidereCommeDisponible(l, lieuxIdsAvecCreneauxDuJourSelectionne)) + .map(l => ({ + ...l, + disponible: false + })); + this.lieuxParDepartementAffiches = { + derniereMiseAJour: this.lieuxParDepartement.derniereMiseAJour, + codeDepartements: this.lieuxParDepartement.codeDepartements, + lieuxMatchantCriteres: lieuxDisponiblesAffiches.concat(lieuxIndisponiblesAffiches), + lieuxDisponibles: lieuxDisponiblesAffiches + }; + } + } + + private filtrerCreneauxQuotidiensEnFonctionDesLieuxMatchantLesCriteres(creneauxQuotidiensDetailles: RendezVousDuJour[], lieuxMatchantCriteres: LieuAffichableAvecDistance[], searchTypeConfig: SearchTypeConfig): RendezVousDuJour[] { + const lieuIdsMatchantCriteres = lieuxMatchantCriteres.map(l => l.internal_id); + return creneauxQuotidiensDetailles.map(rdvDuJour => { + return { + ...rdvDuJour, + lieux: rdvDuJour.lieux + .filter(l => lieuIdsMatchantCriteres.includes(l.id)) + .map(l => ({...l, creneaux: searchTypeConfig.filtrerCreneauxCompatibles(l.creneaux) })) + }; + }) + } + private prendreRdv(lieu: Lieu) { if(this.currentSearch && SearchRequest.isByCommune(this.currentSearch) && lieu.url) { Analytics.INSTANCE.clickSurRdv(lieu, this.currentTri(), this.currentSearch.type, this.currentSearch.commune); @@ -563,7 +621,7 @@ export abstract class AbstractVmdRdvView extends LitElement { abstract libelleLieuSelectionne(): TemplateResult; // FIXME move me to a testable file - abstract afficherLieuxParDepartement(lieuxParDepartement: LieuxParDepartement, search: SearchRequest): LieuxAvecDistanceParDepartement; + abstract filtrerLieuxMatchantLesCriteres(lieuxParDepartement: LieuxParDepartement, search: SearchRequest): LieuAffichableAvecDistance[]; } @customElement('vmd-rdv-par-commune') @@ -595,7 +653,7 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { if (this.currentSearchMarker !== marker) { return } const commune = await State.current.autocomplete.findCommune(this._codePostalSelectionne, this._codeCommuneSelectionne) if (commune) { - this.currentSearch = SearchRequest.ByCommune(commune, this._searchType, this.rdvsDuJourSelectionne?.date) + this.currentSearch = SearchRequest.ByCommune(commune, this._searchType, this.jourSelectionne) this.refreshLieux() } } @@ -618,35 +676,24 @@ export class VmdRdvParCommuneView extends AbstractVmdRdvView { } // FIXME move me to testable file - afficherLieuxParDepartement(lieuxParDepartement: LieuxParDepartement, search: SearchRequest.ByCommune): LieuxAvecDistanceParDepartement { + filtrerLieuxMatchantLesCriteres(lieuxParDepartement: LieuxParDepartement, search: SearchRequest.ByCommune): LieuAffichableAvecDistance[] { const origin = search.commune const distanceAvec = (lieu: Lieu) => (lieu.location ? distanceEntreDeuxPoints(origin, lieu.location) : Infinity) - const { lieuxDisponibles, lieuxIndisponibles } = lieuxParDepartement let lieuxAffichablesBuilder = ArrayBuilder.from([...lieuxDisponibles].map(l => ({...l, disponible: true}))) .concat([...lieuxIndisponibles].map(l => ({...l, disponible: false}))) - .map(l => ({ - ...l, - distance: distanceAvec(l), - appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l, this.rdvsDuJourSelectionne?.lieux.find(cpl => cpl.id === l.internal_id)) - })).filter(l => - (!l.distance || l.distance < MAX_DISTANCE_CENTRE_IN_KM) - && (!this.rdvsDuJourSelectionne || this.rdvsDuJourSelectionne.lieux.map(_l => _l.id).includes(l.internal_id)) - ) + .map(l => ({ ...l, distance: distanceAvec(l) }) + ).filter(l => (!l.distance || l.distance < MAX_DISTANCE_CENTRE_IN_KM)) if(searchTypeConfigFromSearch(this.currentSearch, 'standard').excludeAppointmentByPhoneOnly) { lieuxAffichablesBuilder.filter(l => !l.appointment_by_phone_only) } lieuxAffichablesBuilder.sortBy(l => this.extraireFormuleDeTri(l, 'distance')) - const lieuxAffichables = lieuxAffichablesBuilder.build(); - return { - ...lieuxParDepartement, - lieuxAffichables, - lieuxDisponibles: searchTypeConfigFromSearch(this.currentSearch, 'standard').filterLieuxDisponibles(lieuxAffichables) - }; + const lieuxMatchantCriteres = lieuxAffichablesBuilder.build(); + return lieuxMatchantCriteres; } } @@ -672,7 +719,7 @@ export class VmdRdvParDepartementView extends AbstractVmdRdvView { const departements = await State.current.departementsDisponibles() const departementSelectionne = departements.find(d => d.code_departement === code); if (departementSelectionne) { - this.currentSearch = SearchRequest.ByDepartement(departementSelectionne, this._searchType, this.rdvsDuJourSelectionne?.date) + this.currentSearch = SearchRequest.ByDepartement(departementSelectionne, this._searchType, this.jourSelectionne) this.refreshLieux() } } @@ -695,18 +742,12 @@ export class VmdRdvParDepartementView extends AbstractVmdRdvView { } // FIXME move me to testable file - afficherLieuxParDepartement(lieuxParDepartement: LieuxParDepartement, search: SearchRequest): LieuxAvecDistanceParDepartement { + filtrerLieuxMatchantLesCriteres(lieuxParDepartement: LieuxParDepartement /*, search: SearchRequest */): LieuAffichableAvecDistance[] { const { lieuxDisponibles, lieuxIndisponibles } = lieuxParDepartement let lieuxAffichablesBuilder = ArrayBuilder.from([...lieuxDisponibles].map(l => ({...l, disponible: true}))) .concat([...lieuxIndisponibles].map(l => ({...l, disponible: false}))) - .map(l => ({ - ...l, - distance: undefined, - appointment_count: searchTypeConfigFor(search.type).cardAppointmentsExtractor(l, this.rdvsDuJourSelectionne?.lieux.find(cpl => cpl.id === l.internal_id)) - })).filter(l => - (!this.rdvsDuJourSelectionne || this.rdvsDuJourSelectionne.lieux.map(_l => _l.id).includes(l.internal_id)) - ) + .map(l => ({ ...l, distance: undefined })) if(searchTypeConfigFromSearch(this.currentSearch, 'standard').excludeAppointmentByPhoneOnly) { lieuxAffichablesBuilder.filter(l => !l.appointment_by_phone_only) @@ -714,13 +755,8 @@ export class VmdRdvParDepartementView extends AbstractVmdRdvView { lieuxAffichablesBuilder.sortBy(l => this.extraireFormuleDeTri(l, 'distance')) - const lieuxAffichables = lieuxAffichablesBuilder.build(); - - return { - ...lieuxParDepartement, - lieuxAffichables, - lieuxDisponibles: searchTypeConfigFromSearch(this.currentSearch, 'standard').filterLieuxDisponibles(lieuxAffichables) - }; + const lieuxMatchantCriteres = lieuxAffichablesBuilder.build(); + return lieuxMatchantCriteres; } currentCritereTri(): CodeTriCentre { From 1e9b4d51a72b925847958803b684a380232af2e8 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Tue, 25 May 2021 04:15:18 +0200 Subject: [PATCH 16/52] introduced 18-55 search --- src/state/State.ts | 24 ++++++++++++++++++++++-- src/views/vmd-rdv.view.ts | 7 ++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/state/State.ts b/src/state/State.ts index 984c95b7a..5e33af50f 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -276,7 +276,7 @@ export type RendezVousDuJour_JSON = { lieux: CreneauxPourLieu[]; } -export type SearchType = "standard"; +export type SearchType = "standard"|"18_55"; export type SearchTypeConfig = { filtrerCreneauxCompatibles: (creneaux: Creneau[]) => Creneau[]; cardAppointmentsExtractor: (lieu: Lieu, daySelectorDisponible: boolean, creneauxPourLieu: CreneauxPourLieu|undefined) => number; @@ -308,7 +308,27 @@ const SEARCH_TYPE_CONFIGS: {[type in SearchType]: SearchTypeConfig & {type: type searchResultsByDepartement: 'search_results_by_department', searchResultsByCity: 'search_results_by_city' } - } + }, + '18_55': { + type: '18_55', + filtrerCreneauxCompatibles: (creneaux) => creneaux.filter(c => c.tags.includes('preco18_55')), + cardAppointmentsExtractor: (_, daySelectorDisponible, creneauxPourLieu) => { + if(daySelectorDisponible) { + return creneauxPourLieu?creneauxPourLieu.creneaux.filter(cbt => cbt.tags.includes('preco18_55')).length:0; + } + throw new Error("We're not supposed to call cardAppointmentsExtractor() on 18_55 without day selector !") + }, + lieuConsidereCommeDisponible: (lieu, lieuxIdsDuJourSelectionne) => lieu.appointment_by_phone_only || !lieuxIdsDuJourSelectionne || lieuxIdsDuJourSelectionne.includes(lieu.internal_id), + pathParam: '18_55', + standardTabSelected: true, + excludeAppointmentByPhoneOnly: false, + jourSelectionnable: true, + theme: 'standard', + analytics: { + searchResultsByDepartement: 'search_results_by_department_18_55', + searchResultsByCity: 'search_results_by_city_18_55' + } + }, }; export function searchTypeConfigFromPathParam(pathParams: Record): SearchTypeConfig & {type: SearchType} { const config = Object.values(SEARCH_TYPE_CONFIGS).find(config => pathParams && config.pathParam === pathParams['typeRecherche']); diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 5b21a92b9..f8ecceacf 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -254,9 +254,10 @@ export abstract class AbstractVmdRdvView extends LitElement {
    - + codeSelectionne="${this.currentSearch ? this.currentSearch.type : '18_55'}" + .options="${[{code:"18_55", libelle: "Préconisé pour les 18-55 ans"}, {code:"standard", libelle: "Tous"}]}" + @changed="${(e: CustomEvent<{value: SearchType}>) => this.updateSearchTypeTo(e.detail.value)}" + >
    `:html``}
    From ea68a8152f9ec2057a2a6a82621179fa108ef047 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Tue, 25 May 2021 12:22:29 +0200 Subject: [PATCH 17/52] =?UTF-8?q?[temporaire]=20champ=20distance=20planqu?= =?UTF-8?q?=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/vmd-rdv.view.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index f8ecceacf..48f4b0926 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -260,6 +260,7 @@ export abstract class AbstractVmdRdvView extends LitElement { >
    `:html``} + ${false?html`
    - + `:html``} ${false?html`
    `:html``} - ${false?html` -
    - -
    - -
    -
    `:html``} + ${this.options.criteresDeRechercheAdditionnels()} ${false?html`