Skip to content

Commit

Permalink
Merge pull request #999 from parlemonde/VIL-611
Browse files Browse the repository at this point in the history
Vil 611
  • Loading branch information
Benjyhy authored Nov 5, 2024
2 parents 96ef588 + 6ece7a3 commit 84c40f9
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 151 deletions.
10 changes: 10 additions & 0 deletions server/controllers/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
81 changes: 41 additions & 40 deletions server/stats/villageStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
13 changes: 13 additions & 0 deletions src/api/statistics/statistics.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ async function getSessionsStats(phase: number | null): Promise<SessionsStats> {
).data;
}

async function getOneVillageStats(): Promise<VillageStats> {
return (
await axiosRequest({
method: 'GET',
baseURL: '/api',
url: `/statistics/onevillage`,
})
).data;
}

async function getVillagesStats(villageId: number | null): Promise<VillageStats> {
return (
await axiosRequest({
Expand All @@ -26,6 +36,9 @@ async function getVillagesStats(villageId: number | null): Promise<VillageStats>
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), {
Expand Down
166 changes: 113 additions & 53 deletions src/components/admin/dashboard-statistics/GlobalStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,102 @@ 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 } 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 { useGetSessionsStats } from 'src/api/statistics/statistics.get';
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 type { OneVillageTableRow } from 'types/statistics.type';

const GlobalStats = () => {
const [value, setValue] = React.useState(0);
const sessionsStats = useGetSessionsStats(null);
const oneVillageStats = useGetOneVillageStats();
const [familiesWithoutAccountRows, setFamiliesWithoutAccountRows] = React.useState<Array<OneVillageTableRow>>([]);
const [floatingAccountsRows, setFloatingAccountsRows] = React.useState<Array<OneVillageTableRow>>([]);
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 <p>Error!</p>;
if (sessionsStats.isLoading || sessionsStats.isIdle) return <p>Loading...</p>;

// eslint-disable-next-line no-console
console.log('Sessions stats', sessionsStats.data);
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};

return (
<>
<TeamComments />
<DashboardWorldMap />
<Grid container spacing={4} direction={{ xs: 'column', md: 'row' }}>
<Grid item xs={12} lg={4}>
<StatsCard data={10}>
Nombre de classes <br />
inscrites
</StatsCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatsCard data={10}>
Nombre de classes <br /> connectées
</StatsCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatsCard data={10}>
Nombre de classes <br /> contributrices
</StatsCard>
</Grid>
<Grid item xs={12} lg={6}>
<AverageStatsCard
data={{
min: Math.floor(sessionsStats.data.minDuration / 60),
max: Math.floor(sessionsStats.data.maxDuration / 60),
average: Math.floor(sessionsStats.data.averageDuration / 60),
median: Math.floor(sessionsStats.data.medianDuration / 60),
}}
unit="min"
icon={<AccessTimeIcon sx={{ fontSize: 'inherit' }} />}
>
Temps de connexion moyen par classe
</AverageStatsCard>
</Grid>
<Grid item xs={12} lg={6}>
<AverageStatsCard
data={{
min: sessionsStats.data.minConnections,
max: sessionsStats.data.maxConnections,
average: sessionsStats.data.averageConnections,
median: sessionsStats.data.medianConnections,
}}
icon={<VisibilityIcon sx={{ fontSize: 'inherit' }} />}
>
Nombre de connexions moyen par classe
</AverageStatsCard>
</Grid>
<Grid item xs={12} lg={6}>
<ClassesExchangesCard totalPublications={100} totalComments={100} totalVideos={100} />
</Grid>
{/* <div>
<Tabs value={value} onChange={handleTabChange} aria-label="basic tabs example" sx={{ py: 3 }}>
<Tab label="En classe" />
<Tab label="En famille" />
</Tabs>
<TabPanel value={value} index={0}>
<Grid container spacing={4} direction={{ xs: 'column', md: 'row' }}>
<Grid item xs={12} lg={4}>
<StatsCard data={10}>
Nombre de classes <br />
inscrites
</StatsCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatsCard data={10}>
Nombre de classes <br /> connectées
</StatsCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatsCard data={10}>
Nombre de classes <br /> contributrices
</StatsCard>
</Grid>
<Grid item xs={12} lg={6}>
<AverageStatsCard
data={{
min: Math.floor(sessionsStats.data.minDuration / 60),
max: Math.floor(sessionsStats.data.maxDuration / 60),
average: Math.floor(sessionsStats.data.averageDuration / 60),
median: Math.floor(sessionsStats.data.medianDuration / 60),
}}
unit="min"
icon={<AccessTimeIcon sx={{ fontSize: 'inherit' }} />}
>
Temps de connexion moyen par classe
</AverageStatsCard>
</Grid>
<Grid item xs={12} lg={6}>
<AverageStatsCard
data={{
min: sessionsStats.data.minConnections,
max: sessionsStats.data.maxConnections,
average: sessionsStats.data.averageConnections,
median: sessionsStats.data.medianConnections,
}}
icon={<VisibilityIcon sx={{ fontSize: 'inherit' }} />}
>
Nombre de connexions moyen par classe
</AverageStatsCard>
</Grid>
<Grid item xs={12} lg={6}>
<ClassesExchangesCard totalPublications={100} totalComments={100} totalVideos={100} />
</Grid>
{/* <div>
<PhaseDetails
phase={1}
data={[
Expand All @@ -98,7 +124,41 @@ const GlobalStats = () => {
]}
/>
</div> */}
</Grid>
</Grid>
</TabPanel>
<TabPanel value={value} index={1}>
<>
<OneVillageTable
admin={false}
emptyPlaceholder={<p>{'Pas de données'}</p>}
data={familiesWithoutAccountRows}
columns={FamiliesWithoutAccountHeaders}
titleContent={`À surveiller : comptes non créés (${familiesWithoutAccountRows.length})`}
/>
<OneVillageTable
admin={false}
emptyPlaceholder={<p>{'Pas de données'}</p>}
data={floatingAccountsRows}
columns={FloatingAccountsHeaders}
titleContent={`À surveiller : comptes flottants (${floatingAccountsRows.length})`}
/>
<Box
className={styles.classroomStats}
sx={{
display: 'flex',
flexDirection: {
xs: 'column',
md: 'row',
},
gap: 2,
}}
>
<StatsCard data={oneVillageStats.data?.familyAccountsCount}>Nombre de profs ayant créé des comptes famille</StatsCard>
<StatsCard data={oneVillageStats.data?.childrenCodesCount}>Nombre de codes enfant créés</StatsCard>
<StatsCard data={oneVillageStats.data?.connectedFamiliesCount}>Nombre de familles connectées</StatsCard>
</Box>
</>
</TabPanel>
</>
);
};
Expand Down
23 changes: 23 additions & 0 deletions src/components/admin/dashboard-statistics/TabPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div role="tabpanel" hidden={value !== index} id={`tabpanel-${index}`} aria-labelledby={`tab-${index}`} {...other}>
{value === index && (
<Box sx={{ p: 0 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
};

export default TabPanel;
Loading

0 comments on commit 84c40f9

Please sign in to comment.