Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vil 83 #992

Merged
merged 13 commits into from
Oct 7, 2024
Merged
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
Loading