From e2abf454272ad91494821811a569ef80042d7a1a Mon Sep 17 00:00:00 2001 From: Benjamin Ramet Date: Mon, 21 Oct 2024 10:27:15 +0200 Subject: [PATCH 1/2] Add stats to 1Village Tab and tweak the queries to check whether villageId is in the req --- server/controllers/statistics.ts | 10 + server/stats/villageStats.ts | 81 +++---- src/api/statistics/statistics.get.ts | 13 + .../dashboard-statistics/GlobalStats.tsx | 224 ++++++++++++++---- 4 files changed, 237 insertions(+), 91 deletions(-) diff --git a/server/controllers/statistics.ts b/server/controllers/statistics.ts index 1cc1098ed..72e8a5de9 100644 --- a/server/controllers/statistics.ts +++ b/server/controllers/statistics.ts @@ -51,6 +51,16 @@ statisticsController.get({ path: '/classrooms' }, async (_req, res) => { }); }); +statisticsController.get({ path: '/onevillage' }, async (_req, res) => { + res.sendJSON({ + familyAccountsCount: await getFamilyAccountsCount(), + childrenCodesCount: await getChildrenCodesCount(), + connectedFamiliesCount: await getConnectedFamiliesCount(), + familiesWithoutAccount: await getFamiliesWithoutAccount(), + floatingAccounts: await getFloatingAccounts(), + }); +}); + statisticsController.get({ path: '/villages/:villageId' }, async (_req, res) => { const villageId = parseInt(_req.params.villageId); res.sendJSON({ diff --git a/server/stats/villageStats.ts b/server/stats/villageStats.ts index 0ea3d0ac9..43a526e87 100644 --- a/server/stats/villageStats.ts +++ b/server/stats/villageStats.ts @@ -5,68 +5,69 @@ import { AppDataSource } from '../utils/data-source'; const userRepository = AppDataSource.getRepository(User); const studentRepository = AppDataSource.getRepository(Student); -export const getChildrenCodesCount = async (villageId: number) => { - const childrenCodeCount = await studentRepository - .createQueryBuilder('student') - .innerJoin('student.classroom', 'classroom') - .innerJoin('classroom.village', 'village') - .where('classroom.villageId = :villageId', { villageId }) - .getCount(); +export const getChildrenCodesCount = async (villageId?: number) => { + const query = studentRepository.createQueryBuilder('student').innerJoin('student.classroom', 'classroom').innerJoin('classroom.village', 'village'); + + if (villageId) query.where('classroom.villageId = :villageId', { villageId }); + const childrenCodeCount = await query.getCount(); return childrenCodeCount; }; -export const getFamilyAccountsCount = async (villageId: number) => { - const familyAccountsCount = await userRepository +export const getFamilyAccountsCount = async (villageId?: number) => { + const query = userRepository .createQueryBuilder('user') .innerJoin('user.village', 'village') .innerJoin('classroom', 'classroom', 'classroom.villageId = village.id') - .innerJoin('student', 'student', 'student.classroomId = classroom.id') - .where('classroom.villageId = :villageId', { villageId }) - .groupBy('user.id') - .getCount(); + .innerJoin('student', 'student', 'student.classroomId = classroom.id'); + + if (villageId) query.where('classroom.villageId = :villageId', { villageId }); + + query.groupBy('user.id'); + const familyAccountsCount = await query.getCount(); return familyAccountsCount; }; -export const getConnectedFamiliesCount = async (villageId: number) => { - const connectedFamiliesCount = await studentRepository +export const getConnectedFamiliesCount = async (villageId?: number) => { + const query = studentRepository .createQueryBuilder('student') .innerJoin('classroom', 'classroom', 'classroom.id = student.classroomId') - .where('classroom.villageId = :villageId', { villageId }) - .andWhere('student.numLinkedAccount >= 1') - .getCount(); + .where('student.numLinkedAccount >= 1'); + if (villageId) query.andWhere('classroom.villageId = :villageId', { villageId }); + + const connectedFamiliesCount = await query.getCount(); return connectedFamiliesCount; }; -export const getFamiliesWithoutAccount = async (villageId: number) => { - const familiesWithoutAccount = await studentRepository +export const getFamiliesWithoutAccount = async (villageId?: number) => { + const query = studentRepository .createQueryBuilder('student') .innerJoin('student.classroom', 'classroom') .innerJoin('classroom.user', 'user') .innerJoin('user.village', 'village') - .where('classroom.villageId = :villageId', { villageId }) - .andWhere('student.numLinkedAccount < 1') - .select([ - 'classroom.name AS classroom_name', - 'classroom.countryCode as classroom_country', - 'student.firstname AS student_firstname', - 'student.lastname AS student_lastname', - 'student.id AS student_id', - 'student.createdAt as student_creation_date', - 'village.name AS village_name', - ]) - .getRawMany(); + .where('student.numLinkedAccount < 1'); + if (villageId) query.andWhere('classroom.villageId = :villageId', { villageId }); + + query.select([ + 'classroom.name AS classroom_name', + 'classroom.countryCode as classroom_country', + 'student.firstname AS student_firstname', + 'student.lastname AS student_lastname', + 'student.id AS student_id', + 'student.createdAt as student_creation_date', + 'village.name AS village_name', + ]); + const familiesWithoutAccount = query.getRawMany(); return familiesWithoutAccount; }; -export const getFloatingAccounts = async (villageId: number) => { - const floatingAccounts = await userRepository - .createQueryBuilder('user') - .where('user.villageId = :villageId', { villageId }) - .andWhere('user.hasStudentLinked = 0') - .andWhere('user.type = 4') - .select(['user.id', 'user.firstname', 'user.lastname', 'user.language', 'user.email', 'user.createdAt']) - .getMany(); +export const getFloatingAccounts = async (villageId?: number) => { + const query = userRepository.createQueryBuilder('user').where('user.hasStudentLinked = 0').andWhere('user.type = 4'); + + if (villageId) query.andWhere('user.villageId = :villageId', { villageId }); + + query.select(['user.id', 'user.firstname', 'user.lastname', 'user.language', 'user.email', 'user.createdAt']); + const floatingAccounts = query.getMany(); return floatingAccounts; }; diff --git a/src/api/statistics/statistics.get.ts b/src/api/statistics/statistics.get.ts index e2c81c6e3..a7e589dd6 100644 --- a/src/api/statistics/statistics.get.ts +++ b/src/api/statistics/statistics.get.ts @@ -13,6 +13,16 @@ async function getSessionsStats(phase: number | null): Promise { ).data; } +async function getOneVillageStats(): Promise { + return ( + await axiosRequest({ + method: 'GET', + baseURL: '/api', + url: `/statistics/onevillage`, + }) + ).data; +} + async function getVillagesStats(villageId: number | null): Promise { return ( await axiosRequest({ @@ -26,6 +36,9 @@ async function getVillagesStats(villageId: number | null): Promise export const useGetSessionsStats = (phase: number | null) => { return useQuery(['sessions-stats'], () => getSessionsStats(phase)); }; +export const useGetOneVillageStats = () => { + return useQuery(['1v-stats'], () => getOneVillageStats()); +}; export const useGetVillagesStats = (villageId: number | null) => { return useQuery(['villages-stats', villageId], () => getVillagesStats(villageId), { diff --git a/src/components/admin/dashboard-statistics/GlobalStats.tsx b/src/components/admin/dashboard-statistics/GlobalStats.tsx index db98cadd8..449250596 100644 --- a/src/components/admin/dashboard-statistics/GlobalStats.tsx +++ b/src/components/admin/dashboard-statistics/GlobalStats.tsx @@ -2,20 +2,103 @@ import React from 'react'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import VisibilityIcon from '@mui/icons-material/Visibility'; -import { Grid } from '@mui/material'; +import { Box, Grid, Tab, Tabs, Typography } from '@mui/material'; +import { OneVillageTable } from '../OneVillageTable'; import TeamComments from './TeamComments'; import AverageStatsCard from './cards/AverageStatsCard/AverageStatsCard'; import ClassesExchangesCard from './cards/ClassesExchangesCard/ClassesExchangesCard'; import StatsCard from './cards/StatsCard/StatsCard'; import DashboardWorldMap from './map/DashboardWorldMap/DashboardWorldMap'; -import { useGetSessionsStats } from 'src/api/statistics/statistics.get'; +import styles from './styles/charts.module.css'; +import { useGetOneVillageStats, useGetSessionsStats } from 'src/api/statistics/statistics.get'; +import { formatDate } from 'src/utils'; +import type { FamiliesWithoutAccount, FloatingAccount, OneVillageTableRow } from 'types/statistics.type'; +function createFamiliesWithoutAccountRows(data: Array): Array { + return data.map((row) => { + return { + id: row.student_id, + student: `${row.student_firstname} ${row.student_lastname}`, + vm: row.village_name, + classroom: row.classroom_name, + country: row.classroom_country, + creationDate: row.student_creation_date ? formatDate(row.student_creation_date) : 'Donnée non disponible', + }; + }); +} +function createFloatingAccountsRows(data: Array): Array { + return data.map((row) => { + return { + id: row.id, + family: `${row.firstname} ${row.lastname}`, + language: row.language, + email: row.email, + creationDate: row.createdAt ? formatDate(row.createdAt) : 'Donnée non disponible', + }; + }); +} +const FamiliesWithoutAccountHeaders = [ + { key: 'student', label: 'Nom Prénom Enfant', sortable: true }, + { key: 'vm', label: 'Village-Monde', sortable: true }, + { key: 'classroom', label: 'Classe', sortable: true }, + { key: 'country', label: 'Pays', sortable: true }, + { key: 'creationDate', label: 'Date de création identifiant', sortable: true }, +]; +const FloatingAccountsHeaders = [ + { key: 'family', label: 'Nom Prénom Famille', sortable: true }, + { key: 'language', label: 'Langue', sortable: true }, + { key: 'email', label: 'Mail', sortable: true }, + { key: 'creationDate', label: 'Date de création compte', sortable: true }, +]; const GlobalStats = () => { + const [value, setValue] = React.useState(0); const sessionsStats = useGetSessionsStats(null); + const oneVillageStats = useGetOneVillageStats(); + const [familiesWithoutAccountRows, setFamiliesWithoutAccountRows] = React.useState>([]); + const [floatingAccountsRows, setFloatingAccountsRows] = React.useState>([]); + React.useEffect(() => { + if (oneVillageStats.data?.familiesWithoutAccount) { + setFamiliesWithoutAccountRows([]); + setFamiliesWithoutAccountRows(createFamiliesWithoutAccountRows(oneVillageStats.data?.familiesWithoutAccount)); + } + if (oneVillageStats.data?.floatingAccounts) { + setFloatingAccountsRows([]); + setFloatingAccountsRows(createFloatingAccountsRows(oneVillageStats.data?.floatingAccounts)); + } + }, [oneVillageStats.data?.familiesWithoutAccount, oneVillageStats.data?.floatingAccounts]); if (sessionsStats.isError) return

Error!

; if (sessionsStats.isLoading || sessionsStats.isIdle) return

Loading...

; + interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; + } + function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); + } + + function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; + } + + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; // eslint-disable-next-line no-console console.log('Sessions stats', sessionsStats.data); @@ -24,54 +107,59 @@ const GlobalStats = () => { <> - - - - Nombre de classes
- inscrites -
-
- - - Nombre de classes
connectées -
-
- - - Nombre de classes
contributrices -
-
- - } - > - Temps de connexion moyen par classe - - - - } - > - Nombre de connexions moyen par classe - - - - - - {/*
+ + + + + + + + + Nombre de classes
+ inscrites +
+
+ + + Nombre de classes
connectées +
+
+ + + Nombre de classes
contributrices +
+
+ + } + > + Temps de connexion moyen par classe + + + + } + > + Nombre de connexions moyen par classe + + + + + + {/*
{ ]} />
*/} -
+ +
+ + <> + {'Pas de données'}

} + data={familiesWithoutAccountRows} + columns={FamiliesWithoutAccountHeaders} + titleContent={`À surveiller : comptes non créés (${familiesWithoutAccountRows.length})`} + /> + {'Pas de données'}

} + data={floatingAccountsRows} + columns={FloatingAccountsHeaders} + titleContent={`À surveiller : comptes flottants (${floatingAccountsRows.length})`} + /> + + Nombre de profs ayant créé des comptes famille + Nombre de codes enfant créés + Nombre de familles connectées + + +
); }; From 6ece7a35f331d5629e904c6c9ae9f5d9d2f054c3 Mon Sep 17 00:00:00 2001 From: Benjamin Ramet Date: Mon, 21 Oct 2024 11:47:13 +0200 Subject: [PATCH 2/2] Refactor tabPanel component, and constants for table headers --- .../dashboard-statistics/GlobalStats.tsx | 84 +++---------------- .../admin/dashboard-statistics/TabPanel.tsx | 23 +++++ .../dashboard-statistics/VillageStats.tsx | 72 ++++------------ .../utils/tableCreator.ts | 23 +++++ .../utils/tableHeaders.ts | 14 ++++ 5 files changed, 85 insertions(+), 131 deletions(-) create mode 100644 src/components/admin/dashboard-statistics/TabPanel.tsx create mode 100644 src/components/admin/dashboard-statistics/utils/tableCreator.ts create mode 100644 src/components/admin/dashboard-statistics/utils/tableHeaders.ts diff --git a/src/components/admin/dashboard-statistics/GlobalStats.tsx b/src/components/admin/dashboard-statistics/GlobalStats.tsx index 449250596..04e867cb9 100644 --- a/src/components/admin/dashboard-statistics/GlobalStats.tsx +++ b/src/components/admin/dashboard-statistics/GlobalStats.tsx @@ -2,55 +2,21 @@ import React from 'react'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import VisibilityIcon from '@mui/icons-material/Visibility'; -import { Box, Grid, Tab, Tabs, Typography } from '@mui/material'; +import { Box, Grid, Tab, Tabs } from '@mui/material'; import { OneVillageTable } from '../OneVillageTable'; +import TabPanel from './TabPanel'; import TeamComments from './TeamComments'; import AverageStatsCard from './cards/AverageStatsCard/AverageStatsCard'; import ClassesExchangesCard from './cards/ClassesExchangesCard/ClassesExchangesCard'; import StatsCard from './cards/StatsCard/StatsCard'; import DashboardWorldMap from './map/DashboardWorldMap/DashboardWorldMap'; import styles from './styles/charts.module.css'; +import { createFamiliesWithoutAccountRows, createFloatingAccountsRows } from './utils/tableCreator'; +import { FamiliesWithoutAccountHeaders, FloatingAccountsHeaders } from './utils/tableHeaders'; import { useGetOneVillageStats, useGetSessionsStats } from 'src/api/statistics/statistics.get'; -import { formatDate } from 'src/utils'; -import type { FamiliesWithoutAccount, FloatingAccount, OneVillageTableRow } from 'types/statistics.type'; +import type { OneVillageTableRow } from 'types/statistics.type'; -function createFamiliesWithoutAccountRows(data: Array): Array { - return data.map((row) => { - return { - id: row.student_id, - student: `${row.student_firstname} ${row.student_lastname}`, - vm: row.village_name, - classroom: row.classroom_name, - country: row.classroom_country, - creationDate: row.student_creation_date ? formatDate(row.student_creation_date) : 'Donnée non disponible', - }; - }); -} -function createFloatingAccountsRows(data: Array): Array { - return data.map((row) => { - return { - id: row.id, - family: `${row.firstname} ${row.lastname}`, - language: row.language, - email: row.email, - creationDate: row.createdAt ? formatDate(row.createdAt) : 'Donnée non disponible', - }; - }); -} -const FamiliesWithoutAccountHeaders = [ - { key: 'student', label: 'Nom Prénom Enfant', sortable: true }, - { key: 'vm', label: 'Village-Monde', sortable: true }, - { key: 'classroom', label: 'Classe', sortable: true }, - { key: 'country', label: 'Pays', sortable: true }, - { key: 'creationDate', label: 'Date de création identifiant', sortable: true }, -]; -const FloatingAccountsHeaders = [ - { key: 'family', label: 'Nom Prénom Famille', sortable: true }, - { key: 'language', label: 'Langue', sortable: true }, - { key: 'email', label: 'Mail', sortable: true }, - { key: 'creationDate', label: 'Date de création compte', sortable: true }, -]; const GlobalStats = () => { const [value, setValue] = React.useState(0); const sessionsStats = useGetSessionsStats(null); @@ -70,48 +36,20 @@ const GlobalStats = () => { if (sessionsStats.isError) return

Error!

; if (sessionsStats.isLoading || sessionsStats.isIdle) return

Loading...

; - interface TabPanelProps { - children?: React.ReactNode; - index: number; - value: number; - } - function CustomTabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); - } - - function a11yProps(index: number) { - return { - id: `simple-tab-${index}`, - 'aria-controls': `simple-tabpanel-${index}`, - }; - } const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; - // eslint-disable-next-line no-console - console.log('Sessions stats', sessionsStats.data); - return ( <> - - + + - + @@ -187,8 +125,8 @@ const GlobalStats = () => { />
*/}
- - + + <> { Nombre de familles connectées - + ); }; diff --git a/src/components/admin/dashboard-statistics/TabPanel.tsx b/src/components/admin/dashboard-statistics/TabPanel.tsx new file mode 100644 index 000000000..2d5ca7db9 --- /dev/null +++ b/src/components/admin/dashboard-statistics/TabPanel.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { Box, Typography } from '@mui/material'; + +interface TabPanelProps { + children?: React.ReactNode; + value: number; + index: number; +} + +const TabPanel = ({ children, value, index, ...other }: TabPanelProps) => { + return ( + + ); +}; + +export default TabPanel; diff --git a/src/components/admin/dashboard-statistics/VillageStats.tsx b/src/components/admin/dashboard-statistics/VillageStats.tsx index 795341f37..6df9b0563 100644 --- a/src/components/admin/dashboard-statistics/VillageStats.tsx +++ b/src/components/admin/dashboard-statistics/VillageStats.tsx @@ -3,28 +3,27 @@ import React, { useState } from 'react'; import Box from '@mui/material/Box'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; import { OneVillageTable } from '../OneVillageTable'; +import TabPanel from './TabPanel'; import StatsCard from './cards/StatsCard/StatsCard'; import CountriesDropdown from './filters/CountriesDropdown'; import VillageDropdown from './filters/VillageDropdown'; import { PelicoCard } from './pelico-card'; import styles from './styles/charts.module.css'; +import { createFamiliesWithoutAccountRows } from './utils/tableCreator'; +import { FamiliesWithoutAccountHeaders } from './utils/tableHeaders'; import { useGetVillagesStats } from 'src/api/statistics/statistics.get'; import { useCountries } from 'src/services/useCountries'; import { useVillages } from 'src/services/useVillages'; -import { formatDate } from 'src/utils'; -import type { FamiliesWithoutAccount, OneVillageTableRow } from 'types/statistics.type'; +import type { OneVillageTableRow } from 'types/statistics.type'; import type { VillageFilter } from 'types/village.type'; const VillageStats = () => { const [selectedCountry, setSelectedCountry] = useState(''); const [selectedVillage, setSelectedVillage] = useState(''); const [options, setOptions] = useState({ countryIsoCode: '' }); - - const pelicoMessage = 'Merci de sélectionner un village-monde pour analyser ses statistiques '; - const noDataFoundMessage = 'Pas de données pour le Village-Monde sélectionné'; + const [value, setValue] = React.useState(0); const { countries } = useCountries(); @@ -39,10 +38,9 @@ const VillageStats = () => { const [familiesWithoutAccountRows, setFamiliesWithoutAccountRows] = React.useState>([]); React.useEffect(() => { if (villagesStats.data?.familiesWithoutAccount) { - setFamiliesWithoutAccountRows([]); setFamiliesWithoutAccountRows(createFamiliesWithoutAccountRows(villagesStats.data?.familiesWithoutAccount)); } - }, [villagesStats.data?.familiesWithoutAccount, villagesStats.data?.floatingAccounts]); + }, [villagesStats.data?.familiesWithoutAccount]); const handleCountryChange = (country: string) => { setSelectedCountry(country); @@ -53,54 +51,12 @@ const VillageStats = () => { setSelectedVillage(village); }; - interface TabPanelProps { - children?: React.ReactNode; - index: number; - value: number; - } - - function a11yProps(index: number) { - return { - id: `simple-tab-${index}`, - 'aria-controls': `simple-tabpanel-${index}`, - }; - } - const [value, setValue] = React.useState(0); const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; - const FamiliesWithoutAccountHeaders = [ - { key: 'student', label: 'Nom Prénom Enfant', sortable: true }, - { key: 'vm', label: 'Village-Monde', sortable: true }, - { key: 'classroom', label: 'Classe', sortable: true }, - { key: 'country', label: 'Pays', sortable: true }, - { key: 'creationDate', label: 'Date de création identifiant', sortable: true }, - ]; - function CustomTabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props; - return ( - - ); - } - function createFamiliesWithoutAccountRows(data: Array): Array { - return data.map((row) => { - return { - id: row.student_id, - student: `${row.student_firstname} ${row.student_lastname}`, - vm: row.village_name, - classroom: row.classroom_name, - country: row.classroom_country, - creationDate: row.student_creation_date ? formatDate(row.student_creation_date) : 'Donnée non disponible', - }; - }); - } + const pelicoMessage = 'Merci de sélectionner un village-monde pour analyser ses statistiques '; + const noDataFoundMessage = 'Pas de données pour le Village-Monde sélectionné'; return ( <> { - - + + - +

Statistiques - En classe

-
- + + {!selectedVillage ? ( ) : ( @@ -157,7 +113,7 @@ const VillageStats = () => { )} - + ); }; diff --git a/src/components/admin/dashboard-statistics/utils/tableCreator.ts b/src/components/admin/dashboard-statistics/utils/tableCreator.ts new file mode 100644 index 000000000..b296d917c --- /dev/null +++ b/src/components/admin/dashboard-statistics/utils/tableCreator.ts @@ -0,0 +1,23 @@ +import { formatDate } from 'src/utils'; +import type { FamiliesWithoutAccount, FloatingAccount, OneVillageTableRow } from 'types/statistics.type'; + +export function createFamiliesWithoutAccountRows(data: FamiliesWithoutAccount[]): OneVillageTableRow[] { + return data.map((row) => ({ + id: row.student_id, + student: `${row.student_firstname} ${row.student_lastname}`, + vm: row.village_name, + classroom: row.classroom_name, + country: row.classroom_country, + creationDate: row.student_creation_date ? formatDate(row.student_creation_date) : 'Donnée non disponible', + })); +} + +export function createFloatingAccountsRows(data: FloatingAccount[]): OneVillageTableRow[] { + return data.map((row) => ({ + id: row.id, + family: `${row.firstname} ${row.lastname}`, + language: row.language, + email: row.email, + creationDate: row.createdAt ? formatDate(row.createdAt) : 'Donnée non disponible', + })); +} diff --git a/src/components/admin/dashboard-statistics/utils/tableHeaders.ts b/src/components/admin/dashboard-statistics/utils/tableHeaders.ts new file mode 100644 index 000000000..7bb844548 --- /dev/null +++ b/src/components/admin/dashboard-statistics/utils/tableHeaders.ts @@ -0,0 +1,14 @@ +export const FamiliesWithoutAccountHeaders = [ + { key: 'student', label: 'Nom Prénom Enfant', sortable: true }, + { key: 'vm', label: 'Village-Monde', sortable: true }, + { key: 'classroom', label: 'Classe', sortable: true }, + { key: 'country', label: 'Pays', sortable: true }, + { key: 'creationDate', label: 'Date de création identifiant', sortable: true }, +]; + +export const FloatingAccountsHeaders = [ + { key: 'family', label: 'Nom Prénom Famille', sortable: true }, + { key: 'language', label: 'Langue', sortable: true }, + { key: 'email', label: 'Mail', sortable: true }, + { key: 'creationDate', label: 'Date de création compte', sortable: true }, +];