From aa721c32f2a238f7c7132f4dbd9a753143a2e47e Mon Sep 17 00:00:00 2001
From: Benjamin Ramet
Date: Tue, 26 Nov 2024 10:39:06 +0100
Subject: [PATCH 1/5] Add classroom tab in statistics dashboard
---
server/controllers/classroom.ts | 21 +++++
.../dashboard-statistics/ClassroomStats.tsx | 86 ++++++++++++++++---
.../filters/ClassroomDropdown.tsx | 8 +-
src/services/useClassrooms.ts | 50 +++++++++++
types/classroom.type.ts | 4 +
5 files changed, 153 insertions(+), 16 deletions(-)
create mode 100644 src/services/useClassrooms.ts
diff --git a/server/controllers/classroom.ts b/server/controllers/classroom.ts
index e78a00193..403efb1de 100644
--- a/server/controllers/classroom.ts
+++ b/server/controllers/classroom.ts
@@ -19,6 +19,27 @@ export const classroomController = new Controller('/classrooms');
* @return {object} Route API JSON response
*/
+classroomController.get({ path: '', userType: UserType.ADMIN }, async (_req: Request, res: Response) => {
+ const villageId = _req.query.villageId as string;
+ try {
+ const classroomRepository = AppDataSource.getRepository(Classroom);
+ let classrooms;
+ if (villageId) {
+ const query = classroomRepository.createQueryBuilder('classroom');
+ query.where('classroom.villageId = :villageId', { villageId });
+
+ classrooms = await query.getMany();
+ //classrooms = await classroomRepository.find({ where: { villageId: +villageId } });
+ } else {
+ classrooms = await classroomRepository.find();
+ }
+ res.sendJSON(classrooms);
+ } catch (e) {
+ console.error(e);
+ res.status(500).sendJSON({ message: 'An error occurred while fetching classrooms' });
+ }
+});
+
classroomController.get({ path: '/:id', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => {
const id = parseInt(req.params.id, 10) || 0;
const classroom = await AppDataSource.getRepository(Classroom).findOne({
diff --git a/src/components/admin/dashboard-statistics/ClassroomStats.tsx b/src/components/admin/dashboard-statistics/ClassroomStats.tsx
index 0932e7138..9fede9382 100644
--- a/src/components/admin/dashboard-statistics/ClassroomStats.tsx
+++ b/src/components/admin/dashboard-statistics/ClassroomStats.tsx
@@ -1,14 +1,24 @@
import React, { useState } from 'react';
+import { Box, Tab, Tabs } from '@mui/material';
+
+import { OneVillageTable } from '../OneVillageTable';
+import TabPanel from './TabPanel';
+import StatsCard from './cards/StatsCard/StatsCard';
import ClassroomDropdown from './filters/ClassroomDropdown';
import CountriesDropdown from './filters/CountriesDropdown';
import PhaseDropdown from './filters/PhaseDropdown';
import VillageDropdown from './filters/VillageDropdown';
-import { mockClassroomsStats } from './mocks/mocks';
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 { useClassrooms } from 'src/services/useClassrooms';
import { useCountries } from 'src/services/useCountries';
import { useVillages } from 'src/services/useVillages';
+import type { ClassroomFilter } from 'types/classroom.type';
+import type { OneVillageTableRow } from 'types/statistics.type';
import type { VillageFilter } from 'types/village.type';
const ClassroomStats = () => {
@@ -16,12 +26,16 @@ const ClassroomStats = () => {
const [selectedVillage, setSelectedVillage] = useState('');
const [selectedClassroom, setSelectedClassroom] = useState();
const [selectedPhase, setSelectedPhase] = useState('4');
- const [options, setOptions] = useState({ countryIsoCode: '' });
+ const [villageFilter, setVillageFilter] = useState({ countryIsoCode: '' });
+ const [classroomFilter, setClassroomFilter] = useState({ villageId: '' });
+ const [value, setValue] = React.useState(0);
const pelicoMessage = 'Merci de sélectionner une classe pour analyser ses statistiques ';
const { countries } = useCountries();
- const { villages } = useVillages(options);
+ const { villages } = useVillages(villageFilter);
+ const villagesStats = useGetVillagesStats(+selectedVillage, +selectedPhase);
+ const { classrooms } = useClassrooms(classroomFilter);
const handleCountryChange = (country: string) => {
setSelectedCountry(country);
@@ -30,21 +44,26 @@ const ClassroomStats = () => {
};
React.useEffect(() => {
- setOptions({
+ setVillageFilter({
countryIsoCode: selectedCountry,
});
- }, [selectedCountry, selectedPhase]);
+ setClassroomFilter({
+ villageId: selectedVillage,
+ });
+ }, [selectedCountry, selectedPhase, selectedVillage]);
+
+ const [familiesWithoutAccountRows, setFamiliesWithoutAccountRows] = React.useState>([]);
+ React.useEffect(() => {
+ if (villagesStats.data?.familiesWithoutAccount) {
+ setFamiliesWithoutAccountRows(createFamiliesWithoutAccountRows(villagesStats.data?.familiesWithoutAccount));
+ }
+ }, [villagesStats.data?.familiesWithoutAccount]);
const handleVillageChange = (village: string) => {
setSelectedVillage(village);
setSelectedClassroom('');
};
- const classroomsMap = mockClassroomsStats
- .filter((classroom) => classroom.villageName === selectedVillage)
- .map((classroom) => classroom.classroomId);
- const classrooms = [...new Set(classroomsMap)];
-
const handleClassroomChange = (classroom: string) => {
setSelectedClassroom(classroom);
};
@@ -53,9 +72,15 @@ const ClassroomStats = () => {
setSelectedPhase(phase);
};
+ const noDataFoundMessage = 'Pas de données pour le Village-Monde sélectionné';
+
+ const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
+ setValue(newValue);
+ };
+
return (
<>
-
+
@@ -68,8 +93,43 @@ const ClassroomStats = () => {
-
- {!selectedClassroom ? : null}
+
+
+
+
+
+
+ Statistiques - En classe
+
+
+ {!selectedClassroom ? (
+
+ ) : (
+ <>
+ {noDataFoundMessage}
}
+ data={familiesWithoutAccountRows}
+ columns={FamiliesWithoutAccountHeaders}
+ titleContent={`À surveiller : comptes non créés (${familiesWithoutAccountRows.length})`}
+ />
+
+ Nombre de codes enfant créés
+ Nombre de familles connectées
+
+ >
+ )}
+
>
);
};
diff --git a/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx b/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx
index a641ccdc4..60196d481 100644
--- a/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx
+++ b/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx
@@ -7,8 +7,10 @@ import MenuItem from '@mui/material/MenuItem';
import type { SelectChangeEvent } from '@mui/material/Select';
import Select from '@mui/material/Select';
+import type { Classroom } from 'types/classroom.type';
+
interface ClassroomDropdownProps {
- classrooms: number[];
+ classrooms: Classroom[];
onClassroomChange: (classrooms: string) => void;
}
@@ -27,8 +29,8 @@ export default function ClassroomDropdown({ classrooms, onClassroomChange }: Cla
diff --git a/src/services/useClassrooms.ts b/src/services/useClassrooms.ts
new file mode 100644
index 000000000..6f517015e
--- /dev/null
+++ b/src/services/useClassrooms.ts
@@ -0,0 +1,50 @@
+import React from 'react';
+import type { QueryFunction } from 'react-query';
+import { useQueryClient, useQuery } from 'react-query';
+
+import { axiosRequest } from 'src/utils/axiosRequest';
+import type { Classroom, ClassroomFilter } from 'types/classroom.type';
+
+export const useClassrooms = (options?: ClassroomFilter): { classrooms: Classroom[]; setClassrooms(newClassrooms: Classroom[]): void } => {
+ const queryClient = useQueryClient();
+ const buildUrl = () => {
+ if (!options) return '/classrooms';
+ const { villageId } = options;
+ const queryParams = [];
+
+ if (villageId) queryParams.push(`villageId=${villageId}`);
+
+ return queryParams.length ? `/classrooms?${queryParams.join('&')}` : '/classrooms';
+ };
+
+ const url = buildUrl();
+ // The query function for fetching villages, memoized with useCallback
+ const getClassrooms: QueryFunction = React.useCallback(async () => {
+ const response = await axiosRequest({
+ method: 'GET',
+ url,
+ });
+ if (response.error) {
+ return [];
+ }
+ return response.data;
+ }, [url]);
+
+ // Notice that we include `countryIsoCode` in the query key so that the query is refetched
+ const { data, isLoading, error } = useQuery(
+ ['classrooms', options], // Include countryIsoCode in the query key to trigger refetching
+ getClassrooms,
+ );
+
+ const setClassrooms = React.useCallback(
+ (newClassrooms: Classroom[]) => {
+ queryClient.setQueryData(['classrooms', options], newClassrooms); // Ensure that the query key includes countryIsoCode
+ },
+ [queryClient, options],
+ );
+
+ return {
+ classrooms: isLoading || error ? [] : data || [],
+ setClassrooms,
+ };
+};
diff --git a/types/classroom.type.ts b/types/classroom.type.ts
index 4c8e2f2cf..020a093be 100644
--- a/types/classroom.type.ts
+++ b/types/classroom.type.ts
@@ -36,3 +36,7 @@ export interface InitialStateOptionsProps {
ownClass: StateOptions;
ownClassTimeDelay: StateOptions;
}
+
+export interface ClassroomFilter {
+ villageId: string;
+}
From 04a9d54b13ee0161a099947e7747dd4928222708 Mon Sep 17 00:00:00 2001
From: Benjamin Ramet
Date: Wed, 27 Nov 2024 11:10:31 +0100
Subject: [PATCH 2/5] Create queries for classroom statistics in admin
dashboard
---
server/controllers/statistics.ts | 13 ++++
server/stats/classroomStats.ts | 78 +++++++++++++++++++
server/stats/villageStats.ts | 5 +-
src/api/statistics/statistics.get.ts | 12 +--
.../dashboard-statistics/ClassroomStats.tsx | 16 ++--
types/statistics.type.ts | 2 +
6 files changed, 110 insertions(+), 16 deletions(-)
diff --git a/server/controllers/statistics.ts b/server/controllers/statistics.ts
index 500a2b748..3530aafcd 100644
--- a/server/controllers/statistics.ts
+++ b/server/controllers/statistics.ts
@@ -5,6 +5,9 @@ import {
getConnectedClassroomsCount,
getContributedClassroomsCount,
getRegisteredClassroomsCount,
+ getChildrenCodesCount as getChildrenCodesCountForClassroom,
+ getConnectedFamiliesCount as getConnectedFamiliesCountForClassroom,
+ getFamiliesWithoutAccount as getFamiliesWithoutAccountForClassroom,
} from '../stats/classroomStats';
import {
getAverageConnections,
@@ -72,3 +75,13 @@ statisticsController.get({ path: '/villages/:villageId' }, async (_req, res) =>
floatingAccounts: await getFloatingAccounts(villageId),
});
});
+
+statisticsController.get({ path: '/classrooms/:classroomId' }, async (_req, res) => {
+ const classroomId = parseInt(_req.params.classroomId);
+ const phase = _req.query.phase as unknown as number;
+ res.sendJSON({
+ childrenCodesCount: await getChildrenCodesCountForClassroom(classroomId, phase),
+ connectedFamiliesCount: await getConnectedFamiliesCountForClassroom(classroomId, phase),
+ familiesWithoutAccount: await getFamiliesWithoutAccountForClassroom(classroomId),
+ });
+});
diff --git a/server/stats/classroomStats.ts b/server/stats/classroomStats.ts
index 7eca0bec4..67c11e0a6 100644
--- a/server/stats/classroomStats.ts
+++ b/server/stats/classroomStats.ts
@@ -1,9 +1,14 @@
import { UserType } from '../../types/user.type';
import { Activity } from '../entities/activity';
import { Classroom } from '../entities/classroom';
+import { Student } from '../entities/student';
+import { Village } from '../entities/village';
import { AppDataSource } from '../utils/data-source';
+import { getPhasePeriod, phaseWasSelected } from './villageStats';
const classroomRepository = AppDataSource.getRepository(Classroom);
+const studentRepository = AppDataSource.getRepository(Student);
+const villageRepository = AppDataSource.getRepository(Village);
const teacherType = UserType.TEACHER;
@@ -86,3 +91,76 @@ export const getContributedClassroomsCount = async (phase: number | null) => {
// return parseInt(result.contributedUsersCount);
return 10;
};
+
+export const getChildrenCodesCount = async (classroomId?: number, phase?: number) => {
+ const classroom = await classroomRepository
+ .createQueryBuilder('classroom')
+ .innerJoin('classroom.village', 'village')
+ .where('classroom.id = :classroomId', { classroomId })
+ .getOne();
+ const villageId = classroom?.villageId;
+ const query = studentRepository.createQueryBuilder('student').innerJoin('student.classroom', 'classroom').innerJoin('classroom.village', 'village');
+ if (classroomId) {
+ query.andWhere('classroom.id = :classroomId', { classroomId });
+ if (phaseWasSelected(phase) && villageId) {
+ const village = await villageRepository.findOne({ where: { id: villageId } });
+ const phaseValue = phase as number;
+ const { debut, end } = await getPhasePeriod(villageId, phaseValue);
+ query.andWhere('student.createdAt >= :debut', { debut });
+ if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
+ }
+ }
+ const childrenCodeCount = await query.getCount();
+ return childrenCodeCount;
+};
+
+export const getConnectedFamiliesCount = async (classroomId?: number, phase?: number) => {
+ const classroom = await classroomRepository
+ .createQueryBuilder('classroom')
+ .innerJoin('classroom.village', 'village')
+ .where('classroom.id = :classroomId', { classroomId })
+ .getOne();
+ const villageId = classroom?.villageId;
+ const village = await villageRepository.findOne({ where: { id: villageId } });
+ const query = studentRepository
+ .createQueryBuilder('student')
+ .innerJoin('classroom', 'classroom', 'classroom.id = student.classroomId')
+ .andWhere('classroom.id = :classroomId', { classroomId })
+ .andWhere('student.numLinkedAccount >= 1');
+ if (villageId) {
+ query.andWhere('classroom.villageId = :villageId', { villageId });
+ if (phaseWasSelected(phase)) {
+ const phaseValue = phase as number;
+ const { debut, end } = await getPhasePeriod(villageId, phaseValue);
+ query.andWhere('student.createdAt >= :debut', { debut });
+ if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
+ }
+ }
+
+ const connectedFamiliesCount = await query.getCount();
+
+ return connectedFamiliesCount;
+};
+
+export const getFamiliesWithoutAccount = async (classroomId?: number) => {
+ const query = studentRepository
+ .createQueryBuilder('student')
+ .innerJoin('student.classroom', 'classroom')
+ .innerJoin('classroom.user', 'user')
+ .innerJoin('user.village', 'village')
+ .where('student.numLinkedAccount < 1');
+ if (classroomId) query.andWhere('classroom.id = :classroomId', { classroomId });
+
+ 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;
+};
diff --git a/server/stats/villageStats.ts b/server/stats/villageStats.ts
index 4a41b73d9..1ed202ab3 100644
--- a/server/stats/villageStats.ts
+++ b/server/stats/villageStats.ts
@@ -9,7 +9,7 @@ const studentRepository = AppDataSource.getRepository(Student);
const phaseHistoryRepository = AppDataSource.getRepository(PhaseHistory);
const villageRepository = AppDataSource.getRepository(Village);
-const getPhasePeriod = async (villageId: number, phase: number): Promise<{ debut: Date | undefined; end: Date | undefined }> => {
+export const getPhasePeriod = async (villageId: number, phase: number): Promise<{ debut: Date | undefined; end: Date | undefined }> => {
// Getting the debut and end dates for the given phase
const query = phaseHistoryRepository
.createQueryBuilder('phaseHistory')
@@ -26,10 +26,9 @@ const getPhasePeriod = async (villageId: number, phase: number): Promise<{ debut
};
};
-const phaseWasSelected = (phase: number | undefined): boolean => {
+export const phaseWasSelected = (phase: number | undefined): boolean => {
return phase !== undefined && Object.values(VillagePhase).includes(+phase);
};
-
export const getChildrenCodesCount = async (villageId?: number, phase?: number) => {
const query = studentRepository.createQueryBuilder('student').innerJoin('student.classroom', 'classroom').innerJoin('classroom.village', 'village');
const village = await villageRepository.findOne({ where: { id: villageId } });
diff --git a/src/api/statistics/statistics.get.ts b/src/api/statistics/statistics.get.ts
index 037934e8b..2dc640028 100644
--- a/src/api/statistics/statistics.get.ts
+++ b/src/api/statistics/statistics.get.ts
@@ -1,7 +1,7 @@
import { useQuery } from 'react-query';
import { axiosRequest } from 'src/utils/axiosRequest';
-import type { ClassroomsStats, SessionsStats, VillageStats } from 'types/statistics.type';
+import type { ClassroomStats, SessionsStats, VillageStats } from 'types/statistics.type';
async function getSessionsStats(phase: number | null): Promise {
return (
@@ -46,16 +46,18 @@ export const useGetVillagesStats = (villageId: number | null, phase: number) =>
});
};
-async function getClassroomsStats(): Promise {
+async function getClassroomsStats(classroomId: number | null, phase: number): Promise {
return (
await axiosRequest({
method: 'GET',
baseURL: '/api',
- url: '/statistics/classrooms',
+ url: phase ? `/statistics/classrooms/${classroomId}?phase=${phase}` : `/statistics/classrooms/${classroomId}`,
})
).data;
}
-export const useGetClassroomsStats = () => {
- return useQuery(['classrooms-stats'], () => getClassroomsStats());
+export const useGetClassroomsStats = (classroomId: number | null, phase: number) => {
+ return useQuery(['classrooms-stats', classroomId, phase], () => getClassroomsStats(classroomId, phase), {
+ enabled: classroomId !== null,
+ });
};
diff --git a/src/components/admin/dashboard-statistics/ClassroomStats.tsx b/src/components/admin/dashboard-statistics/ClassroomStats.tsx
index 9fede9382..42fda598a 100644
--- a/src/components/admin/dashboard-statistics/ClassroomStats.tsx
+++ b/src/components/admin/dashboard-statistics/ClassroomStats.tsx
@@ -13,7 +13,7 @@ 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 { useGetClassroomsStats } from 'src/api/statistics/statistics.get';
import { useClassrooms } from 'src/services/useClassrooms';
import { useCountries } from 'src/services/useCountries';
import { useVillages } from 'src/services/useVillages';
@@ -24,7 +24,7 @@ import type { VillageFilter } from 'types/village.type';
const ClassroomStats = () => {
const [selectedCountry, setSelectedCountry] = useState('');
const [selectedVillage, setSelectedVillage] = useState('');
- const [selectedClassroom, setSelectedClassroom] = useState();
+ const [selectedClassroom, setSelectedClassroom] = useState('');
const [selectedPhase, setSelectedPhase] = useState('4');
const [villageFilter, setVillageFilter] = useState({ countryIsoCode: '' });
const [classroomFilter, setClassroomFilter] = useState({ villageId: '' });
@@ -34,7 +34,7 @@ const ClassroomStats = () => {
const { countries } = useCountries();
const { villages } = useVillages(villageFilter);
- const villagesStats = useGetVillagesStats(+selectedVillage, +selectedPhase);
+ const classroomsStats = useGetClassroomsStats(+selectedClassroom, +selectedPhase);
const { classrooms } = useClassrooms(classroomFilter);
const handleCountryChange = (country: string) => {
@@ -54,10 +54,10 @@ const ClassroomStats = () => {
const [familiesWithoutAccountRows, setFamiliesWithoutAccountRows] = React.useState>([]);
React.useEffect(() => {
- if (villagesStats.data?.familiesWithoutAccount) {
- setFamiliesWithoutAccountRows(createFamiliesWithoutAccountRows(villagesStats.data?.familiesWithoutAccount));
+ if (classroomsStats.data?.familiesWithoutAccount) {
+ setFamiliesWithoutAccountRows(createFamiliesWithoutAccountRows(classroomsStats.data?.familiesWithoutAccount));
}
- }, [villagesStats.data?.familiesWithoutAccount]);
+ }, [classroomsStats.data?.familiesWithoutAccount]);
const handleVillageChange = (village: string) => {
setSelectedVillage(village);
@@ -124,8 +124,8 @@ const ClassroomStats = () => {
gap: 2,
}}
>
- Nombre de codes enfant créés
- Nombre de familles connectées
+ Nombre de codes enfant créés
+ Nombre de familles connectées
>
)}
diff --git a/types/statistics.type.ts b/types/statistics.type.ts
index 51a142ad8..cc74dec8a 100644
--- a/types/statistics.type.ts
+++ b/types/statistics.type.ts
@@ -34,6 +34,8 @@ export interface VillageStats {
floatingAccounts: FloatingAccount[];
}
+export type ClassroomStats = Omit;
+
export interface FamiliesWithoutAccount {
student_id: number;
student_firstname: string;
From e8207c7d5f734fb1321e33ac46796c602dba634e Mon Sep 17 00:00:00 2001
From: Benjamin Ramet
Date: Fri, 29 Nov 2024 08:47:49 +0100
Subject: [PATCH 3/5] Add display name in classroom dropdown
---
server/controllers/classroom.ts | 3 +--
.../filters/ClassroomDropdown.tsx | 27 ++++++++++---------
2 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/server/controllers/classroom.ts b/server/controllers/classroom.ts
index 403efb1de..76c904aaa 100644
--- a/server/controllers/classroom.ts
+++ b/server/controllers/classroom.ts
@@ -25,11 +25,10 @@ classroomController.get({ path: '', userType: UserType.ADMIN }, async (_req: Req
const classroomRepository = AppDataSource.getRepository(Classroom);
let classrooms;
if (villageId) {
- const query = classroomRepository.createQueryBuilder('classroom');
+ const query = classroomRepository.createQueryBuilder('classroom').leftJoinAndSelect('classroom.user', 'user');
query.where('classroom.villageId = :villageId', { villageId });
classrooms = await query.getMany();
- //classrooms = await classroomRepository.find({ where: { villageId: +villageId } });
} else {
classrooms = await classroomRepository.find();
}
diff --git a/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx b/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx
index 60196d481..48d56e779 100644
--- a/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx
+++ b/src/components/admin/dashboard-statistics/filters/ClassroomDropdown.tsx
@@ -7,6 +7,7 @@ import MenuItem from '@mui/material/MenuItem';
import type { SelectChangeEvent } from '@mui/material/Select';
import Select from '@mui/material/Select';
+import { getUserDisplayName } from 'src/utils';
import type { Classroom } from 'types/classroom.type';
interface ClassroomDropdownProps {
@@ -24,17 +25,19 @@ export default function ClassroomDropdown({ classrooms, onClassroomChange }: Cla
};
return (
-
-
-
-
-
-
+ classrooms && (
+
+
+
+
+
+
+ )
);
}
From 80f7071d15ffaef3fb5976293811b068c3ce2388 Mon Sep 17 00:00:00 2001
From: Benjamin Ramet
Date: Mon, 2 Dec 2024 08:43:40 +0100
Subject: [PATCH 4/5] Redefine style to avoid glitch between village and
classroom tabs
---
.../admin/dashboard-statistics/ClassroomStats.tsx | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/components/admin/dashboard-statistics/ClassroomStats.tsx b/src/components/admin/dashboard-statistics/ClassroomStats.tsx
index 42fda598a..2c0e14ce9 100644
--- a/src/components/admin/dashboard-statistics/ClassroomStats.tsx
+++ b/src/components/admin/dashboard-statistics/ClassroomStats.tsx
@@ -72,7 +72,7 @@ const ClassroomStats = () => {
setSelectedPhase(phase);
};
- const noDataFoundMessage = 'Pas de données pour le Village-Monde sélectionné';
+ const noDataFoundMessage = 'Pas de données pour la classe sélectionnée';
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
@@ -80,7 +80,17 @@ const ClassroomStats = () => {
return (
<>
-
+
From 6918008e2799dcb8b69610080c59ed0c34492e73 Mon Sep 17 00:00:00 2001
From: Benjamin Ramet
Date: Mon, 2 Dec 2024 16:16:40 +0100
Subject: [PATCH 5/5] Refactor stats controller
---
server/controllers/index.ts | 2 +
server/controllers/statistics.ts | 43 +++++----
server/stats/classroomStats.ts | 77 ++++-----------
server/stats/globalStats.ts | 31 ++++++
server/stats/queryStatsByFilter.ts | 149 +++++++++++++++++++++++++++++
server/stats/villageStats.ts | 143 ++++++---------------------
types/statistics.type.ts | 8 ++
7 files changed, 261 insertions(+), 192 deletions(-)
create mode 100644 server/stats/globalStats.ts
create mode 100644 server/stats/queryStatsByFilter.ts
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 0aa408ca1..d4c30efe5 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -14,6 +14,7 @@ import { languageController } from './languages';
import { mediathequeController } from './mediatheque';
import { notificationsController } from './notifications';
import { pelicoController } from './pelicoPresentation';
+import { phaseHistoryController } from './phaseHistory';
import { statisticsController } from './statistics';
import { storyController } from './story';
import { studentController } from './student';
@@ -51,6 +52,7 @@ const controllers = [
pelicoController,
mediathequeController,
notificationsController,
+ phaseHistoryController,
];
for (let i = 0, n = controllers.length; i < n; i++) {
diff --git a/server/controllers/statistics.ts b/server/controllers/statistics.ts
index 3530aafcd..2324f8c55 100644
--- a/server/controllers/statistics.ts
+++ b/server/controllers/statistics.ts
@@ -5,10 +5,17 @@ import {
getConnectedClassroomsCount,
getContributedClassroomsCount,
getRegisteredClassroomsCount,
- getChildrenCodesCount as getChildrenCodesCountForClassroom,
- getConnectedFamiliesCount as getConnectedFamiliesCountForClassroom,
- getFamiliesWithoutAccount as getFamiliesWithoutAccountForClassroom,
+ getChildrenCodesCountForClassroom,
+ getConnectedFamiliesCountForClassroom,
+ getFamiliesWithoutAccountForClassroom,
} from '../stats/classroomStats';
+import {
+ getChildrenCodesCountForGlobal,
+ getConnectedFamiliesCountForGlobal,
+ getFamiliesWithoutAccountForGlobal,
+ getFamilyAccountsCountForGlobal,
+ getFloatingAccountsForGlobal,
+} from '../stats/globalStats';
import {
getAverageConnections,
getAverageDuration,
@@ -20,11 +27,11 @@ import {
getMinDuration,
} from '../stats/sessionStats';
import {
- getChildrenCodesCount,
- getFamilyAccountsCount,
- getConnectedFamiliesCount,
- getFamiliesWithoutAccount,
- getFloatingAccounts,
+ getChildrenCodesCountForVillage,
+ getConnectedFamiliesCountForVillage,
+ getFamiliesWithoutAccountForVillage,
+ getFloatingAccountsForVillage,
+ getFamilyAccountsCountForVillage,
} from '../stats/villageStats';
import { Controller } from './controller';
@@ -56,11 +63,11 @@ 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(),
+ familyAccountsCount: await getFamilyAccountsCountForGlobal(),
+ childrenCodesCount: await getChildrenCodesCountForGlobal(),
+ connectedFamiliesCount: await getConnectedFamiliesCountForGlobal(),
+ familiesWithoutAccount: await getFamiliesWithoutAccountForGlobal(),
+ floatingAccounts: await getFloatingAccountsForGlobal(),
});
});
@@ -68,11 +75,11 @@ statisticsController.get({ path: '/villages/:villageId' }, async (_req, res) =>
const villageId = parseInt(_req.params.villageId);
const phase = _req.query.phase as unknown as number;
res.sendJSON({
- familyAccountsCount: await getFamilyAccountsCount(villageId, phase),
- childrenCodesCount: await getChildrenCodesCount(villageId, phase),
- connectedFamiliesCount: await getConnectedFamiliesCount(villageId, phase),
- familiesWithoutAccount: await getFamiliesWithoutAccount(villageId),
- floatingAccounts: await getFloatingAccounts(villageId),
+ familyAccountsCount: await getFamilyAccountsCountForVillage(villageId, phase),
+ childrenCodesCount: await getChildrenCodesCountForVillage(villageId, phase),
+ connectedFamiliesCount: await getConnectedFamiliesCountForVillage(villageId, phase),
+ familiesWithoutAccount: await getFamiliesWithoutAccountForVillage(villageId),
+ floatingAccounts: await getFloatingAccountsForVillage(villageId),
});
});
diff --git a/server/stats/classroomStats.ts b/server/stats/classroomStats.ts
index 67c11e0a6..3e0645924 100644
--- a/server/stats/classroomStats.ts
+++ b/server/stats/classroomStats.ts
@@ -1,14 +1,11 @@
import { UserType } from '../../types/user.type';
import { Activity } from '../entities/activity';
import { Classroom } from '../entities/classroom';
-import { Student } from '../entities/student';
-import { Village } from '../entities/village';
+import type { VillagePhase } from '../entities/village';
import { AppDataSource } from '../utils/data-source';
-import { getPhasePeriod, phaseWasSelected } from './villageStats';
+import { generateEmptyFilterParams, getChildrenCodesCount, getConnectedFamiliesCount, getFamiliesWithoutAccount } from './queryStatsByFilter';
const classroomRepository = AppDataSource.getRepository(Classroom);
-const studentRepository = AppDataSource.getRepository(Student);
-const villageRepository = AppDataSource.getRepository(Village);
const teacherType = UserType.TEACHER;
@@ -92,75 +89,35 @@ export const getContributedClassroomsCount = async (phase: number | null) => {
return 10;
};
-export const getChildrenCodesCount = async (classroomId?: number, phase?: number) => {
+export const getChildrenCodesCountForClassroom = async (classroomId: number, phase: VillagePhase) => {
const classroom = await classroomRepository
.createQueryBuilder('classroom')
.innerJoin('classroom.village', 'village')
.where('classroom.id = :classroomId', { classroomId })
.getOne();
+
const villageId = classroom?.villageId;
- const query = studentRepository.createQueryBuilder('student').innerJoin('student.classroom', 'classroom').innerJoin('classroom.village', 'village');
- if (classroomId) {
- query.andWhere('classroom.id = :classroomId', { classroomId });
- if (phaseWasSelected(phase) && villageId) {
- const village = await villageRepository.findOne({ where: { id: villageId } });
- const phaseValue = phase as number;
- const { debut, end } = await getPhasePeriod(villageId, phaseValue);
- query.andWhere('student.createdAt >= :debut', { debut });
- if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
- }
- }
- const childrenCodeCount = await query.getCount();
- return childrenCodeCount;
+
+ if (!classroomId || !villageId) return 0;
+ let filterParams = generateEmptyFilterParams();
+ filterParams = { ...filterParams, villageId, classroomId, phase };
+ const whereClause = { clause: 'classroom.id = :classroomId', value: { classroomId } };
+ return await getChildrenCodesCount(filterParams, whereClause);
};
-export const getConnectedFamiliesCount = async (classroomId?: number, phase?: number) => {
+export const getConnectedFamiliesCountForClassroom = async (classroomId: number, phase: VillagePhase) => {
const classroom = await classroomRepository
.createQueryBuilder('classroom')
.innerJoin('classroom.village', 'village')
.where('classroom.id = :classroomId', { classroomId })
.getOne();
- const villageId = classroom?.villageId;
- const village = await villageRepository.findOne({ where: { id: villageId } });
- const query = studentRepository
- .createQueryBuilder('student')
- .innerJoin('classroom', 'classroom', 'classroom.id = student.classroomId')
- .andWhere('classroom.id = :classroomId', { classroomId })
- .andWhere('student.numLinkedAccount >= 1');
- if (villageId) {
- query.andWhere('classroom.villageId = :villageId', { villageId });
- if (phaseWasSelected(phase)) {
- const phaseValue = phase as number;
- const { debut, end } = await getPhasePeriod(villageId, phaseValue);
- query.andWhere('student.createdAt >= :debut', { debut });
- if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
- }
- }
-
- const connectedFamiliesCount = await query.getCount();
- return connectedFamiliesCount;
+ const villageId = classroom?.villageId;
+ let filterParams = generateEmptyFilterParams();
+ filterParams = { ...filterParams, villageId, classroomId, phase };
+ return await getConnectedFamiliesCount(filterParams);
};
-export const getFamiliesWithoutAccount = async (classroomId?: number) => {
- const query = studentRepository
- .createQueryBuilder('student')
- .innerJoin('student.classroom', 'classroom')
- .innerJoin('classroom.user', 'user')
- .innerJoin('user.village', 'village')
- .where('student.numLinkedAccount < 1');
- if (classroomId) query.andWhere('classroom.id = :classroomId', { classroomId });
-
- 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 getFamiliesWithoutAccountForClassroom = async (classroomId: number) => {
+ return getFamiliesWithoutAccount('classroom.id = :classroomId', { classroomId });
};
diff --git a/server/stats/globalStats.ts b/server/stats/globalStats.ts
new file mode 100644
index 000000000..98a381754
--- /dev/null
+++ b/server/stats/globalStats.ts
@@ -0,0 +1,31 @@
+import {
+ generateEmptyFilterParams,
+ getChildrenCodesCount,
+ getConnectedFamiliesCount,
+ getFamiliesWithoutAccount,
+ getFamilyAccountsCount,
+ getFloatingAccounts,
+} from './queryStatsByFilter';
+
+export const getFamiliesWithoutAccountForGlobal = async () => {
+ return getFamiliesWithoutAccount();
+};
+
+export const getConnectedFamiliesCountForGlobal = async () => {
+ const filterParams = generateEmptyFilterParams();
+ return getConnectedFamiliesCount(filterParams);
+};
+
+export const getChildrenCodesCountForGlobal = async () => {
+ const filterParams = generateEmptyFilterParams();
+ return await getChildrenCodesCount(filterParams);
+};
+export const getFloatingAccountsForGlobal = async () => {
+ const filterParams = generateEmptyFilterParams();
+ return await getFloatingAccounts(filterParams);
+};
+
+export const getFamilyAccountsCountForGlobal = async () => {
+ const filterParams = generateEmptyFilterParams();
+ return await getFamilyAccountsCount(filterParams);
+};
diff --git a/server/stats/queryStatsByFilter.ts b/server/stats/queryStatsByFilter.ts
new file mode 100644
index 000000000..8e97d32b9
--- /dev/null
+++ b/server/stats/queryStatsByFilter.ts
@@ -0,0 +1,149 @@
+import type { StatsFilterParams } from '../../types/statistics.type';
+import { PhaseHistory } from '../entities/phaseHistory';
+import { Student } from '../entities/student';
+import { User } from '../entities/user';
+import { VillagePhase, Village } from '../entities/village';
+import { AppDataSource } from '../utils/data-source';
+
+const studentRepository = AppDataSource.getRepository(Student);
+const villageRepository = AppDataSource.getRepository(Village);
+const userRepository = AppDataSource.getRepository(User);
+const phaseHistoryRepository = AppDataSource.getRepository(PhaseHistory);
+
+export const getFamiliesWithoutAccount = async (condition?: string, conditionValue?: object) => {
+ const query = studentRepository
+ .createQueryBuilder('student')
+ .innerJoin('student.classroom', 'classroom')
+ .innerJoin('classroom.user', 'user')
+ .innerJoin('user.village', 'village')
+ .where('student.numLinkedAccount < 1');
+
+ if (condition && conditionValue) {
+ query.andWhere(condition, conditionValue);
+ }
+
+ 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',
+ ]);
+
+ return query.getRawMany();
+};
+
+export const getConnectedFamiliesCount = async (filterParams: StatsFilterParams) => {
+ const { villageId, classroomId, phase } = filterParams;
+ const query = studentRepository
+ .createQueryBuilder('student')
+ .innerJoin('classroom', 'classroom', 'classroom.id = student.classroomId')
+ .andWhere('student.numLinkedAccount >= 1');
+
+ if (classroomId) {
+ query.andWhere('classroom.id = :classroomId', { classroomId });
+ }
+
+ if (villageId) {
+ query.andWhere('classroom.villageId = :villageId', { villageId });
+
+ if (phaseWasSelected(phase)) {
+ const phaseValue = phase as number;
+ const { debut, end } = await getPhasePeriod(villageId, phaseValue);
+ query.andWhere('student.createdAt >= :debut', { debut });
+ if (phaseValue != (await villageRepository.findOne({ where: { id: villageId } }))?.activePhase) {
+ query.andWhere('student.createdAt <= :end', { end });
+ }
+ }
+ }
+
+ return await query.getCount();
+};
+
+export const getFloatingAccounts = async (filterParams: StatsFilterParams) => {
+ const { villageId } = filterParams;
+ 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 = await query.getMany();
+ return floatingAccounts;
+};
+
+export const getFamilyAccountsCount = async (filterParams: { villageId: number | undefined; phase: VillagePhase | undefined }) => {
+ const { villageId, phase } = filterParams;
+ const village = await villageRepository.findOne({ where: { id: villageId } });
+ const query = userRepository
+ .createQueryBuilder('user')
+ .innerJoin('user.village', 'village')
+ .innerJoin('classroom', 'classroom', 'classroom.villageId = village.id')
+ .innerJoin('student', 'student', 'student.classroomId = classroom.id')
+ .where('user.type = 3');
+
+ if (villageId) {
+ query.andWhere('classroom.villageId = :villageId', { villageId });
+ if (phaseWasSelected(phase)) {
+ const phaseValue = phase as number;
+ const { debut, end } = await getPhasePeriod(villageId, phaseValue);
+ query.andWhere('user.createdAt >= :debut', { debut });
+ if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
+ }
+ }
+
+ query.groupBy('user.id');
+ const familyAccountsCount = await query.getCount();
+ return familyAccountsCount;
+};
+
+export const generateEmptyFilterParams = (): StatsFilterParams => {
+ const filterParams: { [K in keyof StatsFilterParams]: StatsFilterParams[K] } = {
+ villageId: undefined,
+ classroomId: undefined,
+ phase: undefined,
+ };
+
+ return filterParams;
+};
+export type WhereClause = {
+ clause: string;
+ value: object;
+};
+export const getChildrenCodesCount = async (filterParams: StatsFilterParams, whereClause?: WhereClause) => {
+ const { villageId, phase } = filterParams;
+ const village = await villageRepository.findOne({ where: { id: villageId } });
+ const query = studentRepository.createQueryBuilder('student').innerJoin('student.classroom', 'classroom').innerJoin('classroom.village', 'village');
+ if (whereClause) query.where(whereClause.clause, whereClause.value);
+
+ if (phaseWasSelected(phase) && villageId) {
+ const phaseValue = phase as number;
+ const { debut, end } = await getPhasePeriod(villageId, phaseValue);
+ query.andWhere('student.createdAt >= :debut', { debut });
+ if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
+ }
+
+ return await query.getCount();
+};
+
+export const getPhasePeriod = async (villageId: number, phase: number): Promise<{ debut: Date | undefined; end: Date | undefined }> => {
+ // Getting the debut and end dates for the given phase
+ const query = phaseHistoryRepository
+ .createQueryBuilder('phaseHistory')
+ .withDeleted()
+ .where('phaseHistory.villageId = :villageId', { villageId })
+ .andWhere('phaseHistory.phase = :phase', { phase });
+ query.select(['phaseHistory.startingOn', 'phaseHistory.endingOn']);
+ const result = await query.getOne();
+ const debut = result?.startingOn;
+ const end = result?.endingOn;
+ return {
+ debut,
+ end,
+ };
+};
+
+export const phaseWasSelected = (phase: number | undefined): boolean => {
+ return phase !== undefined && Object.values(VillagePhase).includes(+phase);
+};
diff --git a/server/stats/villageStats.ts b/server/stats/villageStats.ts
index 1ed202ab3..41c0ea372 100644
--- a/server/stats/villageStats.ts
+++ b/server/stats/villageStats.ts
@@ -1,123 +1,38 @@
-import { PhaseHistory } from '../entities/phaseHistory';
-import { Student } from '../entities/student';
-import { User } from '../entities/user';
-import { Village, VillagePhase } from '../entities/village';
-import { AppDataSource } from '../utils/data-source';
-
-const userRepository = AppDataSource.getRepository(User);
-const studentRepository = AppDataSource.getRepository(Student);
-const phaseHistoryRepository = AppDataSource.getRepository(PhaseHistory);
-const villageRepository = AppDataSource.getRepository(Village);
-
-export const getPhasePeriod = async (villageId: number, phase: number): Promise<{ debut: Date | undefined; end: Date | undefined }> => {
- // Getting the debut and end dates for the given phase
- const query = phaseHistoryRepository
- .createQueryBuilder('phaseHistory')
- .withDeleted()
- .where('phaseHistory.villageId = :villageId', { villageId })
- .andWhere('phaseHistory.phase = :phase', { phase });
- query.select(['phaseHistory.startingOn', 'phaseHistory.endingOn']);
- const result = await query.getOne();
- const debut = result?.startingOn;
- const end = result?.endingOn;
- return {
- debut,
- end,
- };
+import type { VillagePhase } from '../entities/village';
+import {
+ generateEmptyFilterParams,
+ getChildrenCodesCount,
+ getConnectedFamiliesCount,
+ getFamiliesWithoutAccount,
+ getFamilyAccountsCount,
+ getFloatingAccounts,
+} from './queryStatsByFilter';
+
+export const getChildrenCodesCountForVillage = async (villageId: number, phase: VillagePhase) => {
+ let filterParams = generateEmptyFilterParams();
+ filterParams = { ...filterParams, villageId, phase };
+ const whereClause = { clause: 'classroom.villageId = :villageId', value: { villageId } };
+ return await getChildrenCodesCount(filterParams, whereClause);
};
-export const phaseWasSelected = (phase: number | undefined): boolean => {
- return phase !== undefined && Object.values(VillagePhase).includes(+phase);
+export const getFamilyAccountsCountForVillage = async (villageId: number, phase: number) => {
+ let filterParams = generateEmptyFilterParams();
+ filterParams = { ...filterParams, villageId, phase };
+ return await getFamilyAccountsCount(filterParams);
};
-export const getChildrenCodesCount = async (villageId?: number, phase?: number) => {
- const query = studentRepository.createQueryBuilder('student').innerJoin('student.classroom', 'classroom').innerJoin('classroom.village', 'village');
- const village = await villageRepository.findOne({ where: { id: villageId } });
- if (villageId) {
- query.andWhere('classroom.villageId = :villageId', { villageId });
- if (phaseWasSelected(phase)) {
- const phaseValue = phase as number;
- const { debut, end } = await getPhasePeriod(villageId, phaseValue);
- query.andWhere('student.createdAt >= :debut', { debut });
- if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
- }
- }
- const childrenCodeCount = await query.getCount();
- return childrenCodeCount;
-};
-
-export const getFamilyAccountsCount = async (villageId?: number, phase?: number) => {
- const village = await villageRepository.findOne({ where: { id: villageId } });
- const query = userRepository
- .createQueryBuilder('user')
- .innerJoin('user.village', 'village')
- .innerJoin('classroom', 'classroom', 'classroom.villageId = village.id')
- .innerJoin('student', 'student', 'student.classroomId = classroom.id');
- if (villageId) {
- query.andWhere('classroom.villageId = :villageId', { villageId });
- if (phaseWasSelected(phase)) {
- const phaseValue = phase as number;
- const { debut, end } = await getPhasePeriod(villageId, phaseValue);
- query.andWhere('user.createdAt >= :debut', { debut });
- if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
- }
- }
-
- query.groupBy('user.id');
- const familyAccountsCount = await query.getCount();
- return familyAccountsCount;
+export const getConnectedFamiliesCountForVillage = async (villageId: number, phase: number) => {
+ let filterParams = generateEmptyFilterParams();
+ filterParams = { ...filterParams, villageId, phase };
+ return await getConnectedFamiliesCount(filterParams);
};
-export const getConnectedFamiliesCount = async (villageId?: number, phase?: number) => {
- const village = await villageRepository.findOne({ where: { id: villageId } });
- const query = studentRepository
- .createQueryBuilder('student')
- .innerJoin('classroom', 'classroom', 'classroom.id = student.classroomId')
- .andWhere('student.numLinkedAccount >= 1');
- if (villageId) {
- query.andWhere('classroom.villageId = :villageId', { villageId });
- if (phaseWasSelected(phase)) {
- const phaseValue = phase as number;
- const { debut, end } = await getPhasePeriod(villageId, phaseValue);
- query.andWhere('student.createdAt >= :debut', { debut });
- if (phaseValue != village?.activePhase) query.andWhere('student.createdAt <= :end', { end });
- }
- }
-
- const connectedFamiliesCount = await query.getCount();
-
- return connectedFamiliesCount;
+export const getFamiliesWithoutAccountForVillage = async (villageId: number) => {
+ return getFamiliesWithoutAccount('classroom.villageId = :villageId', { villageId });
};
-export const getFamiliesWithoutAccount = async (villageId?: number) => {
- const query = studentRepository
- .createQueryBuilder('student')
- .innerJoin('student.classroom', 'classroom')
- .innerJoin('classroom.user', 'user')
- .innerJoin('user.village', 'village')
- .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 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;
+export const getFloatingAccountsForVillage = async (villageId: number) => {
+ let filterParams = generateEmptyFilterParams();
+ filterParams = { ...filterParams, villageId };
+ return await getFloatingAccounts(filterParams);
};
diff --git a/types/statistics.type.ts b/types/statistics.type.ts
index cc74dec8a..44b069ced 100644
--- a/types/statistics.type.ts
+++ b/types/statistics.type.ts
@@ -1,3 +1,5 @@
+import type { VillagePhase } from './village.type';
+
export interface ClassroomsStats {
classroomId: number;
classroomCountryCode: string;
@@ -59,3 +61,9 @@ export interface OneVillageTableRow {
id: string | number;
[key: string]: string | boolean | number | React.ReactNode;
}
+
+export type StatsFilterParams = {
+ villageId: number | undefined;
+ classroomId: number | undefined;
+ phase: VillagePhase | undefined;
+};