this.prendreRdv()}">
+
+ let cardConfig: {cardLink:(content: TemplateResult) => TemplateResult, estCliquable: boolean, disabledBG: boolean, actions: TemplateResult|undefined, libelleDateAbsente: string };
+ let typeLieu = typeActionPour(this.lieu);
+ if(typeLieu === 'actif-via-plateforme' || typeLieu === 'inactif-via-plateforme') {
+ let specificCardConfig: { disabledBG: boolean, libelleDateAbsente: string, libelleBouton: string, typeBouton: 'btn-info'|'btn-primary', onclick: ()=>void };
+ if(typeLieu === 'inactif-via-plateforme') {
+ specificCardConfig = {
+ disabledBG: true,
+ libelleDateAbsente: 'Aucun rendez-vous',
+ libelleBouton: 'Vérifier le centre de vaccination',
+ typeBouton: 'btn-info',
+ onclick: () => this.verifierRdv()
+ };
+ } else {
+ specificCardConfig = {
+ disabledBG: false,
+ libelleDateAbsente: 'Date inconnue',
+ libelleBouton: 'Prendre rendez-vous',
+ typeBouton: 'btn-primary',
+ onclick: () => this.prendreRdv()
+ };
+ }
+ cardConfig = {
+ estCliquable: true,
+ disabledBG: specificCardConfig.disabledBG,
+ libelleDateAbsente: specificCardConfig.libelleDateAbsente,
+ cardLink: (content) =>
+ html`
{ specificCardConfig.onclick(); e.preventDefault(); } }">${content}`,
+ actions: html`
+
e.preventDefault()}"
+ class="btn btn-lg ${classMap({ 'btn-primary': specificCardConfig.typeBouton==='btn-primary', 'btn-info': specificCardConfig.typeBouton==='btn-info' })}">
+ ${specificCardConfig.libelleBouton}
+
+
+
+ ${this.lieu.appointment_count.toLocaleString()} créneau${Strings.plural(this.lieu.appointment_count, "x")}
+
+ ${this.lieu.plateforme?html`
+ |
+
+ ${plateforme?html`
+
+ `:html`
+ ${this.lieu.plateforme}
+ `}
+
+ `:html``}
+
+ `
+ };
+ } else if(typeLieu === 'actif-via-tel') {
+ cardConfig = {
+ estCliquable: true,
+ disabledBG: false,
+ libelleDateAbsente: 'Réservation tél uniquement',
+ cardLink: (content) => html`
+
+ ${content}
+ `,
+ actions: html`
+
+ Appeler le ${Strings.toNormalizedPhoneNumber(this.lieu.metadata.phone_number)}
+
+ `
+ };
+ } else if(typeLieu === 'inactif') {
+ cardConfig = {
+ estCliquable: false,
+ disabledBG: true,
+ libelleDateAbsente: 'Aucun rendez-vous',
+ cardLink: (content) => content,
+ actions: undefined
+ };
+ } else {
+ throw new Error(`Unsupported typeLieu : ${typeLieu}`)
+ }
+
+ return cardConfig.cardLink(html`
+
-
${Dates.isoToFRDatetime(this.lieu.prochain_rdv)}${distance ? `- ${distance} km` : ''}
+
+ ${this.lieu.prochain_rdv?Dates.isoToFRDatetime(this.lieu.prochain_rdv):cardConfig.libelleDateAbsente}
+ ${distance ? `- ${distance} km` : ''}
+
- ${this.estCliquable?html`
+ ${cardConfig.actions?html`
-
Prendre rendez-vous
-
-
- ${this.lieu.appointment_count.toLocaleString()} dose${Strings.plural(this.lieu.appointment_count)}
-
- ${this.lieu.plateforme?html`
- |
-
- ${plateforme?html`
-
- `:html`
- ${this.lieu.plateforme}
- `}
-
- `:html``}
-
+ ${cardConfig.actions}
`:html``}
- `;
- } else {
- return html`
-
this.verifierRdv()}">
-
-
-
-
Aucun rendez-vous
-
-
- ${this.lieu.nom}
-
- ${this.lieu.metadata.address}
-
-
-
-
- ${this.estCliquable?html`
-
- `:html``}
-
-
-
- `;
- }
+ `);
}
connectedCallback() {
diff --git a/src/components/vmd-commune-selector.component.ts b/src/components/vmd-commune-selector.component.ts
index 545335b03..3143bcf8a 100644
--- a/src/components/vmd-commune-selector.component.ts
+++ b/src/components/vmd-commune-selector.component.ts
@@ -1,4 +1,12 @@
-import {css, customElement, html, LitElement, property, unsafeCSS} from 'lit-element';
+import {
+ css,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ unsafeCSS
+} from 'lit-element';
import {classMap} from "lit-html/directives/class-map";
import {Commune, Departement} from "../state/State";
import {repeat} from "lit-html/directives/repeat";
@@ -26,12 +34,12 @@ export class VmdCommuneSelectorComponent extends LitElement {
@property({type: String}) codeCommuneSelectionne: string | undefined = undefined;
- @property({type: Boolean, attribute: false}) inputHasFocus: boolean = false;
+ @internalProperty() inputHasFocus: boolean = false;
@property({type: Boolean, attribute: false}) inputModeFixedToText = true;
@property({type: String, attribute: false}) inputMode: 'numeric'|'text' = 'numeric';
@property({type: Array, attribute: false}) autocompleteTriggers: Set
|undefined;
- @property({type: Boolean, attribute: false}) recuperationCommunesEnCours: boolean = false;
+ @internalProperty() recuperationCommunesEnCours: boolean = false;
@property({type: Array, attribute: false}) set communesDisponibles(cd: Commune[]|undefined) {
if(cd !== this._communesDisponibles) {
this._communesDisponibles = cd;
@@ -43,14 +51,19 @@ export class VmdCommuneSelectorComponent extends LitElement {
get communesDisponibles(): Commune[]|undefined{ return this._communesDisponibles; }
private _communesDisponibles: Commune[]|undefined = undefined;
- @property({type: Array, attribute: false}) communesAffichees: Commune[]|undefined = undefined;
- @property({type: String, attribute: false}) filter: string = "";
+ @internalProperty() communesAffichees: Commune[]|undefined = undefined;
+ @internalProperty() filter: string = "";
private filterMatchingAutocomplete: string|undefined = undefined;
get showDropdown() {
return this.inputHasFocus
- && ((this.inputMode === 'text' && this.communesAffichees && this.communesAffichees.length)
+ && this.filter
+ // This one is done because otherwise we would start showing some departments matching
+ // first digit, and this would encourage search by department (whereas search by commune
+ // is by far better)
+ && this.filter.length >= 2
+ && ((this.inputMode === 'text' && !this.dropDownVide())
|| this.inputMode === 'numeric');
}
@@ -62,6 +75,14 @@ export class VmdCommuneSelectorComponent extends LitElement {
return undefined;
}
+ protected aucuneCommuneAffichee(): boolean {
+ return !this.communesAffichees || !this.communesAffichees.length;
+ }
+
+ protected dropDownVide(): boolean {
+ return this.aucuneCommuneAffichee();
+ }
+
private filtrerCommunesAffichees() {
// /!\ important note : this is important to have the same implementation of toFullTextSearchableString()
// function here, than the one used in communes-import.js tooling
@@ -175,7 +196,7 @@ export class VmdCommuneSelectorComponent extends LitElement {
`:html``}
${this.showDropdown?html`
- ${(this.inputMode==='numeric' && (!this.communesAffichees || !this.communesAffichees.length))?html`
+ ${(this.inputMode==='numeric' && this.aucuneCommuneAffichee())?html`
- { this.inputMode='text'; this.shadowRoot!.querySelector("input")!.focus(); }}">Je ne connais pas le code postal
`:html``}
${this.renderListItems()}
@@ -212,9 +233,21 @@ export class VmdCommuneSelectorComponent extends LitElement {
}
+type DepartementRecherchable = Departement & {
+ fullTextSearchableNom: string;
+ fullTextSearchableCodeDepartement: string;
+};
+
@customElement('vmd-commune-or-departement-selector')
export class VmdCommuneOrDepartmentSelectorComponent extends VmdCommuneSelectorComponent {
- @property({type: Array, attribute: false}) departementsDisponibles: Departement[] = [];
+ @property({type: Array, attribute: false}) set departementsDisponibles(dpts: Departement[]) {
+ this.departementsCherchables = dpts.map(d => ({...d,
+ fullTextSearchableCodeDepartement: Strings.toFullTextSearchableString(d.code_departement),
+ fullTextSearchableNom: Strings.toFullTextSearchableString(d.nom_departement)
+ }));
+ }
+ @internalProperty() departementsCherchables: DepartementRecherchable[] = [];
+
@property({type: Array, attribute: false}) departementsAffiches: Departement[] = [];
departementSelectionne(dpt: Departement) {
@@ -235,15 +268,17 @@ export class VmdCommuneOrDepartmentSelectorComponent extends VmdCommuneSelectorC
this.filtrerDepartementsAffichees();
}
+ protected dropDownVide(): boolean {
+ return this.aucuneCommuneAffichee() && !this.departementsAffiches.length;
+ }
+
private filtrerDepartementsAffichees() {
const fullTextSearchableQuery = Strings.toFullTextSearchableString(this.filter)
- this.departementsAffiches = this.departementsDisponibles?this.departementsDisponibles.filter(dpt => {
- const fullTextSearchableNomCommune = Strings.toFullTextSearchableString(dpt.nom_departement)
-
- return dpt.code_departement.indexOf(fullTextSearchableQuery) === 0
- || fullTextSearchableNomCommune.indexOf(fullTextSearchableQuery) !== -1;
- }):[];
+ this.departementsAffiches = this.departementsCherchables.filter(dpt => {
+ return dpt.fullTextSearchableCodeDepartement.indexOf(fullTextSearchableQuery) === 0
+ || dpt.fullTextSearchableNom.indexOf(fullTextSearchableQuery) !== -1;
+ });
}
renderListItems(): TemplateResult|DirectiveFn {
diff --git a/src/state/State.ts b/src/state/State.ts
index 952a71d0c..48c3adfa9 100644
--- a/src/state/State.ts
+++ b/src/state/State.ts
@@ -75,6 +75,7 @@ export type Lieu = {
location: Coordinates,
nom: string;
url: string;
+ appointment_by_phone_only: boolean;
plateforme: string;
prochain_rdv: ISODateString|null;
metadata: {
@@ -113,11 +114,24 @@ export type LieuxParDepartement = {
};
export type LieuxParDepartements = Map;
-export type LieuAvecDistance = Lieu & { distance: number|undefined };
-export type LieuxAvecDistanceParDepartement = LieuxParDepartement & {
- lieuxDisponibles: LieuAvecDistance[];
- lieuxIndisponibles: LieuAvecDistance[];
+export type LieuAffichableAvecDistance = Lieu & { disponible: boolean, distance: number|undefined };
+export type LieuxAvecDistanceParDepartement = {
+ lieuxAffichables: LieuAffichableAvecDistance[];
+ codeDepartements: CodeDepartement[];
+ derniereMiseAJour: ISODateString;
};
+export function typeActionPour(lieuAffichable: LieuAffichableAvecDistance): 'actif-via-plateforme'|'inactif-via-plateforme'|'actif-via-tel'|'inactif' {
+ const phoneOnly = lieuAffichable.appointment_by_phone_only && lieuAffichable.metadata.phone_number;
+ if(phoneOnly) { // Phone only may have url, but we should ignore it !
+ return 'actif-via-tel';
+ } else if(lieuAffichable && lieuAffichable.appointment_count !== 0){
+ return 'actif-via-plateforme';
+ } else if(lieuAffichable && lieuAffichable.appointment_count === 0){
+ return 'inactif-via-plateforme';
+ } else {
+ return 'inactif';
+ }
+}
function convertDepartementForSort(codeDepartement: CodeDepartement) {
switch(codeDepartement) {
@@ -173,8 +187,8 @@ export class State {
}
private _lieuxParDepartement: LieuxParDepartements = new Map();
- async lieuxPour(codeDepartement: CodeDepartement): Promise {
- if(this._lieuxParDepartement.has(codeDepartement)) {
+ async lieuxPour(codeDepartement: CodeDepartement, avoidCache: boolean = false): Promise {
+ if(this._lieuxParDepartement.has(codeDepartement) && !avoidCache) {
return Promise.resolve(this._lieuxParDepartement.get(codeDepartement)!);
} else {
const resp = await fetch(`${VMD_BASE_URL}/${codeDepartement}.json`)
diff --git a/src/utils/Analytics.ts b/src/utils/Analytics.ts
index 4fa5a09c8..6885f8d05 100644
--- a/src/utils/Analytics.ts
+++ b/src/utils/Analytics.ts
@@ -2,7 +2,7 @@ import {
CodeDepartement, CodeTriCentre,
Commune,
Lieu,
- LieuxAvecDistanceParDepartement,
+ LieuxAvecDistanceParDepartement, typeActionPour,
} from "../state/State";
@@ -61,9 +61,15 @@ 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_doses' : resultats?resultats.lieuxDisponibles.reduce((totalDoses, lieu) => totalDoses+lieu.appointment_count, 0):undefined,
- 'search_nb_lieu_vaccination' : resultats?resultats.lieuxDisponibles.length:undefined,
- 'search_nb_lieu_vaccination_inactive' : resultats?resultats.lieuxIndisponibles.length:undefined,
+ // kept for legacy reasons
+ 'search_nb_doses' : resultats?resultats.lieuxAffichables.reduce((totalDoses, lieu) => totalDoses+lieu.appointment_count, 0):undefined,
+ 'search_nb_appointments' : resultats?resultats.lieuxAffichables.reduce((totalDoses, lieu) => totalDoses+lieu.appointment_count, 0):undefined,
+ 'search_nb_lieu_vaccination' : resultats?resultats.lieuxAffichables
+ .filter(l => typeActionPour(l) === 'actif-via-plateforme' || typeActionPour(l) === 'actif-via-tel')
+ .length:undefined,
+ 'search_nb_lieu_vaccination_inactive' : resultats?resultats.lieuxAffichables
+ .filter(l => typeActionPour(l) === 'inactif')
+ .length:undefined,
'search_filter_type': (this as any).critèreDeTri || 'date'
});
}
diff --git a/src/utils/Arrays.ts b/src/utils/Arrays.ts
new file mode 100644
index 000000000..dc088c7da
--- /dev/null
+++ b/src/utils/Arrays.ts
@@ -0,0 +1,38 @@
+
+
+
+export class ArrayBuilder {
+ constructor(private array: T[]) {
+ }
+
+ concat(array: T[]) {
+ this.array = this.array.concat(array);
+ return this;
+ }
+
+ map(mapper: (start: T, index?: number, array?: T[]) => R) {
+ return new ArrayBuilder(this.array.map(mapper));
+ }
+
+ filter(predicate: (v: T, index: number) => boolean) {
+ this.array = this.array.filter(predicate);
+ return this;
+ }
+
+ sort(comparator: (v1: T, v2: T) => number) {
+ this.array.sort(comparator);
+ return this;
+ }
+
+ sortBy(sortableDataExtractor: (v: T) => string) {
+ return this.sort((v1: T, v2: T) => sortableDataExtractor(v1).localeCompare(sortableDataExtractor(v2)));
+ }
+
+ build() {
+ return this.array;
+ }
+
+ public static from(array: T[]) {
+ return new ArrayBuilder(array);
+ }
+}
diff --git a/src/utils/Schedulers.ts b/src/utils/Schedulers.ts
new file mode 100644
index 000000000..b9db494bf
--- /dev/null
+++ b/src/utils/Schedulers.ts
@@ -0,0 +1,17 @@
+
+
+export const setDebouncedInterval = function(handler: Function, duration?: number, ...args: any[]) {
+ let ongoingHandler = false;
+ return setInterval(async () => {
+ if(ongoingHandler) {
+ console.warn("Skipped setDebouncedInterval()'s handler as ongoing already");
+ return;
+ }
+ try {
+ ongoingHandler = true;
+ await handler(...args);
+ } finally {
+ ongoingHandler = false;
+ }
+ }, duration);
+}
diff --git a/src/views/vmd-home.view.scss b/src/views/vmd-home.view.scss
index f4dc436f0..fe1792d16 100644
--- a/src/views/vmd-home.view.scss
+++ b/src/views/vmd-home.view.scss
@@ -36,7 +36,7 @@
}
}
-.searchDose {
+.searchAppointment {
display: flex;
flex-direction: column;
padding: 9vh 3.2rem;
@@ -86,7 +86,7 @@
}
}
-.searchDoseForm {
+.searchAppointmentForm {
&-fields {
max-width: 75vw;
@@ -110,7 +110,7 @@
}
}
-.searchAppointment {
+.platforms {
&-logo {
margin: {
top: 1.6rem;
@@ -118,6 +118,7 @@
bottom: 1.6rem;
left: 3.2rem;
};
+ max-width: 75vw;
&._doctolib {
max-height: 6rem;
diff --git a/src/views/vmd-home.view.ts b/src/views/vmd-home.view.ts
index 1e490aa8a..a9e4e35d7 100644
--- a/src/views/vmd-home.view.ts
+++ b/src/views/vmd-home.view.ts
@@ -80,13 +80,13 @@ export class VmdHomeView extends LitElement {
render() {
return html`
-