Skip to content

Commit

Permalink
Merge pull request #992 from parlemonde/VIL-83
Browse files Browse the repository at this point in the history
Vil 83
  • Loading branch information
guillaume-pages authored Oct 7, 2024
2 parents 93c384e + 84f33e9 commit 9dc4105
Show file tree
Hide file tree
Showing 29 changed files with 777 additions and 518 deletions.
Empty file modified .pnp.cjs
100755 → 100644
Empty file.
Empty file modified .yarn/sdks/eslint/bin/eslint.js
100755 → 100644
Empty file.
Empty file modified .yarn/sdks/prettier/bin-prettier.js
100755 → 100644
Empty file.
Empty file modified .yarn/sdks/typescript/bin/tsc
100755 → 100644
Empty file.
Empty file modified .yarn/sdks/typescript/bin/tsserver
100755 → 100644
Empty file.
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3.7'
services:
backend:
build:
Expand Down
100 changes: 55 additions & 45 deletions server/controllers/statistics.ts
Original file line number Diff line number Diff line change
@@ -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),
});
});
19 changes: 17 additions & 2 deletions server/controllers/village.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand Down
2 changes: 0 additions & 2 deletions server/stats/classroomStats.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
39 changes: 39 additions & 0 deletions server/stats/villageStats.ts
Original file line number Diff line number Diff line change
@@ -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;
};
80 changes: 48 additions & 32 deletions src/api/statistics/statistics.get.ts
Original file line number Diff line number Diff line change
@@ -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<SessionsStats> {
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<ClassroomsStats[]> {
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<SessionsStats> {
return (
await axiosRequest({
method: 'GET',
baseURL: '/api',
url: `/statistics/sessions/${phase}`,
})
).data;
}

async function getVillagesStats(villageId: number | null): Promise<VillageStats> {
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<ClassroomsStats[]> {
return (
await axiosRequest({
method: 'GET',
baseURL: '/api',
url: '/statistics/classrooms',
})
).data;
}

export const useGetClassroomsStats = () => {
return useQuery(['classrooms-stats'], () => getClassroomsStats());
};
11 changes: 7 additions & 4 deletions src/api/villages/villages.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import type { Village } from 'server/entities/village';

import { axiosRequest } from 'src/utils/axiosRequest';

async function getVillages(): Promise<Village[]> {
async function getVillages(countryIsoCode?: string): Promise<Village[]> {
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,
});
};
1 change: 0 additions & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ export const Header = () => {
>
{hasAccessToNewAdmin && <MenuItem onClick={() => goToPage('/admin/newportal/create')}>Portail admin</MenuItem>}
{hasAccessToOldAdmin && <MenuItem onClick={() => goToPage('/admin/villages')}>Admin (old)</MenuItem>}

<MenuItem onClick={() => goToPage('/mon-compte')}>Mon compte</MenuItem>
{user.type !== UserType.FAMILY && <MenuItem onClick={() => goToPage('/mes-videos')}>Mes vidéos</MenuItem>}
<AccessControl featureName="id-family" key={user?.id || 'default'}>
Expand Down
22 changes: 17 additions & 5 deletions src/components/admin/dashboard-statistics/ClassroomStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,33 @@ 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<string>('');
const [selectedVillage, setSelectedVillage] = useState<string>('');
const [selectedClassroom, setSelectedClassroom] = useState<string>();
const [selectedPhase, setSelectedPhase] = useState<string>('4');
const [options, setOptions] = useState<VillageFilter>({ 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);
setSelectedVillage('');
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);
Expand All @@ -41,11 +49,15 @@ const ClassroomStats = () => {
setSelectedClassroom(classroom);
};

const handlePhaseChange = (phase: string) => {
setSelectedPhase(phase);
};

return (
<>
<div className={styles.filtersContainer}>
<div className={styles.phaseFilter}>
<PhaseDropdown />
<PhaseDropdown onPhaseChange={handlePhaseChange} />
</div>
<div className={styles.countryFilter}>
<CountriesDropdown countries={countries} onCountryChange={handleCountryChange} />
Expand Down
Loading

0 comments on commit 9dc4105

Please sign in to comment.