Skip to content

Commit 6c7752d

Browse files
committed
feat: améliorer la gestion des établissements
1 parent 1f05436 commit 6c7752d

File tree

10 files changed

+236
-161
lines changed

10 files changed

+236
-161
lines changed

src/components/EtablissementDetails.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Card from '../design-system/Card';
33
import Typography from '../design-system/Typography';
44
import Button from '../design-system/Button';
55
import { colors, spacing, radii } from '../design-system/tokens';
6-
import { Etablissement } from '../types/etablissement';
6+
import { Etablissement, getEtablissementName, getEtablissementTypes } from '../types/etablissement';
77
import { Isochrone } from '../types/isochrone';
88
import { useEffect, useState } from 'react';
99

@@ -30,7 +30,19 @@ const EtablissementDetails: React.FC<EtablissementDetailsProps> = ({
3030
onClose,
3131
}) => {
3232
const { t } = useTranslation();
33-
const { uai, uo_lib, type_d_etablissement, reg_nom, dep_nom, com_nom, coordonnees, color } = etablissement;
33+
const { uai, siege_lib, implantation_lib, reg_nom, dep_nom, com_nom, coordonnees, color, services, date_ouverture } = etablissement;
34+
const displayName = getEtablissementName(etablissement);
35+
const types = getEtablissementTypes(etablissement);
36+
const normalizedSiege = siege_lib ? siege_lib.replace(/\s+/g, ' ').trim() : undefined;
37+
const normalizedImplantation = implantation_lib ? implantation_lib.replace(/\s+/g, ' ').trim() : undefined;
38+
const servicesValue = services && services.length > 0 ? services.join(', ') : undefined;
39+
const typeValue = types.length > 0 ? types.join(', ') : undefined;
40+
const openingDateValue = (() => {
41+
if (!date_ouverture) return undefined;
42+
const parsed = new Date(date_ouverture);
43+
return Number.isNaN(parsed.getTime()) ? date_ouverture : parsed.toLocaleDateString();
44+
})();
45+
const coordinatesValue = coordonnees ? `${coordonnees.lat.toFixed(5)}, ${coordonnees.lon.toFixed(5)}` : undefined;
3446

3547
const [mounted, setMounted] = useState(false);
3648
useEffect(() => {
@@ -60,18 +72,24 @@ const EtablissementDetails: React.FC<EtablissementDetailsProps> = ({
6072
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
6173
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
6274
<div style={{ width: 12, height: 12, borderRadius: 2, background: color || '#000' }} />
63-
<Typography variant="h2">{uo_lib}</Typography>
75+
<Typography variant="h2">{displayName}</Typography>
6476
</div>
6577
<Button variant="secondary" compact onClick={onClose}>{t('details.close')}</Button>
6678
</div>
6779

6880
<div style={{ marginTop: spacing.md }}>
6981
<Row label={t('details.uai')} value={uai || '—'} />
70-
<Row label={t('details.type')} value={(type_d_etablissement || []).join(', ') || '—'} />
82+
{normalizedSiege && <Row label={t('details.headquarters')} value={normalizedSiege} />}
83+
{normalizedImplantation && normalizedImplantation !== displayName && (
84+
<Row label={t('details.implantation')} value={normalizedImplantation} />
85+
)}
86+
<Row label={t('details.type')} value={typeValue || '—'} />
7187
<Row label={t('details.region')} value={reg_nom || '—'} />
7288
<Row label={t('details.department')} value={dep_nom || '—'} />
7389
<Row label={t('details.city')} value={com_nom || '—'} />
74-
<Row label={t('details.coordinates')} value={`${coordonnees.lat.toFixed(5)}, ${coordonnees.lon.toFixed(5)}`} />
90+
{servicesValue && <Row label={t('details.services')} value={servicesValue} />}
91+
{openingDateValue && <Row label={t('details.openingDate')} value={openingDateValue} />}
92+
<Row label={t('details.coordinates')} value={coordinatesValue || '—'} />
7593
</div>
7694

7795
<div style={{ marginTop: spacing.md }}>

src/components/EtablissementMarkers.tsx

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Marker } from 'react-leaflet';
2-
import { Etablissement } from '../types/etablissement';
2+
import { Etablissement, getEtablissementName } from '../types/etablissement';
33

44
interface EtablissementMarkersProps {
55
etablissements: Etablissement[];
@@ -14,22 +14,29 @@ const EtablissementMarkers: React.FC<EtablissementMarkersProps> = ({
1414
}) => {
1515
return (
1616
<>
17-
{etablissements.map((etablissement) => {
18-
const etabId = etablissement.uai || `${etablissement.coordonnees.lat},${etablissement.coordonnees.lon}`;
19-
return (
20-
<Marker
21-
key={etabId}
22-
position={[etablissement.coordonnees.lat, etablissement.coordonnees.lon]}
23-
title={etablissement.uo_lib}
24-
alt={etablissement.uo_lib}
25-
eventHandlers={{
26-
mouseover: () => onHover(etabId),
27-
mouseout: () => onHover(null),
28-
click: () => onSelect(etabId),
29-
}}
30-
/>
31-
);
32-
})}
17+
{etablissements
18+
.filter(etablissement =>
19+
etablissement.coordonnees &&
20+
etablissement.coordonnees.lat !== undefined &&
21+
etablissement.coordonnees.lon !== undefined
22+
)
23+
.map((etablissement) => {
24+
const etabId = etablissement.uai || `${etablissement.coordonnees.lat},${etablissement.coordonnees.lon}`;
25+
const title = getEtablissementName(etablissement);
26+
return (
27+
<Marker
28+
key={etabId}
29+
position={[etablissement.coordonnees.lat, etablissement.coordonnees.lon]}
30+
title={title}
31+
alt={title}
32+
eventHandlers={{
33+
mouseover: () => onHover(etabId),
34+
mouseout: () => onHover(null),
35+
click: () => onSelect(etabId),
36+
}}
37+
/>
38+
);
39+
})}
3340
</>
3441
);
3542
};

src/components/FilterPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
22
import Select from '../design-system/Select';
33
import Button from '../design-system/Button';
44
import TextInput from '../design-system/TextInput';
5-
import { Etablissement } from '../types/etablissement';
5+
import { Etablissement, getEtablissementTypes } from '../types/etablissement';
66

77
export type Filters = {
88
type?: string;
@@ -19,13 +19,13 @@ interface FilterPanelProps {
1919
onReset?: () => void;
2020
}
2121

22-
const uniqueSorted = (arr: (string | undefined)[]) =>
22+
const uniqueSorted = (arr: (string | null | undefined)[]) =>
2323
Array.from(new Set(arr.filter(Boolean) as string[])).sort((a, b) => a.localeCompare(b));
2424

2525
const FilterPanel: React.FC<FilterPanelProps> = ({ etablissements, values, onChange, onReset }) => {
2626
const { t } = useTranslation();
2727

28-
const types = uniqueSorted((etablissements || []).flatMap(e => e.type_d_etablissement || []));
28+
const types = uniqueSorted((etablissements || []).flatMap(e => getEtablissementTypes(e)));
2929
const regions = uniqueSorted((etablissements || []).map(e => e.reg_nom));
3030
const deps = uniqueSorted((etablissements || []).map(e => e.dep_nom));
3131
const communes = uniqueSorted((etablissements || []).map(e => e.com_nom));

src/components/IsochronePolygons.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Polygon } from 'react-leaflet';
22
import { Isochrone } from '../types/isochrone';
3-
import { Etablissement } from '../types/etablissement';
3+
import { Etablissement, getEtablissementName, getEtablissementTypes } from '../types/etablissement';
44
import { Filters } from './FilterPanel';
55

66
interface IsochronePolygonsProps {
@@ -27,11 +27,14 @@ const IsochronePolygons: React.FC<IsochronePolygonsProps> = ({
2727
if (!etablissements || !iso.etablissementId) return true;
2828
const e = etablissements.find(x => (x.uai || `${x.coordonnees.lat},${x.coordonnees.lon}`) === iso.etablissementId);
2929
if (!e) return false;
30-
if (filters.type && !(e.type_d_etablissement || []).includes(filters.type)) return false;
30+
if (filters.type && !getEtablissementTypes(e).includes(filters.type)) return false;
3131
if (filters.region && e.reg_nom !== filters.region) return false;
3232
if (filters.departement && e.dep_nom !== filters.departement) return false;
3333
if (filters.commune && e.com_nom !== filters.commune) return false;
34-
if (filters.name && !e.uo_lib?.toLowerCase().includes(filters.name.toLowerCase())) return false;
34+
if (filters.name) {
35+
const label = getEtablissementName(e).toLowerCase();
36+
if (!label.includes(filters.name.toLowerCase())) return false;
37+
}
3538
return true;
3639
});
3740

src/components/MapView.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MapContainer, TileLayer } from "react-leaflet";
2-
import { Etablissement } from '../types/etablissement';
2+
import { Etablissement, getEtablissementName, getEtablissementTypes } from '../types/etablissement';
33
import { Isochrone } from '../types/isochrone';
44
import EtablissementMarkers from './EtablissementMarkers';
55
import IsochronePolygons from './IsochronePolygons';
@@ -27,11 +27,14 @@ const MapView: React.FC<MapViewProps> = ({
2727
}) => {
2828
// Apply filters to etablissements
2929
const filteredEtab = etablissements.filter(e => {
30-
if (filters.type && !(e.type_d_etablissement || []).includes(filters.type)) return false;
30+
if (filters.type && !getEtablissementTypes(e).includes(filters.type)) return false;
3131
if (filters.region && e.reg_nom !== filters.region) return false;
3232
if (filters.departement && e.dep_nom !== filters.departement) return false;
3333
if (filters.commune && e.com_nom !== filters.commune) return false;
34-
if (filters.name && !e.uo_lib?.toLowerCase().includes(filters.name.toLowerCase())) return false;
34+
if (filters.name) {
35+
const label = getEtablissementName(e).toLowerCase();
36+
if (!label.includes(filters.name.toLowerCase())) return false;
37+
}
3538
return true;
3639
});
3740

src/dataGouvFetcher.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const dataGouvBaseURL = "https://data.enseignementsup-recherche.gouv.fr"
44

55
const etablissements = (offset: number) =>
66
dataGouvBaseURL +
7-
`/api/explore/v2.1/catalog/datasets/fr-esr-principaux-etablissements-enseignement-superieur/records?order_by=uai&select=coordonnees%2Ctype_d_etablissement%2Cuo_lib%2Cuai%2Creg_nom%2Cdep_nom%2Ccom_nom&limit=100&offset=${offset}&lang=fr&refine=pays_etranger_acheminement%3A%22France%22`
7+
`/api/explore/v2.1/catalog/datasets/fr-esr-implantations_etablissements_d_enseignement_superieur_publics/records?select=uai%2Csiege_lib%2Ctype_d_etablissement%2Cimplantation_lib%2Ccoordonnees%2Cservices%2Cdate_ouverture%2Cur_lib%2Ccom_nom%2Cdep_nom%2Creg_nom&limit=100&offset=${offset}&lang=fr`
88

99
const fetchEtablissementsData = async (offset: number): Promise<Etablissements> => {
1010
const response = await fetch(etablissements(offset))
@@ -13,21 +13,35 @@ const fetchEtablissementsData = async (offset: number): Promise<Etablissements>
1313

1414
export const fetchAllEtablissementsData = async (): Promise<Etablissement[]> => {
1515
let offset = 0
16-
const etablissements = []
16+
const etablissements: Etablissement[] = []
17+
const seenUai = new Set<string>()
18+
1719
let data = await fetchEtablissementsData(offset)
1820
const totalEtablissement = data.total_count
1921

20-
etablissements.push(...data.results)
22+
for (const e of data.results) {
23+
if (e?.uai && e?.coordonnees?.lat != null && e?.coordonnees?.lon != null && !seenUai.has(e.uai)) {
24+
seenUai.add(e.uai)
25+
etablissements.push(e)
26+
}
27+
}
2128

2229
while (totalEtablissement > offset) {
2330
offset += 100
2431
data = await fetchEtablissementsData(offset)
25-
etablissements.push(...data.results)
32+
for (const e of data.results) {
33+
if (e?.uai && e?.coordonnees?.lat != null && e?.coordonnees?.lon != null && !seenUai.has(e.uai)) {
34+
seenUai.add(e.uai)
35+
etablissements.push(e)
36+
}
37+
}
2638
}
2739

28-
return etablissements
29-
.map((etablissement) => ({
30-
...etablissement,
31-
color: `#${Math.floor(Math.random() * 16777215).toString(16)}`
32-
} as Etablissement))
40+
return etablissements.map(
41+
(etablissement) =>
42+
({
43+
...etablissement,
44+
color: `#${Math.floor(Math.random() * 16777215).toString(16)}`
45+
} as Etablissement)
46+
)
3347
}

src/i18n/locales/en.json

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,57 @@
11
{
2-
"header": {
3-
"title": "Universities or schools in France",
4-
"description": "Displaying isochrones of universities and schools in France.",
5-
"loadedFromCache": "Loaded from cache",
6-
"showConfig": "Show settings",
7-
"hideConfig": "Hide settings"
8-
},
9-
"filters": {
10-
"type": "Type",
11-
"all": "All",
12-
"region": "Region",
13-
"allFeminine": "All",
14-
"department": "Department",
15-
"allMasculine": "All",
16-
"city": "City",
17-
"name": "Name (contains)",
18-
"namePlaceholder": "E.g.: Sorbonne",
19-
"reset": "Reset"
20-
},
21-
"time": {
22-
"label": "Time in minutes"
23-
},
24-
"transport": {
25-
"label": "Transport mode",
26-
"driving": "Driving",
27-
"walking": "Walking",
28-
"cycling": "Cycling",
29-
"drivingTraffic": "Driving (Traffic)"
30-
},
31-
"progress": {
32-
"resolved": "Isochrones resolved: {{resolved}} / {{total}} ({{percent}}%)",
33-
"allResolved": "All isochrones resolved!"
34-
},
35-
"details": {
36-
"title": "Establishment Details",
37-
"close": "Close",
38-
"uai": "UAI",
39-
"type": "Type",
40-
"region": "Region",
41-
"department": "Department",
42-
"city": "City",
43-
"coordinates": "Coordinates",
44-
"statistics": "Statistics",
45-
"isochrone": "Isochrone",
46-
"available": "Available",
47-
"unavailable": "In progress / unavailable",
48-
"points": "Points",
49-
"mode": "Mode",
50-
"time": "Time",
51-
"minutes": "{{time}} min"
52-
}
2+
"header": {
3+
"title": "Universities or schools in France",
4+
"description": "Displaying isochrones of universities and schools in France.",
5+
"loadedFromCache": "Loaded from cache",
6+
"showConfig": "Show settings",
7+
"hideConfig": "Hide settings"
8+
},
9+
"filters": {
10+
"type": "Type",
11+
"all": "All",
12+
"region": "Region",
13+
"allFeminine": "All",
14+
"department": "Department",
15+
"allMasculine": "All",
16+
"city": "City",
17+
"name": "Name (contains)",
18+
"namePlaceholder": "E.g.: Sorbonne",
19+
"reset": "Reset"
20+
},
21+
"time": {
22+
"label": "Time in minutes"
23+
},
24+
"transport": {
25+
"label": "Transport mode",
26+
"driving": "Driving",
27+
"walking": "Walking",
28+
"cycling": "Cycling",
29+
"drivingTraffic": "Driving (Traffic)"
30+
},
31+
"progress": {
32+
"resolved": "Isochrones resolved: {{resolved}} / {{total}} ({{percent}}%)",
33+
"allResolved": "All isochrones resolved!"
34+
},
35+
"details": {
36+
"title": "Establishment Details",
37+
"close": "Close",
38+
"uai": "UAI",
39+
"headquarters": "Headquarters",
40+
"implantation": "Campus / Site",
41+
"type": "Type",
42+
"region": "Region",
43+
"department": "Department",
44+
"city": "City",
45+
"services": "Services",
46+
"openingDate": "Opening date",
47+
"coordinates": "Coordinates",
48+
"statistics": "Statistics",
49+
"isochrone": "Isochrone",
50+
"available": "Available",
51+
"unavailable": "In progress / unavailable",
52+
"points": "Points",
53+
"mode": "Mode",
54+
"time": "Time",
55+
"minutes": "{{time}} min"
56+
}
5357
}

0 commit comments

Comments
 (0)