Maj
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# Parcoursup Explorer
|
||||
|
||||
Application web permettant de consulter les données ouvertes de Parcoursup pour aider à l'orientation des lycéens.
|
||||
|
||||
Ce projet a été réalisé dans le cadre des modules R4.01 et R4.A.10.
|
||||
|
||||
## Fonctionnalités principales
|
||||
* **Recherche** de formations par mots-clés (via l'API OpenData)
|
||||
* **Visualisation cartographique** des établissements avec Leaflet
|
||||
* **Consultation détaillée** d'une formation (taux d'accès, profil des admis, évolution multi-années)
|
||||
* **Graphiques statistiques** intégrés avec Charts.css
|
||||
* **Comparateur de sélection** pour estimer ses chances d'admission
|
||||
* **Espace utilisateur** connecté avec Firebase Auth et Firestore (persistance avancée)
|
||||
|
||||
---
|
||||
|
||||
## Architecture et Composants (Riot.js)
|
||||
|
||||
L'application est construite de façon déclarative et modulaire avec le framework **Riot.js (v9)** et utilise le routeur officiel **@riotjs/route** pour la navigation SPA (Single Page Application).
|
||||
|
||||
### 1. `app.riot` (Composant Racine)
|
||||
C'est le chef d'orchestre de l'application.
|
||||
* **Fonction :** Gère le routage principal (`<router>`, `<route>`), l'état global (données de recherche, sélection) et coordonne les autres composants.
|
||||
* **État (State) :** Stocke les résultats de recherche (`state.results`), la formation active (`state.selected`), la sélection pour comparaison (`state.selectedFormations`), et l'utilisateur connecté (`state.user`).
|
||||
* **Enfants :**
|
||||
* Charge `<search-bar>`, `<map-view>`, `<result-list>` sur la route `/`
|
||||
* Charge `<detail-view>` sur la route `/formation/:id`
|
||||
* Gère seule la route `/comparateur`
|
||||
|
||||
### 2. `components/search-bar.riot`
|
||||
* **Fonction :** Moteur de recherche et filtres.
|
||||
* **Entrées :** L'utilisateur tape sa requête (mot clé) et peut appliquer des filtres avancés (filière, sélectivité, région...).
|
||||
* **Sorties :** Déclenche l'événement `onsearch` qui remonte jusqu'à `<app>` pour lancer la requête API (`lancerRecherche`).
|
||||
|
||||
### 3. `components/result-list.riot`
|
||||
* **Fonction :** Affiche la liste paginée des formations correspondant à la recherche.
|
||||
* **Props :**
|
||||
* `results` : tableau des données renvoyées par l'API
|
||||
* `loading` : booléen pour l'état d'attente
|
||||
* `hasSearched` : détermine si la liste doit s'afficher
|
||||
* **Sorties (Events) :**
|
||||
* `ondetail(index)` : L'application navigue vers la vue détaillée d'une formation.
|
||||
* `onselect(index)` : Ajoute la formation au tableau "Comparateur".
|
||||
|
||||
### 4. `components/detail-view.riot`
|
||||
* **Fonction :** Affiche une fiche complète de la formation sélectionnée avec statistiques approfondies et graphiques via Charts.css.
|
||||
* **Props :**
|
||||
* `formation` : l'objet complet de la formation courante.
|
||||
* **Sorties (Events) :**
|
||||
* `onback()` : Demande de retour à l'écran de recherche principal.
|
||||
* **Interactions :** Fait un appel API supplémentaire (via `window.chargerHistoriqueFormation`) pour récupérer l'historique sur 6 ans depuis l'API Parcoursup.
|
||||
|
||||
### 5. `components/map-view.riot`
|
||||
* **Fonction :** Affiche une carte interactive Leaflet.
|
||||
* **Props :**
|
||||
* `results` : La liste courante des formations de la vue recherche.
|
||||
* **Interactions :** Instancie Leaflet dans `onMounted`, place les marqueurs sur carte selon les coordonnées latitude/longitude récupérées, et se redessine automatiquement avec `onUpdated()`. Fournit également une API window globale (`window.mapFocus`) que `result-list` utilise pour recentrer la carte.
|
||||
|
||||
### 6. `components/auth-panel.riot`
|
||||
* **Fonction :** Panneau de connexion / inscription de l'utilisateur (Bonus Firebase).
|
||||
* **Props :**
|
||||
* `user` (objet utilisateur Firebase passé par `<app>`).
|
||||
* **Sorties (Events) :**
|
||||
* `onauth()` et `onlogout()` pour notifier `<app>` des changements de session, ce qui déclenche la synchronisation de la sélection `localStorage` <-> `Firestore`.
|
||||
|
||||
---
|
||||
|
||||
## Modèle de Données et API
|
||||
|
||||
### `api.js` (Modèle de communication HTTP)
|
||||
Contient les modules d'appel réseau `fetch`.
|
||||
* `chargerFormations()` : Requete paginée avec filtres croisés vers l'API Data ESR.
|
||||
* `chargerHistoriqueFormation()` : Requete intelligente multi-datasets (2020-2025) ciblant le code UAI d'un établissement précis pour en extraire l'historique d'admission.
|
||||
|
||||
### `formation.js` (Modèle Métier)
|
||||
* **Fonction :** Isoler et abstraire la complexité du schéma JSON renvoyé par l'API Parcoursup.
|
||||
* Transforme `brut.acc_tot` ou `brut.lib_for_voe_ins` en un objet lisible clair pour les composants (ex: `formation.admis` ou `formation.nom`), et y précalcule certains champs de base (comme le `tauxAcces`).
|
||||
|
||||
### `firebase.js` (Service Backend As a Service)
|
||||
* Gère l'authentification (Emai/Password) avec `firebase-auth`.
|
||||
* Gère la persistance cloud de la sélection de vœux dans `users/{uid}` via `firebase-firestore`.
|
||||
|
||||
|
||||
## Routing (Hash Router)
|
||||
L'application utilise un système de routage manuel natif basé sur l'API `window.location.hash` :
|
||||
* Écoute de l'événement natif `hashchange` dans `app.riot`.
|
||||
* Routage conditionnel géré par la fonction `gererRoute()` (`#/`, `#/formation/:id`, `#/comparateur`).
|
||||
* Affichage conditionnel des composants via les directives natives `if={ state.view === ... }`.
|
||||
|
||||
***
|
||||
|
||||
*Réalisé par Aylane Sehl, Jenson Val et Séri-Khane Yolou à l'IUT Sénart-Fontainebleau (UPEC).*
|
||||
+62
-52
@@ -1,34 +1,35 @@
|
||||
export function buildURL(query, limit = 20, offset = 0, filters = {}) {
|
||||
let url =
|
||||
"https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-parcoursup/records?"
|
||||
// Construire l'URL de requête vers l'API Parcoursup
|
||||
export function construireURL(requete, limite = 20, decalage = 0, filtres = {}) {
|
||||
|
||||
url += "limit=" + limit
|
||||
url += "&offset=" + offset
|
||||
var url = "https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-parcoursup/records?"
|
||||
|
||||
url += "limit=" + limite
|
||||
url += "&offset=" + decalage
|
||||
|
||||
var conditions = []
|
||||
|
||||
if (query && query.trim() !== "") {
|
||||
conditions.push("search(lib_for_voe_ins, '" + query + "')")
|
||||
if (requete && requete.trim() !== "") {
|
||||
conditions.push("search(lib_for_voe_ins, '" + requete + "')")
|
||||
}
|
||||
|
||||
if (filters.filiere && filters.filiere !== "") {
|
||||
conditions.push("fili='" + filters.filiere + "'")
|
||||
if (filtres.filiere && filtres.filiere !== "") {
|
||||
conditions.push("fili='" + filtres.filiere + "'")
|
||||
}
|
||||
|
||||
if (filters.selectivite && filters.selectivite !== "") {
|
||||
conditions.push("select_form='" + filters.selectivite + "'")
|
||||
if (filtres.selectivite && filtres.selectivite !== "") {
|
||||
conditions.push("select_form='" + filtres.selectivite + "'")
|
||||
}
|
||||
|
||||
if (filters.region && filters.region !== "") {
|
||||
conditions.push("region_etab_aff='" + filters.region + "'")
|
||||
if (filtres.region && filtres.region !== "") {
|
||||
conditions.push("region_etab_aff='" + filtres.region + "'")
|
||||
}
|
||||
|
||||
if (filters.tauxMin && filters.tauxMin > 0) {
|
||||
conditions.push("taux_acces_ens>=" + filters.tauxMin)
|
||||
if (filtres.tauxMin && filtres.tauxMin > 0) {
|
||||
conditions.push("taux_acces_ens>=" + filtres.tauxMin)
|
||||
}
|
||||
|
||||
if (filters.tauxMax && filters.tauxMax < 100) {
|
||||
conditions.push("taux_acces_ens<=" + filters.tauxMax)
|
||||
if (filtres.tauxMax && filtres.tauxMax < 100) {
|
||||
conditions.push("taux_acces_ens<=" + filtres.tauxMax)
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
@@ -38,19 +39,23 @@ export function buildURL(query, limit = 20, offset = 0, filters = {}) {
|
||||
return url
|
||||
}
|
||||
|
||||
export async function fetchFormations(query, limit = 20, offset = 0, filters = {}) {
|
||||
const url = buildURL(query, limit, offset, filters)
|
||||
const response = await fetch(url)
|
||||
// Charger les formations depuis l'API Parcoursup
|
||||
export async function chargerFormations(requete, limite = 20, decalage = 0, filtres = {}) {
|
||||
|
||||
if (!response.ok) {
|
||||
var url = construireURL(requete, limite, decalage, filtres)
|
||||
var reponse = await fetch(url)
|
||||
|
||||
if (!reponse.ok) {
|
||||
throw new Error("Erreur HTTP")
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
return await reponse.json()
|
||||
}
|
||||
|
||||
export async function fetchFormationHistory(codUai, nomFormation) {
|
||||
var datasets = {
|
||||
// Charger l'historique d'une formation sur plusieurs années
|
||||
export async function chargerHistoriqueFormation(codUai, nomFormation) {
|
||||
|
||||
var jeuDeDonnees = {
|
||||
2020: "fr-esr-parcoursup_2020",
|
||||
2021: "fr-esr-parcoursup_2021",
|
||||
2022: "fr-esr-parcoursup_2022",
|
||||
@@ -59,54 +64,59 @@ export async function fetchFormationHistory(codUai, nomFormation) {
|
||||
2025: "fr-esr-parcoursup"
|
||||
}
|
||||
|
||||
var history = []
|
||||
var searchName = nomFormation.substring(0, 40).replace(/'/g, "\\'")
|
||||
var years = [2020, 2021, 2022, 2023, 2024, 2025]
|
||||
var historique = []
|
||||
var nomCourt = nomFormation.substring(0, 40).replace(/'/g, "\\'")
|
||||
var annees = [2020, 2021, 2022, 2023, 2024, 2025]
|
||||
|
||||
for (var i = 0; i < years.length; i++) {
|
||||
var year = years[i]
|
||||
var dataset = datasets[year]
|
||||
for (var i = 0; i < annees.length; i++) {
|
||||
|
||||
var annee = annees[i]
|
||||
var dataset = jeuDeDonnees[annee]
|
||||
|
||||
try {
|
||||
|
||||
var url = "https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/"
|
||||
+ dataset + "/records?"
|
||||
+ "limit=5"
|
||||
+ "&where=cod_uai%3D'" + codUai + "' AND search(lib_for_voe_ins, '" + searchName + "')"
|
||||
+ "&where=cod_uai%3D'" + codUai + "' AND search(lib_for_voe_ins, '" + nomCourt + "')"
|
||||
+ "&select=cod_uai,lib_for_voe_ins,voe_tot,acc_tot,pct_sansmention,pct_ab,pct_b,pct_tb,pct_tbf,pct_bg,pct_bt,pct_bp"
|
||||
|
||||
var response = await fetch(url)
|
||||
var reponse = await fetch(url)
|
||||
|
||||
if (response.ok) {
|
||||
var data = await response.json()
|
||||
if (reponse.ok) {
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
var r = data.results[0]
|
||||
var donnees = await reponse.json()
|
||||
|
||||
if (donnees.results && donnees.results.length > 0) {
|
||||
|
||||
var ligne = donnees.results[0]
|
||||
var taux = 0
|
||||
|
||||
if (r.voe_tot && r.voe_tot > 0) {
|
||||
taux = Math.round((r.acc_tot / r.voe_tot) * 100)
|
||||
if (ligne.voe_tot && ligne.voe_tot > 0) {
|
||||
taux = Math.round((ligne.acc_tot / ligne.voe_tot) * 100)
|
||||
}
|
||||
|
||||
history.push({
|
||||
annee: year,
|
||||
historique.push({
|
||||
annee: annee,
|
||||
tauxAcces: taux,
|
||||
candidats: r.voe_tot || 0,
|
||||
admis: r.acc_tot || 0,
|
||||
pctSansMention: r.pct_sansmention || 0,
|
||||
pctAB: r.pct_ab || 0,
|
||||
pctB: r.pct_b || 0,
|
||||
pctTB: r.pct_tb || 0,
|
||||
pctTBF: r.pct_tbf || 0,
|
||||
pctGeneral: r.pct_bg || 0,
|
||||
pctTechno: r.pct_bt || 0,
|
||||
pctPro: r.pct_bp || 0
|
||||
candidats: ligne.voe_tot || 0,
|
||||
admis: ligne.acc_tot || 0,
|
||||
pctSansMention: ligne.pct_sansmention || 0,
|
||||
pctAB: ligne.pct_ab || 0,
|
||||
pctB: ligne.pct_b || 0,
|
||||
pctTB: ligne.pct_tb || 0,
|
||||
pctTBF: ligne.pct_tbf || 0,
|
||||
pctGeneral: ligne.pct_bg || 0,
|
||||
pctTechno: ligne.pct_bt || 0,
|
||||
pctPro: ligne.pct_bp || 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.warn("Erreur pour " + year + ":", e)
|
||||
console.warn("Erreur pour l'année " + annee + " :", e)
|
||||
}
|
||||
}
|
||||
|
||||
return history
|
||||
return historique
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export function buildURL(query) {
|
||||
let url =
|
||||
"https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-parcoursup/records?limit=10"
|
||||
|
||||
if (query && query.trim() !== "") {
|
||||
url += "&where=search(lib_for_voe_ins, '" + query + "')"
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export async function fetchFormations(query) {
|
||||
const url = buildURL(query)
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Erreur HTTP")
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
+329
-201
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
<auth-panel>
|
||||
|
||||
<!-- Bouton connexion dans le header (utilisateur non connecté) -->
|
||||
<div class="auth-header-btn" if={ !props.user }>
|
||||
<button class="btn btn-auth" onclick={ ouvrirModale }>
|
||||
<span class="auth-icon"></span> Connexion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Email + bouton déconnexion dans le header (utilisateur connecté) -->
|
||||
<div class="auth-user-info" if={ props.user }>
|
||||
<span class="auth-email">{ props.user.email }</span>
|
||||
<button class="btn btn-auth-logout" onclick={ seDeconnecter }>Déconnexion</button>
|
||||
</div>
|
||||
|
||||
<!-- Modale d'authentification -->
|
||||
<div class="auth-modal-overlay" if={ state.visible } onclick={ cliquerFond }>
|
||||
<div class="auth-modal">
|
||||
|
||||
<button class="auth-modal-close" onclick={ fermerModale }>✕</button>
|
||||
|
||||
<h2 class="auth-modal-title">{ state.titre }</h2>
|
||||
|
||||
<!-- Onglets Connexion / Inscription -->
|
||||
<div class="auth-tabs">
|
||||
<button class={ state.classBtnConnexion } onclick={ afficherConnexion }>
|
||||
Connexion
|
||||
</button>
|
||||
<button class={ state.classBtnInscription } onclick={ afficherInscription }>
|
||||
Inscription
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire -->
|
||||
<form onsubmit={ validerFormulaire } class="auth-form">
|
||||
|
||||
<div class="auth-field">
|
||||
<label>Adresse e-mail</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="exemple@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label>Mot de passe</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Message d'erreur -->
|
||||
<div class="auth-error" if={ state.erreur }>
|
||||
{ state.erreur }
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary auth-submit" disabled={ state.chargement }>
|
||||
{ state.labelBouton }
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
state: {
|
||||
visible: false,
|
||||
mode: 'connexion',
|
||||
chargement: false,
|
||||
erreur: null,
|
||||
titre: 'Connexion',
|
||||
labelBouton: 'Se connecter',
|
||||
classBtnConnexion: 'auth-tab active',
|
||||
classBtnInscription: 'auth-tab'
|
||||
},
|
||||
|
||||
ouvrirModale() {
|
||||
this.update({ visible: true, erreur: null })
|
||||
},
|
||||
|
||||
fermerModale() {
|
||||
this.update({ visible: false, erreur: null })
|
||||
},
|
||||
|
||||
// Fermer si l'utilisateur clique en dehors de la modale
|
||||
cliquerFond(e) {
|
||||
if (e.target === e.currentTarget) {
|
||||
this.fermerModale()
|
||||
}
|
||||
},
|
||||
|
||||
afficherConnexion() {
|
||||
this.update({
|
||||
mode: 'connexion',
|
||||
erreur: null,
|
||||
titre: 'Connexion',
|
||||
labelBouton: 'Se connecter',
|
||||
classBtnConnexion: 'auth-tab active',
|
||||
classBtnInscription: 'auth-tab'
|
||||
})
|
||||
},
|
||||
|
||||
afficherInscription() {
|
||||
this.update({
|
||||
mode: 'inscription',
|
||||
erreur: null,
|
||||
titre: 'Créer un compte',
|
||||
labelBouton: 'Créer le compte',
|
||||
classBtnConnexion: 'auth-tab',
|
||||
classBtnInscription: 'auth-tab active'
|
||||
})
|
||||
},
|
||||
|
||||
async validerFormulaire(e) {
|
||||
e.preventDefault()
|
||||
|
||||
var email = e.target.email.value.trim()
|
||||
var password = e.target.password.value
|
||||
var services = window.firebaseServices
|
||||
|
||||
this.update({ chargement: true, erreur: null })
|
||||
|
||||
try {
|
||||
|
||||
if (this.state.mode === 'inscription') {
|
||||
await services.createAccount(email, password)
|
||||
} else {
|
||||
await services.login(email, password)
|
||||
}
|
||||
|
||||
this.update({ visible: false, chargement: false })
|
||||
this.props.onauth && this.props.onauth()
|
||||
|
||||
} catch (err) {
|
||||
|
||||
var messageErreur = 'Une erreur est survenue.'
|
||||
|
||||
if (err.code === 'auth/email-already-in-use') {
|
||||
messageErreur = 'Cet e-mail est déjà utilisé.'
|
||||
} else if (err.code === 'auth/invalid-email') {
|
||||
messageErreur = 'Adresse e-mail invalide.'
|
||||
} else if (err.code === 'auth/wrong-password' || err.code === 'auth/invalid-credential') {
|
||||
messageErreur = 'E-mail ou mot de passe incorrect.'
|
||||
} else if (err.code === 'auth/weak-password') {
|
||||
messageErreur = 'Le mot de passe doit faire au moins 6 caractères.'
|
||||
} else if (err.code === 'auth/user-not-found') {
|
||||
messageErreur = 'Aucun compte trouvé avec cet e-mail.'
|
||||
}
|
||||
|
||||
this.update({ chargement: false, erreur: messageErreur })
|
||||
}
|
||||
},
|
||||
|
||||
async seDeconnecter() {
|
||||
try {
|
||||
await window.firebaseServices.logout()
|
||||
this.props.onlogout && this.props.onlogout()
|
||||
} catch (err) {
|
||||
console.error('Erreur déconnexion :', err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
</auth-panel>
|
||||
@@ -1,9 +1,7 @@
|
||||
<detail-view>
|
||||
<div if={ props.formation } class="detail-page">
|
||||
<h2>Formation</h2>
|
||||
|
||||
<h1 class="formation-title">{ props.formation.etablissement } - { props.formation.nom }</h1>
|
||||
|
||||
<div class="formation-meta">
|
||||
<p><b>Ville :</b> { props.formation.ville }</p>
|
||||
<p><b>Département :</b> { props.formation.departement } { props.formation.departementLib }</p>
|
||||
@@ -15,7 +13,6 @@
|
||||
<div class="detail-grid">
|
||||
<div>
|
||||
<h2>Phase principale d'admission</h2>
|
||||
|
||||
<table class="detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -68,7 +65,6 @@
|
||||
|
||||
<div class="timeline-box">
|
||||
<h3>Vitesse de remplissage</h3>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
@@ -77,7 +73,6 @@
|
||||
{ props.formation.pctDebutPhase }%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div>
|
||||
@@ -85,7 +80,6 @@
|
||||
{ props.formation.pctDateBac }%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div>
|
||||
@@ -98,7 +92,6 @@
|
||||
</div>
|
||||
|
||||
<h2>Phase complémentaire d'admission</h2>
|
||||
|
||||
<table class="detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -148,146 +141,288 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ===================== GRAPHIQUES CHARTS.CSS ===================== -->
|
||||
<!-- ==================== GRAPHIQUES CHARTS.CSS ==================== -->
|
||||
|
||||
<h2 class="charts-heading">Profil des admis</h2>
|
||||
|
||||
<div class="charts-section">
|
||||
|
||||
<!-- Graphique 1 : Répartition par type de bac -->
|
||||
<div class="chart-wrapper">
|
||||
<h3>Répartition par type de bac</h3>
|
||||
|
||||
<div id="chart-bac">
|
||||
<table class="charts-css column show-labels show-primary-axis show-4-secondary-axes data-spacing-10">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Pourcentage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Général</th>
|
||||
<td style="--size: { safe(props.formation.pctGeneral) }; --color: #3d7fff;">
|
||||
<span class="data">{ props.formation.pctGeneral || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Techno</th>
|
||||
<td style="--size: { safe(props.formation.pctTechno) }; --color: #f59e0b;">
|
||||
<span class="data">{ props.formation.pctTechno || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Pro</th>
|
||||
<td style="--size: { safe(props.formation.pctPro) }; --color: #10b981;">
|
||||
<span class="data">{ props.formation.pctPro || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="chart-bac" ref="graphBac"></div>
|
||||
</div>
|
||||
|
||||
<!-- Graphique 2 : Mentions au bac -->
|
||||
<div class="chart-wrapper">
|
||||
<h3>Mentions au bac des admis</h3>
|
||||
|
||||
<div id="chart-mentions">
|
||||
<table class="charts-css column show-labels show-primary-axis show-4-secondary-axes data-spacing-10">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Mention</th>
|
||||
<th scope="col">Pourcentage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Sans</th>
|
||||
<td style="--size: { safe(props.formation.pctSansMention) }; --color: #94a3b8;">
|
||||
<span class="data">{ props.formation.pctSansMention || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">AB</th>
|
||||
<td style="--size: { safe(props.formation.pctAB) }; --color: #60a5fa;">
|
||||
<span class="data">{ props.formation.pctAB || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Bien</th>
|
||||
<td style="--size: { safe(props.formation.pctB) }; --color: #34d399;">
|
||||
<span class="data">{ props.formation.pctB || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">TB</th>
|
||||
<td style="--size: { safe(props.formation.pctTB) }; --color: #fbbf24;">
|
||||
<span class="data">{ props.formation.pctTB || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">TB Féli.</th>
|
||||
<td style="--size: { safe(props.formation.pctTBF) }; --color: #f472b6;">
|
||||
<span class="data">{ props.formation.pctTBF || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="chart-mentions" ref="graphMentions"></div>
|
||||
</div>
|
||||
|
||||
<!-- Graphique 3 : Profil sociologique (barres horizontales, pleine largeur) -->
|
||||
<div class="chart-wrapper chart-full">
|
||||
<h3>Profil sociologique</h3>
|
||||
|
||||
<div id="chart-profil">
|
||||
<table class="charts-css bar show-labels show-primary-axis show-4-secondary-axes data-spacing-14">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Catégorie</th>
|
||||
<th scope="col">Pourcentage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Femmes</th>
|
||||
<td style="--size: { safe(props.formation.pctFemmes) }; --color: #a78bfa;">
|
||||
<span class="data">{ props.formation.pctFemmes || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Boursiers</th>
|
||||
<td style="--size: { safe(props.formation.pctBoursiers) }; --color: #fb923c;">
|
||||
<span class="data">{ props.formation.pctBoursiers || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Néo-bac</th>
|
||||
<td style="--size: { safe(props.formation.pctNeoBac) }; --color: #2dd4bf;">
|
||||
<span class="data">{ props.formation.pctNeoBac || 0 }%</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="chart-profil" ref="graphProfil"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== ÉVOLUTION HISTORIQUE ==================== -->
|
||||
|
||||
<h2 class="charts-heading">Évolution depuis 2020</h2>
|
||||
|
||||
<div if={ state.chargementHistorique } class="message">
|
||||
Chargement de l'historique...
|
||||
</div>
|
||||
|
||||
<button onclick={ () => props.onback() } class="btn-retour">Retour à la liste</button>
|
||||
<div if={ !state.chargementHistorique && state.historique.length === 0 } class="message">
|
||||
Aucune donnée historique disponible pour cette formation.
|
||||
</div>
|
||||
|
||||
<div class="charts-section" if={ state.historique.length > 0 }>
|
||||
<div class="chart-wrapper">
|
||||
<h3>Taux d'accès par année</h3>
|
||||
<div id="chart-evolution-taux" ref="graphTaux"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrapper">
|
||||
<h3>Nombre de candidats et admis</h3>
|
||||
<div id="chart-evolution-candidats" ref="graphCandidats"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrapper chart-full">
|
||||
<h3>Évolution des mentions au bac</h3>
|
||||
<div ref="graphMentionsHist"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick={ retourListe } class="btn-retour">Retour à la liste</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
safe(val) {
|
||||
if (val === null || val === undefined || isNaN(val)) return 0
|
||||
var v = val / 100
|
||||
if (v > 1) return 1
|
||||
if (v < 0) return 0
|
||||
return Math.round(v * 100) / 100
|
||||
|
||||
state: {
|
||||
historique: [],
|
||||
chargementHistorique: false
|
||||
},
|
||||
|
||||
onMounted() {
|
||||
this.afficherGraphiques()
|
||||
this.chargerHistorique()
|
||||
},
|
||||
|
||||
onUpdated() {
|
||||
this.afficherGraphiques()
|
||||
|
||||
if (this.state.historique.length > 0) {
|
||||
this.afficherGraphiquesHistoriques()
|
||||
}
|
||||
},
|
||||
|
||||
// Retour à la liste des résultats
|
||||
retourListe() {
|
||||
this.props.onback()
|
||||
},
|
||||
|
||||
// Limiter une valeur entre 0 et 1 pour Charts.css
|
||||
limiterValeur(val) {
|
||||
if (val === null || val === undefined || isNaN(val)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var v = val / 100
|
||||
|
||||
if (v > 1) {
|
||||
return 1
|
||||
}
|
||||
if (v < 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.round(v * 100) / 100
|
||||
},
|
||||
|
||||
afficherGraphiques() {
|
||||
var f = this.props.formation
|
||||
if (!f) {
|
||||
return
|
||||
}
|
||||
|
||||
var graphBac = this.$('[ref="graphBac"]')
|
||||
var graphMentions = this.$('[ref="graphMentions"]')
|
||||
var graphProfil = this.$('[ref="graphProfil"]')
|
||||
|
||||
if (graphBac) {
|
||||
graphBac.innerHTML = this.construireGraphiqueColonnes([
|
||||
{ label: 'Général', valeur: f.pctGeneral, couleur: '#3d7fff' },
|
||||
{ label: 'Techno', valeur: f.pctTechno, couleur: '#f59e0b' },
|
||||
{ label: 'Pro', valeur: f.pctPro, couleur: '#10b981' }
|
||||
])
|
||||
}
|
||||
|
||||
if (graphMentions) {
|
||||
graphMentions.innerHTML = this.construireGraphiqueColonnes([
|
||||
{ label: 'Sans', valeur: f.pctSansMention, couleur: '#94a3b8' },
|
||||
{ label: 'AB', valeur: f.pctAB, couleur: '#60a5fa' },
|
||||
{ label: 'Bien', valeur: f.pctB, couleur: '#34d399' },
|
||||
{ label: 'TB', valeur: f.pctTB, couleur: '#fbbf24' },
|
||||
{ label: 'TB Féli.', valeur: f.pctTBF, couleur: '#f472b6' }
|
||||
])
|
||||
}
|
||||
|
||||
if (graphProfil) {
|
||||
graphProfil.innerHTML = this.construireGraphiqueBarres([
|
||||
{ label: 'Femmes', valeur: f.pctFemmes, couleur: '#a78bfa' },
|
||||
{ label: 'Boursiers', valeur: f.pctBoursiers, couleur: '#fb923c' },
|
||||
{ label: 'Néo-bac', valeur: f.pctNeoBac, couleur: '#2dd4bf' }
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
async chargerHistorique() {
|
||||
var f = this.props.formation
|
||||
if (!f) {
|
||||
return
|
||||
}
|
||||
|
||||
var codUai = f.id.split('-')[0]
|
||||
|
||||
if (!codUai || !window.chargerHistoriqueFormation) {
|
||||
return
|
||||
}
|
||||
|
||||
this.update({ chargementHistorique: true })
|
||||
|
||||
try {
|
||||
var historique = await window.chargerHistoriqueFormation(codUai, f.nom)
|
||||
this.update({ historique: historique, chargementHistorique: false })
|
||||
} catch (e) {
|
||||
console.error('Erreur chargement historique :', e)
|
||||
this.update({ historique: [], chargementHistorique: false })
|
||||
}
|
||||
},
|
||||
|
||||
afficherGraphiquesHistoriques() {
|
||||
var historique = this.state.historique
|
||||
if (!historique || historique.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
var graphTaux = this.$('[ref="graphTaux"]')
|
||||
var graphCandidats = this.$('[ref="graphCandidats"]')
|
||||
var graphMentionsHist = this.$('[ref="graphMentionsHist"]')
|
||||
|
||||
// Graphique : taux d'accès par année
|
||||
if (graphTaux) {
|
||||
var elementsAnnee = []
|
||||
|
||||
for (var i = 0; i < historique.length; i++) {
|
||||
elementsAnnee.push({
|
||||
label: '' + historique[i].annee,
|
||||
valeur: historique[i].tauxAcces,
|
||||
couleur: '#1a936f'
|
||||
})
|
||||
}
|
||||
|
||||
graphTaux.innerHTML = this.construireGraphiqueColonnes(elementsAnnee)
|
||||
}
|
||||
|
||||
// Graphique : candidats vs admis
|
||||
if (graphCandidats) {
|
||||
var maxCandidats = 0
|
||||
|
||||
for (var i = 0; i < historique.length; i++) {
|
||||
if (historique[i].candidats > maxCandidats) {
|
||||
maxCandidats = historique[i].candidats
|
||||
}
|
||||
}
|
||||
|
||||
var lignes = ''
|
||||
|
||||
for (var i = 0; i < historique.length; i++) {
|
||||
var h = historique[i]
|
||||
var tailleCand = 0
|
||||
var tailleAdmis = 0
|
||||
|
||||
if (maxCandidats > 0) {
|
||||
tailleCand = Math.round((h.candidats / maxCandidats) * 100) / 100
|
||||
tailleAdmis = Math.round((h.admis / maxCandidats) * 100) / 100
|
||||
}
|
||||
|
||||
lignes += '<tr>'
|
||||
lignes += '<th scope="row">' + h.annee + '</th>'
|
||||
lignes += '<td style="--size: ' + tailleCand + '; --color: #2a5298;">'
|
||||
lignes += '<span class="data">' + h.candidats + '</span></td>'
|
||||
lignes += '<td style="--size: ' + tailleAdmis + '; --color: #1a936f;">'
|
||||
lignes += '<span class="data">' + h.admis + '</span></td>'
|
||||
lignes += '</tr>'
|
||||
}
|
||||
|
||||
graphCandidats.innerHTML = '<table class="charts-css column multiple show-labels show-primary-axis show-4-secondary-axes data-spacing-10">'
|
||||
+ '<thead><tr><th scope="col">Année</th><th scope="col">Candidats</th><th scope="col">Admis</th></tr></thead>'
|
||||
+ '<tbody>' + lignes + '</tbody></table>'
|
||||
}
|
||||
|
||||
// Tableau : évolution des mentions
|
||||
if (graphMentionsHist) {
|
||||
var tableau = '<table class="detail-table">'
|
||||
tableau += '<thead><tr><th>Année</th><th>Sans mention</th><th>AB</th><th>Bien</th><th>TB</th><th>TB Féli.</th></tr></thead>'
|
||||
tableau += '<tbody>'
|
||||
|
||||
for (var i = 0; i < historique.length; i++) {
|
||||
var h = historique[i]
|
||||
tableau += '<tr>'
|
||||
tableau += '<td><b>' + h.annee + '</b></td>'
|
||||
tableau += '<td>' + h.pctSansMention + '%</td>'
|
||||
tableau += '<td>' + h.pctAB + '%</td>'
|
||||
tableau += '<td>' + h.pctB + '%</td>'
|
||||
tableau += '<td>' + h.pctTB + '%</td>'
|
||||
tableau += '<td>' + h.pctTBF + '%</td>'
|
||||
tableau += '</tr>'
|
||||
}
|
||||
|
||||
tableau += '</tbody></table>'
|
||||
graphMentionsHist.innerHTML = tableau
|
||||
}
|
||||
},
|
||||
|
||||
construireGraphiqueColonnes(elements) {
|
||||
var lignes = ''
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
var el = elements[i]
|
||||
var taille = this.limiterValeur(el.valeur)
|
||||
var affiche = el.valeur || 0
|
||||
|
||||
lignes += '<tr>'
|
||||
lignes += '<th scope="row">' + el.label + '</th>'
|
||||
lignes += '<td style="--size: ' + taille + '; --color: ' + el.couleur + ';">'
|
||||
lignes += '<span class="data">' + affiche + '%</span>'
|
||||
lignes += '</td></tr>'
|
||||
}
|
||||
|
||||
return '<table class="charts-css column show-labels show-primary-axis show-4-secondary-axes data-spacing-10">'
|
||||
+ '<thead><tr><th scope="col">Type</th><th scope="col">%</th></tr></thead>'
|
||||
+ '<tbody>' + lignes + '</tbody></table>'
|
||||
},
|
||||
|
||||
construireGraphiqueBarres(elements) {
|
||||
var lignes = ''
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
var el = elements[i]
|
||||
var taille = this.limiterValeur(el.valeur)
|
||||
var affiche = el.valeur || 0
|
||||
|
||||
lignes += '<tr>'
|
||||
lignes += '<th scope="row">' + el.label + '</th>'
|
||||
lignes += '<td style="--size: ' + taille + '; --color: ' + el.couleur + ';">'
|
||||
lignes += '<span class="data">' + affiche + '%</span>'
|
||||
lignes += '</td></tr>'
|
||||
}
|
||||
|
||||
return '<table class="charts-css bar show-labels show-primary-axis show-4-secondary-axes data-spacing-14">'
|
||||
+ '<thead><tr><th scope="col">Catégorie</th><th scope="col">%</th></tr></thead>'
|
||||
+ '<tbody>' + lignes + '</tbody></table>'
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
</detail-view>
|
||||
|
||||
@@ -1,100 +1,120 @@
|
||||
<map-view>
|
||||
<div class="map-box">
|
||||
<h3>Carte des formations</h3>
|
||||
<div class="map" ref="map"></div>
|
||||
<div class="map" ref="carte"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
onMounted() {
|
||||
const mapElement = this.$('div[ref="map"]')
|
||||
|
||||
this.map = L.map(mapElement).setView([46.8, 2.5], 6)
|
||||
onMounted() {
|
||||
var divCarte = this.$('div[ref="carte"]')
|
||||
|
||||
this.carte = L.map(divCarte).setView([46.8, 2.5], 6)
|
||||
this.groupeMarqueurs = L.layerGroup().addTo(this.carte)
|
||||
this.marqueursIndex = {}
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(this.map)
|
||||
}).addTo(this.carte)
|
||||
|
||||
this.markersLayer = L.layerGroup().addTo(this.map)
|
||||
this.markersById = {}
|
||||
this.refreshMarkers()
|
||||
this.afficherMarqueurs()
|
||||
|
||||
setTimeout(() => {
|
||||
this.map.invalidateSize()
|
||||
}, 100)
|
||||
var composant = this
|
||||
|
||||
// Exposer globalement pour que result-list puisse appeler
|
||||
window.mapFocus = (id) => {
|
||||
this.focusFormation(id)
|
||||
setTimeout(function() {
|
||||
if (composant.carte) {
|
||||
composant.carte.invalidateSize()
|
||||
}
|
||||
}, 200)
|
||||
|
||||
setTimeout(function() {
|
||||
if (composant.carte) {
|
||||
composant.carte.invalidateSize()
|
||||
}
|
||||
}, 500)
|
||||
|
||||
window.mapFocus = function(id) {
|
||||
composant.centrerSurFormation(id)
|
||||
}
|
||||
},
|
||||
|
||||
onUpdated() {
|
||||
this.refreshMarkers()
|
||||
this.afficherMarqueurs()
|
||||
|
||||
if (this.map) {
|
||||
setTimeout(() => {
|
||||
this.map.invalidateSize()
|
||||
}, 50)
|
||||
var composant = this
|
||||
|
||||
if (this.carte) {
|
||||
setTimeout(function() {
|
||||
composant.carte.invalidateSize()
|
||||
}, 100)
|
||||
|
||||
setTimeout(function() {
|
||||
composant.carte.invalidateSize()
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
|
||||
onBeforeUnmount() {
|
||||
if (this.map) {
|
||||
this.map.remove()
|
||||
this.map = null
|
||||
if (this.carte) {
|
||||
this.carte.remove()
|
||||
this.carte = null
|
||||
}
|
||||
window.mapFocus = null
|
||||
},
|
||||
|
||||
refreshMarkers() {
|
||||
if (!this.map || !this.markersLayer) {
|
||||
afficherMarqueurs() {
|
||||
if (!this.carte || !this.groupeMarqueurs) {
|
||||
return
|
||||
}
|
||||
|
||||
this.markersLayer.clearLayers()
|
||||
this.markersById = {}
|
||||
this.groupeMarqueurs.clearLayers()
|
||||
this.marqueursIndex = {}
|
||||
|
||||
const points = []
|
||||
const results = this.props.results || []
|
||||
var coordonnees = []
|
||||
var formations = this.props.results || []
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const f = results[i]
|
||||
for (var i = 0; i < formations.length; i++) {
|
||||
var f = formations[i]
|
||||
|
||||
if (f.latitude != null && f.longitude != null) {
|
||||
const marker = L.marker([f.latitude, f.longitude])
|
||||
marker.bindPopup('<b>' + f.nom + '</b><br>' + f.ville)
|
||||
marker.addTo(this.markersLayer)
|
||||
var marqueur = L.marker([f.latitude, f.longitude])
|
||||
marqueur.bindPopup('<b>' + f.nom + '</b><br>' + f.ville)
|
||||
marqueur.addTo(this.groupeMarqueurs)
|
||||
|
||||
this.markersById[f.id] = marker
|
||||
points.push([f.latitude, f.longitude])
|
||||
this.marqueursIndex[f.id] = marqueur
|
||||
coordonnees.push([f.latitude, f.longitude])
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length > 0) {
|
||||
this.map.fitBounds(points, { padding: [20, 20] })
|
||||
if (coordonnees.length > 0) {
|
||||
this.carte.fitBounds(coordonnees, { padding: [20, 20] })
|
||||
} else {
|
||||
this.map.setView([46.8, 2.5], 6)
|
||||
this.carte.setView([46.8, 2.5], 6)
|
||||
}
|
||||
},
|
||||
|
||||
focusFormation(id) {
|
||||
var marker = this.markersById[id]
|
||||
centrerSurFormation(id) {
|
||||
var marqueur = this.marqueursIndex[id]
|
||||
|
||||
if (marker && this.map) {
|
||||
// Scroll vers la carte
|
||||
var mapEl = this.$('div[ref="map"]')
|
||||
if (mapEl) {
|
||||
mapEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
if (marqueur && this.carte) {
|
||||
var divCarte = this.$('div[ref="carte"]')
|
||||
|
||||
if (divCarte) {
|
||||
divCarte.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
|
||||
// Zoom sur le marqueur et ouvrir le popup
|
||||
setTimeout(() => {
|
||||
this.map.setView(marker.getLatLng(), 13, { animate: true })
|
||||
marker.openPopup()
|
||||
var composant = this
|
||||
|
||||
setTimeout(function() {
|
||||
composant.carte.invalidateSize()
|
||||
composant.carte.setView(marqueur.getLatLng(), 13, { animate: true })
|
||||
marqueur.openPopup()
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
</map-view>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<result-list>
|
||||
<div class="results">
|
||||
|
||||
<div class="message" if={ props.results.length === 0 && props.hasSearched && !props.loading }>
|
||||
Aucun résultat trouvé
|
||||
</div>
|
||||
@@ -8,26 +9,41 @@
|
||||
Chargement...
|
||||
</div>
|
||||
|
||||
<div each={ (f, i) in props.results } key={ f.id } class="card">
|
||||
<h3>{ f.nom }</h3>
|
||||
<p><b>Établissement :</b> { f.etablissement }</p>
|
||||
<p><b>Ville :</b> { f.ville } ({ f.departement })</p>
|
||||
<p><b>Filière :</b> { f.filiere }</p>
|
||||
<p><b>Taux d'accès :</b> { f.tauxAcces }%</p>
|
||||
<div each={ (formation, index) in props.results } key={ formation.id } class="card">
|
||||
<h3>{ formation.nom }</h3>
|
||||
<p><b>Établissement :</b> { formation.etablissement }</p>
|
||||
<p><b>Ville :</b> { formation.ville } ({ formation.departement })</p>
|
||||
<p><b>Filière :</b> { formation.filiere }</p>
|
||||
<p><b>Taux d'accès :</b> { formation.tauxAcces }%</p>
|
||||
|
||||
<button onclick={ () => props.ondetail(i) }>Voir détail</button>
|
||||
<button onclick={ () => props.onselect(i) }>Ajouter à la sélection</button>
|
||||
<button onclick={ () => locateOnMap(f) } if={ f.latitude != null }>Localiser</button>
|
||||
<button onclick={ afficherDetail.bind(this, index) }>Voir détail</button>
|
||||
<button onclick={ ajouterALaSelection.bind(this, index) }>Ajouter à la sélection</button>
|
||||
<button onclick={ localiserSurCarte.bind(this, formation) } if={ formation.latitude != null }>Localiser</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
locateOnMap(f) {
|
||||
|
||||
// Déclencher l'affichage du détail d'une formation
|
||||
afficherDetail(index) {
|
||||
this.props.ondetail(index)
|
||||
},
|
||||
|
||||
// Ajouter une formation à la sélection
|
||||
ajouterALaSelection(index) {
|
||||
this.props.onselect(index)
|
||||
},
|
||||
|
||||
// Centrer la carte sur la formation
|
||||
localiserSurCarte(formation) {
|
||||
if (window.mapFocus) {
|
||||
window.mapFocus(f.id)
|
||||
window.mapFocus(formation.id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
</result-list>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<div class="filters-toggle">
|
||||
<button class="btn btn-small btn-outline" onclick={ toggleFilters }>
|
||||
{ state.showFilters ? 'Masquer les filtres' : 'Filtres avancés' }
|
||||
{ state.labelFiltres }
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
state: {
|
||||
query: '',
|
||||
showFilters: false,
|
||||
labelFiltres: 'Filtres avancés',
|
||||
filiere: '',
|
||||
selectivite: '',
|
||||
region: '',
|
||||
@@ -109,14 +110,30 @@
|
||||
},
|
||||
|
||||
toggleFilters() {
|
||||
this.update({ showFilters: !this.state.showFilters })
|
||||
var visible = !this.state.showFilters
|
||||
var label = visible ? 'Masquer les filtres' : 'Filtres avancés'
|
||||
this.update({ showFilters: visible, labelFiltres: label })
|
||||
},
|
||||
|
||||
updateFiliere(e) { this.update({ filiere: e.target.value }) },
|
||||
updateSelectivite(e) { this.update({ selectivite: e.target.value }) },
|
||||
updateRegion(e) { this.update({ region: e.target.value }) },
|
||||
updateTauxMin(e) { this.update({ tauxMin: Number(e.target.value) }) },
|
||||
updateTauxMax(e) { this.update({ tauxMax: Number(e.target.value) }) },
|
||||
updateFiliere(e) {
|
||||
this.update({ filiere: e.target.value })
|
||||
},
|
||||
|
||||
updateSelectivite(e) {
|
||||
this.update({ selectivite: e.target.value })
|
||||
},
|
||||
|
||||
updateRegion(e) {
|
||||
this.update({ region: e.target.value })
|
||||
},
|
||||
|
||||
updateTauxMin(e) {
|
||||
this.update({ tauxMin: Number(e.target.value) })
|
||||
},
|
||||
|
||||
updateTauxMax(e) {
|
||||
this.update({ tauxMax: Number(e.target.value) })
|
||||
},
|
||||
|
||||
submitSearch() {
|
||||
var filters = {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
|
||||
|
||||
import {
|
||||
getAuth,
|
||||
createUserWithEmailAndPassword,
|
||||
signInWithEmailAndPassword,
|
||||
signOut,
|
||||
onAuthStateChanged
|
||||
} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
|
||||
|
||||
import {
|
||||
getFirestore,
|
||||
doc,
|
||||
setDoc,
|
||||
getDoc
|
||||
} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
|
||||
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDr1jMgGm0Oj_bOiWY-8Gy27IlzkmAzlOM",
|
||||
authDomain: "parcoursupp-expl.firebaseapp.com",
|
||||
projectId: "parcoursupp-expl",
|
||||
storageBucket: "parcoursupp-expl.firebasestorage.app",
|
||||
messagingSenderId: "973054617217",
|
||||
appId: "1:973054617217:web:4d52af4280396976228f80"
|
||||
};
|
||||
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const auth = getAuth(app);
|
||||
const db = getFirestore(app);
|
||||
|
||||
|
||||
// Créer un compte avec email et mot de passe
|
||||
async function createAccount(email, password) {
|
||||
return createUserWithEmailAndPassword(auth, email, password);
|
||||
}
|
||||
|
||||
// Se connecter avec email et mot de passe
|
||||
async function login(email, password) {
|
||||
return signInWithEmailAndPassword(auth, email, password);
|
||||
}
|
||||
|
||||
// Se déconnecter
|
||||
async function logout() {
|
||||
return signOut(auth);
|
||||
}
|
||||
|
||||
// Écouter les changements d'état de connexion
|
||||
function onUserChanged(callback) {
|
||||
return onAuthStateChanged(auth, callback);
|
||||
}
|
||||
|
||||
// Sauvegarder les données d'un utilisateur dans Firestore
|
||||
async function saveUserData(uid, data) {
|
||||
await setDoc(doc(db, "users", uid), data, { merge: true });
|
||||
}
|
||||
|
||||
// Charger les données d'un utilisateur depuis Firestore
|
||||
async function loadUserData(uid) {
|
||||
var snap = await getDoc(doc(db, "users", uid));
|
||||
|
||||
if (snap.exists()) {
|
||||
return snap.data();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
auth,
|
||||
db,
|
||||
createAccount,
|
||||
login,
|
||||
logout,
|
||||
onUserChanged,
|
||||
saveUserData,
|
||||
loadUserData
|
||||
};
|
||||
+75
-71
@@ -1,93 +1,97 @@
|
||||
export function createFormation(raw) {
|
||||
let taux = 0
|
||||
// Créer un objet formation à partir des données brutes de l'API
|
||||
export function creerFormation(brut) {
|
||||
|
||||
if (raw.voe_tot && raw.voe_tot > 0) {
|
||||
taux = Math.round((raw.acc_tot / raw.voe_tot) * 100)
|
||||
var taux = 0
|
||||
var latitude = null
|
||||
var longitude = null
|
||||
|
||||
if (brut.voe_tot && brut.voe_tot > 0) {
|
||||
taux = Math.round((brut.acc_tot / brut.voe_tot) * 100)
|
||||
}
|
||||
|
||||
if (brut.g_olocalisation_des_formations) {
|
||||
latitude = brut.g_olocalisation_des_formations.lat
|
||||
longitude = brut.g_olocalisation_des_formations.lon
|
||||
}
|
||||
|
||||
return {
|
||||
id: raw.cod_uai + "-" + raw.lib_for_voe_ins,
|
||||
id: brut.cod_uai + "-" + brut.lib_for_voe_ins,
|
||||
|
||||
nom: raw.lib_for_voe_ins,
|
||||
etablissement: raw.g_ea_lib_vx,
|
||||
ville: raw.ville_etab,
|
||||
departement: raw.dep,
|
||||
departementLib: raw.dep_lib,
|
||||
region: raw.region_etab_aff,
|
||||
academie: raw.acad_mies,
|
||||
contrat: raw.contrat_etab,
|
||||
nom: brut.lib_for_voe_ins,
|
||||
etablissement: brut.g_ea_lib_vx,
|
||||
ville: brut.ville_etab,
|
||||
departement: brut.dep,
|
||||
departementLib: brut.dep_lib,
|
||||
region: brut.region_etab_aff,
|
||||
academie: brut.acad_mies,
|
||||
contrat: brut.contrat_etab,
|
||||
|
||||
filiere: raw.fili,
|
||||
selectivite: raw.select_form,
|
||||
filiere: brut.fili,
|
||||
selectivite: brut.select_form,
|
||||
|
||||
capacite: raw.capa_fin,
|
||||
candidats: raw.voe_tot,
|
||||
admis: raw.acc_tot,
|
||||
capacite: brut.capa_fin,
|
||||
candidats: brut.voe_tot,
|
||||
admis: brut.acc_tot,
|
||||
tauxAcces: taux,
|
||||
|
||||
latitude: raw.g_olocalisation_des_formations
|
||||
? raw.g_olocalisation_des_formations.lat
|
||||
: null,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
|
||||
longitude: raw.g_olocalisation_des_formations
|
||||
? raw.g_olocalisation_des_formations.lon
|
||||
: null,
|
||||
pctFemmes: brut.pct_f,
|
||||
pctBoursiers: brut.pct_bours,
|
||||
pctNeoBac: brut.pct_neobac,
|
||||
|
||||
pctFemmes: raw.pct_f,
|
||||
pctBoursiers: raw.pct_bours,
|
||||
pctNeoBac: raw.pct_neobac,
|
||||
pctGeneral: brut.pct_bg,
|
||||
pctTechno: brut.pct_bt,
|
||||
pctPro: brut.pct_bp,
|
||||
|
||||
pctGeneral: raw.pct_bg,
|
||||
pctTechno: raw.pct_bt,
|
||||
pctPro: raw.pct_bp,
|
||||
pctSansMention: brut.pct_sansmention,
|
||||
pctAB: brut.pct_ab,
|
||||
pctB: brut.pct_b,
|
||||
pctTB: brut.pct_tb,
|
||||
pctTBF: brut.pct_tbf,
|
||||
|
||||
pctSansMention: raw.pct_sansmention,
|
||||
pctAB: raw.pct_ab,
|
||||
pctB: raw.pct_b,
|
||||
pctTB: raw.pct_tb,
|
||||
pctTBF: raw.pct_tbf,
|
||||
pctDebutPhase: brut.pct_acc_debutpp,
|
||||
pctDateBac: brut.pct_acc_datebac,
|
||||
pctFinPhase: brut.pct_acc_finpp,
|
||||
|
||||
pctDebutPhase: raw.pct_acc_debutpp,
|
||||
pctDateBac: raw.pct_acc_datebac,
|
||||
pctFinPhase: raw.pct_acc_finpp,
|
||||
admisDebutPhase: brut.acc_debutpp,
|
||||
admisDateBac: brut.acc_datebac,
|
||||
admisFinPhase: brut.acc_finpp,
|
||||
|
||||
admisDebutPhase: raw.acc_debutpp,
|
||||
admisDateBac: raw.acc_datebac,
|
||||
admisFinPhase: raw.acc_finpp,
|
||||
// Phase principale
|
||||
voePPGeneral: brut.nb_voe_pp_bg,
|
||||
voePPTechno: brut.nb_voe_pp_bt,
|
||||
voePPPro: brut.nb_voe_pp_bp,
|
||||
voePPAutres: brut.nb_voe_pp_at,
|
||||
voePPTotal: brut.nb_voe_pp,
|
||||
|
||||
// phase principale
|
||||
voePPGeneral: raw.nb_voe_pp_bg,
|
||||
voePPTechno: raw.nb_voe_pp_bt,
|
||||
voePPPro: raw.nb_voe_pp_bp,
|
||||
voePPAutres: raw.nb_voe_pp_at,
|
||||
voePPTotal: raw.nb_voe_pp,
|
||||
classesPPGeneral: brut.nb_cla_pp_bg,
|
||||
classesPPTechno: brut.nb_cla_pp_bt,
|
||||
classesPPPro: brut.nb_cla_pp_bp,
|
||||
classesPPAutres: brut.nb_cla_pp_at,
|
||||
classesPPTotal: brut.nb_cla_pp,
|
||||
|
||||
classesPPGeneral: raw.nb_cla_pp_bg,
|
||||
classesPPTechno: raw.nb_cla_pp_bt,
|
||||
classesPPPro: raw.nb_cla_pp_bp,
|
||||
classesPPAutres: raw.nb_cla_pp_at,
|
||||
classesPPTotal: raw.nb_cla_pp,
|
||||
propositionsPPGeneral: brut.prop_tot_bg,
|
||||
propositionsPPTechno: brut.prop_tot_bt,
|
||||
propositionsPPPro: brut.prop_tot_bp,
|
||||
propositionsPPAutres: brut.prop_tot_at,
|
||||
propositionsPPTotal: brut.prop_tot,
|
||||
|
||||
propositionsPPGeneral: raw.prop_tot_bg,
|
||||
propositionsPPTechno: raw.prop_tot_bt,
|
||||
propositionsPPPro: raw.prop_tot_bp,
|
||||
propositionsPPAutres: raw.prop_tot_at,
|
||||
propositionsPPTotal: raw.prop_tot,
|
||||
acceptesPPGeneral: brut.acc_bg,
|
||||
acceptesPPTechno: brut.acc_bt,
|
||||
acceptesPPPro: brut.acc_bp,
|
||||
acceptesPPAutres: brut.acc_at,
|
||||
acceptesPPTotal: brut.acc_pp,
|
||||
|
||||
acceptesPPGeneral: raw.acc_bg,
|
||||
acceptesPPTechno: raw.acc_bt,
|
||||
acceptesPPPro: raw.acc_bp,
|
||||
acceptesPPAutres: raw.acc_at,
|
||||
acceptesPPTotal: raw.acc_pp,
|
||||
// Phase complémentaire
|
||||
voePCGeneral: brut.nb_voe_pc_bg,
|
||||
voePCTechno: brut.nb_voe_pc_bt,
|
||||
voePCPro: brut.nb_voe_pc_bp,
|
||||
voePCAutres: brut.nb_voe_pc_at,
|
||||
voePCTotal: brut.nb_voe_pc,
|
||||
|
||||
// phase complémentaire
|
||||
voePCGeneral: raw.nb_voe_pc_bg,
|
||||
voePCTechno: raw.nb_voe_pc_bt,
|
||||
voePCPro: raw.nb_voe_pc_bp,
|
||||
voePCAutres: raw.nb_voe_pc_at,
|
||||
voePCTotal: raw.nb_voe_pc,
|
||||
|
||||
classesPCTotal: raw.nb_cla_pc,
|
||||
acceptesPCTotal: raw.acc_pc
|
||||
classesPCTotal: brut.nb_cla_pc,
|
||||
acceptesPCTotal: brut.acc_pc
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
export function createFormation(raw) {
|
||||
let taux = 0
|
||||
|
||||
if (raw.voe_tot && raw.voe_tot > 0) {
|
||||
taux = Math.round((raw.acc_tot / raw.voe_tot) * 100)
|
||||
}
|
||||
|
||||
return {
|
||||
id: raw.cod_uai + "-" + raw.lib_for_voe_ins,
|
||||
nom: raw.lib_for_voe_ins,
|
||||
etablissement: raw.g_ea_lib_vx,
|
||||
ville: raw.ville_etab,
|
||||
departement: raw.dep,
|
||||
filiere: raw.fili,
|
||||
selectivite: raw.select_form,
|
||||
capacite: raw.capa_fin,
|
||||
candidats: raw.voe_tot,
|
||||
admis: raw.acc_tot,
|
||||
tauxAcces: taux,
|
||||
latitude: raw.g_olocalisation_des_formations ? raw.g_olocalisation_des_formations.lat : null,
|
||||
longitude: raw.g_olocalisation_des_formations ? raw.g_olocalisation_des_formations.lon : null
|
||||
}
|
||||
}
|
||||
+30
-10
@@ -12,27 +12,47 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/riot@9/riot+compiler.min.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<body>
|
||||
<app></app>
|
||||
|
||||
<script src="./components/search-bar.riot" type="riot"></script>
|
||||
<script src="./components/result-list.riot" type="riot"></script>
|
||||
<script src="./components/detail-view.riot" type="riot"></script>
|
||||
<script src="./components/map-view.riot" type="riot"></script>
|
||||
<script src="./components/auth-panel.riot" type="riot"></script>
|
||||
<script src="./app.riot" type="riot"></script>
|
||||
|
||||
<script type="module">
|
||||
import { fetchFormations } from './api.js'
|
||||
import { createFormation } from './formation.js'
|
||||
import { chargerFormations, chargerHistoriqueFormation } from './api.js'
|
||||
import { creerFormation } from './formation.js'
|
||||
import {
|
||||
auth,
|
||||
db,
|
||||
createAccount,
|
||||
login,
|
||||
logout,
|
||||
onUserChanged,
|
||||
saveUserData,
|
||||
loadUserData
|
||||
} from './firebase.js'
|
||||
|
||||
window.fetchFormations = fetchFormations
|
||||
window.createFormation = createFormation
|
||||
</script>
|
||||
window.chargerFormations = chargerFormations
|
||||
window.creerFormation = creerFormation
|
||||
window.chargerHistoriqueFormation = chargerHistoriqueFormation
|
||||
|
||||
<script>
|
||||
riot.compile().then(() => {
|
||||
window.firebaseServices = {
|
||||
auth,
|
||||
db,
|
||||
createAccount,
|
||||
login,
|
||||
logout,
|
||||
onUserChanged,
|
||||
saveUserData,
|
||||
loadUserData
|
||||
}
|
||||
|
||||
await riot.compile()
|
||||
riot.mount('app')
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Parcoursup - test API</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Mini projet Parcoursup</h1>
|
||||
<p>Recherche de formations Parcoursup</p>
|
||||
|
||||
<input id="search" type="text" placeholder="Ex : BUT informatique" />
|
||||
<button id="btn-test">Rechercher</button>
|
||||
|
||||
<div id="output">En attente...</div>
|
||||
|
||||
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,81 +0,0 @@
|
||||
import { fetchFormations } from "./api.js"
|
||||
import { createFormation } from "./formation.js"
|
||||
|
||||
let lastFormations = []
|
||||
|
||||
const button = document.getElementById("btn-test")
|
||||
const output = document.getElementById("output")
|
||||
const searchInput = document.getElementById("search")
|
||||
|
||||
function showDetail(index) {
|
||||
const f = lastFormations[index]
|
||||
|
||||
output.innerHTML = `
|
||||
<div style="border:1px solid #ccc; padding:10px; margin:10px;">
|
||||
<h2>${f.nom}</h2>
|
||||
<p><b>Établissement :</b> ${f.etablissement}</p>
|
||||
<p><b>Ville :</b> ${f.ville}</p>
|
||||
<p><b>Département :</b> ${f.departement}</p>
|
||||
<p><b>Filière :</b> ${f.filiere}</p>
|
||||
<p><b>Sélectivité :</b> ${f.selectivite}</p>
|
||||
<p><b>Capacité :</b> ${f.capacite}</p>
|
||||
<p><b>Candidats :</b> ${f.candidats}</p>
|
||||
<p><b>Admis :</b> ${f.admis}</p>
|
||||
<p><b>Taux d'accès :</b> ${f.tauxAcces}%</p>
|
||||
<button id="back-btn">Retour</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
const backBtn = document.getElementById("back-btn")
|
||||
backBtn.addEventListener("click", testAPI)
|
||||
}
|
||||
|
||||
window.showDetail = showDetail
|
||||
|
||||
async function testAPI() {
|
||||
output.textContent = "Chargement..."
|
||||
|
||||
try {
|
||||
const query = searchInput.value
|
||||
|
||||
const data = await fetchFormations(query)
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
const formations = []
|
||||
|
||||
for (let i = 0; i < data.results.length; i++) {
|
||||
const raw = data.results[i]
|
||||
const formation = createFormation(raw)
|
||||
formations.push(formation)
|
||||
}
|
||||
|
||||
lastFormations = formations
|
||||
|
||||
let html = ""
|
||||
|
||||
for (let i = 0; i < formations.length; i++) {
|
||||
const f = formations[i]
|
||||
|
||||
html += `
|
||||
<div style="border:1px solid #ccc; padding:10px; margin:10px;">
|
||||
<h3>${f.nom}</h3>
|
||||
<p><b>Établissement :</b> ${f.etablissement}</p>
|
||||
<p><b>Ville :</b> ${f.ville} (${f.departement})</p>
|
||||
<p><b>Filière :</b> ${f.filiere}</p>
|
||||
<p><b>Taux d'accès :</b> ${f.tauxAcces}%</p>
|
||||
<button onclick="showDetail(${i})">Voir détail</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
output.innerHTML = html
|
||||
} else {
|
||||
output.textContent = "Aucun résultat trouvé"
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur :", error)
|
||||
output.textContent = "Erreur lors de la requête"
|
||||
}
|
||||
}
|
||||
|
||||
button.addEventListener("click", testAPI)
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "parcoursup",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user