diff --git a/.pnp.cjs b/.pnp.cjs old mode 100755 new mode 100644 diff --git a/.yarn/sdks/eslint/bin/eslint.js b/.yarn/sdks/eslint/bin/eslint.js old mode 100755 new mode 100644 diff --git a/.yarn/sdks/prettier/bin-prettier.js b/.yarn/sdks/prettier/bin-prettier.js old mode 100755 new mode 100644 diff --git a/.yarn/sdks/typescript/bin/tsc b/.yarn/sdks/typescript/bin/tsc old mode 100755 new mode 100644 diff --git a/.yarn/sdks/typescript/bin/tsserver b/.yarn/sdks/typescript/bin/tsserver old mode 100755 new mode 100644 diff --git a/docker-compose.yml b/docker-compose.yml index 55d75d6d6..09ff47bdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.7' services: backend: build: diff --git a/server/controllers/statistics.ts b/server/controllers/statistics.ts index e4616b2cb..aa57358f1 100644 --- a/server/controllers/statistics.ts +++ b/server/controllers/statistics.ts @@ -1,45 +1,55 @@ -import type { Request } from 'express'; - -import { - getClassroomsInfos, - getConnectedClassroomsCount, - getContributedClassroomsCount, - getRegisteredClassroomsCount, -} from '../stats/classroomStats'; -import { - getAverageConnections, - getAverageDuration, - getMaxConnections, - getMaxDuration, - getMedianConnections, - getMedianDuration, - getMinConnections, - getMinDuration, -} from '../stats/sessionStats'; -import { Controller } from './controller'; - -export const statisticsController = new Controller('/statistics'); - -statisticsController.get({ path: '/sessions/:phase' }, async (req: Request, res) => { - const phase = req.params.phase ? parseInt(req.params.phase) : null; - - res.sendJSON({ - minDuration: await getMinDuration(), // TODO - add phase - maxDuration: await getMaxDuration(), // TODO - add phase - averageDuration: await getAverageDuration(), // TODO - add phase - medianDuration: await getMedianDuration(), // TODO - add phase - minConnections: await getMinConnections(), // TODO - add phase - maxConnections: await getMaxConnections(), // TODO - add phase - averageConnections: await getAverageConnections(), // TODO - add phase - medianConnections: await getMedianConnections(), // TODO - add phase - registeredClassroomsCount: await getRegisteredClassroomsCount(), - connectedClassroomsCount: await getConnectedClassroomsCount(), // TODO - add phase - contributedClassroomsCount: await getContributedClassroomsCount(phase), - }); -}); - -statisticsController.get({ path: '/classrooms' }, async (_req, res) => { - res.sendJSON({ - classrooms: await getClassroomsInfos(), - }); -}); +import type { Request } from 'express'; + +import { + getClassroomsInfos, + getConnectedClassroomsCount, + getContributedClassroomsCount, + getRegisteredClassroomsCount, +} from '../stats/classroomStats'; +import { + getAverageConnections, + getAverageDuration, + getMaxConnections, + getMaxDuration, + getMedianConnections, + getMedianDuration, + getMinConnections, + getMinDuration, +} from '../stats/sessionStats'; +import { getChildrenCodesCount, getFamilyAccountsCount, getConnectedFamiliesCount } from '../stats/villageStats'; +import { Controller } from './controller'; + +export const statisticsController = new Controller('/statistics'); + +statisticsController.get({ path: '/sessions/:phase' }, async (req: Request, res) => { + const phase = req.params.phase ? parseInt(req.params.phase) : null; + + res.sendJSON({ + minDuration: await getMinDuration(), // TODO - add phase + maxDuration: await getMaxDuration(), // TODO - add phase + averageDuration: await getAverageDuration(), // TODO - add phase + medianDuration: await getMedianDuration(), // TODO - add phase + minConnections: await getMinConnections(), // TODO - add phase + maxConnections: await getMaxConnections(), // TODO - add phase + averageConnections: await getAverageConnections(), // TODO - add phase + medianConnections: await getMedianConnections(), // TODO - add phase + registeredClassroomsCount: await getRegisteredClassroomsCount(), + connectedClassroomsCount: await getConnectedClassroomsCount(), // TODO - add phase + contributedClassroomsCount: await getContributedClassroomsCount(phase), + }); +}); + +statisticsController.get({ path: '/classrooms' }, async (_req, res) => { + res.sendJSON({ + classrooms: await getClassroomsInfos(), + }); +}); + +statisticsController.get({ path: '/villages/:villageId' }, async (_req, res) => { + const villageId = parseInt(_req.params.villageId); + res.sendJSON({ + familyAccountsCount: await getFamilyAccountsCount(villageId), + childrenCodesCount: await getChildrenCodesCount(villageId), + connectedFamiliesCount: await getConnectedFamiliesCount(villageId), + }); +}); diff --git a/server/controllers/village.ts b/server/controllers/village.ts index 9361d954d..8c74bdd94 100644 --- a/server/controllers/village.ts +++ b/server/controllers/village.ts @@ -14,8 +14,23 @@ const villageController = new Controller('/villages'); //--- Get all villages --- villageController.get({ path: '', userType: UserType.OBSERVATOR }, async (_req: Request, res: Response) => { - const villages = await AppDataSource.getRepository(Village).find(); - res.sendJSON(villages); + const countryIsoCode = _req.query.countryIsoCode as string; + try { + const villageRepository = AppDataSource.getRepository(Village); + let villages; + if (countryIsoCode) { + const query = villageRepository.createQueryBuilder('village'); + query.where('village.countryCodes LIKE :countryIsoCode', { countryIsoCode: `%${countryIsoCode.toUpperCase()}%` }); + + villages = await query.getMany(); + } else { + villages = await villageRepository.find(); + } + res.sendJSON(villages); + } catch (e) { + console.error(e); + res.status(500).sendJSON({ message: 'An error occurred while fetching villages' }); + } }); //--- Get one village --- diff --git a/server/stats/classroomStats.ts b/server/stats/classroomStats.ts index 99920a794..7eca0bec4 100644 --- a/server/stats/classroomStats.ts +++ b/server/stats/classroomStats.ts @@ -1,8 +1,6 @@ import { UserType } from '../../types/user.type'; import { Activity } from '../entities/activity'; import { Classroom } from '../entities/classroom'; -// import { Comment } from '../entities/comment'; -// import { Video } from '../entities/video'; import { AppDataSource } from '../utils/data-source'; const classroomRepository = AppDataSource.getRepository(Classroom); diff --git a/server/stats/villageStats.ts b/server/stats/villageStats.ts new file mode 100644 index 000000000..76724b303 --- /dev/null +++ b/server/stats/villageStats.ts @@ -0,0 +1,39 @@ +import { Student } from '../entities/student'; +import { User } from '../entities/user'; +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(); + return childrenCodeCount; +}; + +export const getFamilyAccountsCount = async (villageId: number) => { + const familyAccountsCount = await 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(); + return familyAccountsCount; +}; + +export const getConnectedFamiliesCount = async (villageId: number) => { + const connectedFamiliesCount = await studentRepository + .createQueryBuilder('student') + .innerJoin('classroom', 'classroom', 'classroom.id = student.classroomId') + .where('classroom.villageId = :villageId', { villageId }) + .andWhere('student.numLinkedAccount >= 1') + .getCount(); + + return connectedFamiliesCount; +}; diff --git a/src/api/statistics/statistics.get.ts b/src/api/statistics/statistics.get.ts index 1c9d2ae3a..e2c81c6e3 100644 --- a/src/api/statistics/statistics.get.ts +++ b/src/api/statistics/statistics.get.ts @@ -1,32 +1,48 @@ -import { useQuery } from 'react-query'; - -import { axiosRequest } from 'src/utils/axiosRequest'; -import type { ClassroomsStats, SessionsStats } from 'types/statistics.type'; - -async function getSessionsStats(phase: number | null): Promise { - return ( - await axiosRequest({ - method: 'GET', - baseURL: '/api', - url: `/statistics/sessions/${phase}`, - }) - ).data; -} - -export const useGetSessionsStats = (phase: number | null) => { - return useQuery(['sessions-stats'], () => getSessionsStats(phase)); -}; - -async function getClassroomsStats(): Promise { - return ( - await axiosRequest({ - method: 'GET', - baseURL: '/api', - url: '/statistics/classrooms', - }) - ).data; -} - -export const useGetClassroomsStats = () => { - return useQuery(['classrooms-stats'], () => getClassroomsStats()); -}; +import { useQuery } from 'react-query'; + +import { axiosRequest } from 'src/utils/axiosRequest'; +import type { ClassroomsStats, SessionsStats, VillageStats } from 'types/statistics.type'; + +async function getSessionsStats(phase: number | null): Promise { + return ( + await axiosRequest({ + method: 'GET', + baseURL: '/api', + url: `/statistics/sessions/${phase}`, + }) + ).data; +} + +async function getVillagesStats(villageId: number | null): Promise { + return ( + await axiosRequest({ + method: 'GET', + baseURL: '/api', + url: `/statistics/villages/${villageId}`, + }) + ).data; +} + +export const useGetSessionsStats = (phase: number | null) => { + return useQuery(['sessions-stats'], () => getSessionsStats(phase)); +}; + +export const useGetVillagesStats = (villageId: number | null) => { + return useQuery(['villages-stats', villageId], () => getVillagesStats(villageId), { + enabled: villageId !== null, + }); +}; + +async function getClassroomsStats(): Promise { + return ( + await axiosRequest({ + method: 'GET', + baseURL: '/api', + url: '/statistics/classrooms', + }) + ).data; +} + +export const useGetClassroomsStats = () => { + return useQuery(['classrooms-stats'], () => getClassroomsStats()); +}; diff --git a/src/api/villages/villages.get.ts b/src/api/villages/villages.get.ts index 12568153e..544ab8ef9 100644 --- a/src/api/villages/villages.get.ts +++ b/src/api/villages/villages.get.ts @@ -3,16 +3,19 @@ import type { Village } from 'server/entities/village'; import { axiosRequest } from 'src/utils/axiosRequest'; -async function getVillages(): Promise { +async function getVillages(countryIsoCode?: string): Promise { + const url = countryIsoCode ? `/villages?countryIsoCode=${countryIsoCode}` : '/villages'; return ( await axiosRequest({ method: 'GET', baseURL: '/api', - url: '/villages', + url: url, }) ).data; } -export const useGetVillages = () => { - return useQuery(['villages'], getVillages); +export const useGetVillages = (countryIsoCode?: string) => { + return useQuery(['villages', countryIsoCode], () => getVillages(countryIsoCode), { + enabled: !!countryIsoCode || countryIsoCode === undefined, + }); }; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 022989074..b19da43f9 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -115,7 +115,6 @@ export const Header = () => { > {hasAccessToNewAdmin && goToPage('/admin/newportal/create')}>Portail admin} {hasAccessToOldAdmin && goToPage('/admin/villages')}>Admin (old)} - goToPage('/mon-compte')}>Mon compte {user.type !== UserType.FAMILY && goToPage('/mes-videos')}>Mes vidéos} diff --git a/src/components/admin/dashboard-statistics/ClassroomStats.tsx b/src/components/admin/dashboard-statistics/ClassroomStats.tsx index bedc807b9..0932e7138 100644 --- a/src/components/admin/dashboard-statistics/ClassroomStats.tsx +++ b/src/components/admin/dashboard-statistics/ClassroomStats.tsx @@ -7,16 +7,21 @@ import VillageDropdown from './filters/VillageDropdown'; import { mockClassroomsStats } from './mocks/mocks'; import { PelicoCard } from './pelico-card'; import styles from './styles/charts.module.css'; +import { useCountries } from 'src/services/useCountries'; +import { useVillages } from 'src/services/useVillages'; +import type { VillageFilter } from 'types/village.type'; const ClassroomStats = () => { const [selectedCountry, setSelectedCountry] = useState(''); const [selectedVillage, setSelectedVillage] = useState(''); const [selectedClassroom, setSelectedClassroom] = useState(); + const [selectedPhase, setSelectedPhase] = useState('4'); + const [options, setOptions] = useState({ countryIsoCode: '' }); const pelicoMessage = 'Merci de sélectionner une classe pour analyser ses statistiques '; - const countriesMap = mockClassroomsStats.map((country) => country.classroomCountryCode); - const countries = [...new Set(countriesMap)]; + const { countries } = useCountries(); + const { villages } = useVillages(options); const handleCountryChange = (country: string) => { setSelectedCountry(country); @@ -24,8 +29,11 @@ const ClassroomStats = () => { setSelectedClassroom(''); }; - const villagesMap = mockClassroomsStats.filter((village) => village.classroomCountryCode === selectedCountry).map((village) => village.villageName); - const villages = [...new Set(villagesMap)]; + React.useEffect(() => { + setOptions({ + countryIsoCode: selectedCountry, + }); + }, [selectedCountry, selectedPhase]); const handleVillageChange = (village: string) => { setSelectedVillage(village); @@ -41,11 +49,15 @@ const ClassroomStats = () => { setSelectedClassroom(classroom); }; + const handlePhaseChange = (phase: string) => { + setSelectedPhase(phase); + }; + return ( <>
- +
diff --git a/src/components/admin/dashboard-statistics/CountryStats.tsx b/src/components/admin/dashboard-statistics/CountryStats.tsx index 5da52f6d8..883802aeb 100644 --- a/src/components/admin/dashboard-statistics/CountryStats.tsx +++ b/src/components/admin/dashboard-statistics/CountryStats.tsx @@ -1,178 +1,176 @@ -import React, { useState } from 'react'; - -import AccessTimeIcon from '@mui/icons-material/AccessTime'; -import VisibilityIcon from '@mui/icons-material/Visibility'; -import Box from '@mui/material/Box'; - -import AverageStatsCard from './cards/AverageStatsCard/AverageStatsCard'; -import ClassesExchangesCard from './cards/ClassesExchangesCard/ClassesExchangesCard'; -import StatsCard from './cards/StatsCard/StatsCard'; -import BarCharts from './charts/BarCharts'; -import DashboardTable from './charts/DashboardTable'; -import HorizontalBars from './charts/HorizontalChart'; -import PieCharts from './charts/PieCharts'; -import CountriesDropdown from './filters/CountriesDropdown'; -import PhaseDropdown from './filters/PhaseDropdown'; -import PhaseDetails from './menu/PhaseDetails'; -import { mockClassroomsStats, mockConnectionsStats } from './mocks/mocks'; -import { PelicoCard } from './pelico-card'; -import styles from './styles/charts.module.css'; -import { sumContribution } from './utils/sumData'; - -const pieChartData = { - data: [ - { id: 0, value: 10, label: 'series A' }, - { id: 1, value: 15, label: 'series B' }, - { id: 2, value: 20, label: 'series C' }, - ], -}; - -const barChartData = [{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]; -const EngagementBarChartTitle = 'Évolution des connexions'; -const ContributionBarChartTitle = 'Contribution des classes'; - -const CountryStats = () => { - const [selectedCountry, setSelectedCountry] = useState(''); - const pelicoMessage = 'Merci de sélectionner un pays pour analyser ses statistiques '; - - const countriesMap = mockClassroomsStats.map((country) => country.classroomCountryCode); - const countries = [...new Set(countriesMap)]; // avoid duplicates - const handleCountryChange = (country: string) => { - setSelectedCountry(country); - }; - - const filteredVillage = mockClassroomsStats.filter((village) => village.classroomCountryCode === selectedCountry); - const countryData = sumContribution[selectedCountry]; - - const { totalActivities = 0, totalComments = 0, totalVideos = 0 } = countryData || {}; - - const classStats = mockConnectionsStats.map((classroom) => ({ - registered: classroom.registeredClassroomsCount, - connected: classroom.connectedClassroomsCount, - contributed: classroom.contributedClassroomsCount, - })); - - const connectStats = mockConnectionsStats.map((connect) => ({ - averageConnection: connect.averageConnections, - averageDuration: connect.averageDuration, - minDuration: connect.minDuration, - maxDuration: connect.maxDuration, - medianDuration: connect.medianDuration, - minConnections: connect.minConnections, - maxConnections: connect.maxConnections, - medianConnections: connect.medianConnections, - })); - - return ( - <> -
-
- -
-
- -
-
-

Statut: Observateur

- {!selectedCountry ? ( - - ) : ( - <> -
- -
- - Ce pays participe dans les villages-monde suivants : -
    - {filteredVillage.map((village, index) => ( -
  • {village.villageName}
  • - ))} -
-
-
- -
-
- 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 - -
-
- - -
-
- - -
-
- -
-
- -
-
- -
- - )} - - ); -}; - -export default CountryStats; +import React, { useState } from 'react'; + +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import Box from '@mui/material/Box'; + +import AverageStatsCard from './cards/AverageStatsCard/AverageStatsCard'; +import ClassesExchangesCard from './cards/ClassesExchangesCard/ClassesExchangesCard'; +import StatsCard from './cards/StatsCard/StatsCard'; +import BarCharts from './charts/BarCharts'; +import DashboardTable from './charts/DashboardTable'; +import HorizontalBars from './charts/HorizontalChart'; +import PieCharts from './charts/PieCharts'; +import CountriesDropdown from './filters/CountriesDropdown'; +import PhaseDetails from './menu/PhaseDetails'; +import { mockClassroomsStats, mockConnectionsStats } from './mocks/mocks'; +import { PelicoCard } from './pelico-card'; +import styles from './styles/charts.module.css'; +import { sumContribution } from './utils/sumData'; +import { useCountries } from 'src/services/useCountries'; + +const pieChartData = { + data: [ + { id: 0, value: 10, label: 'series A' }, + { id: 1, value: 15, label: 'series B' }, + { id: 2, value: 20, label: 'series C' }, + ], +}; + +const barChartData = [{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]; +const EngagementBarChartTitle = 'Évolution des connexions'; +const ContributionBarChartTitle = 'Contribution des classes'; + +const CountryStats = () => { + const [selectedCountry, setSelectedCountry] = useState(''); + const pelicoMessage = 'Merci de sélectionner un pays pour analyser ses statistiques '; + const { countries } = useCountries(); + const handleCountryChange = (country: string) => { + setSelectedCountry(country); + }; + + const filteredVillage = mockClassroomsStats.filter((village) => village.classroomCountryCode === selectedCountry); + const countryData = sumContribution[selectedCountry]; + + const { totalActivities = 0, totalComments = 0, totalVideos = 0 } = countryData || {}; + + const classStats = mockConnectionsStats.map((classroom) => ({ + registered: classroom.registeredClassroomsCount, + connected: classroom.connectedClassroomsCount, + contributed: classroom.contributedClassroomsCount, + })); + + const connectStats = mockConnectionsStats.map((connect) => ({ + averageConnection: connect.averageConnections, + averageDuration: connect.averageDuration, + minDuration: connect.minDuration, + maxDuration: connect.maxDuration, + medianDuration: connect.medianDuration, + minConnections: connect.minConnections, + maxConnections: connect.maxConnections, + medianConnections: connect.medianConnections, + })); + + return ( + <> +
+ {/*
+ +
*/} +
+ +
+
+

Statut: Observateur

+ {!selectedCountry ? ( + + ) : ( + <> +
+ +
+ + Ce pays participe dans les villages-monde suivants : +
    + {filteredVillage.map((village, index) => ( +
  • {village.villageName}
  • + ))} +
+
+
+ +
+
+ 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 + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+ + )} + + ); +}; + +export default CountryStats; diff --git a/src/components/admin/dashboard-statistics/DashboardStatsNav.tsx b/src/components/admin/dashboard-statistics/DashboardStatsNav.tsx index fea75d4e6..8ddb30ece 100644 --- a/src/components/admin/dashboard-statistics/DashboardStatsNav.tsx +++ b/src/components/admin/dashboard-statistics/DashboardStatsNav.tsx @@ -23,7 +23,7 @@ function CustomTabPanel(props: TabPanelProps) { return (