Skip to content

Commit

Permalink
Merge pull request #1014 from parlemonde/feat/VIL-59
Browse files Browse the repository at this point in the history
Feat/vil 59
  • Loading branch information
Benjyhy authored Dec 5, 2024
2 parents 0603acf + 6918008 commit 88958ad
Show file tree
Hide file tree
Showing 13 changed files with 474 additions and 162 deletions.
20 changes: 20 additions & 0 deletions server/controllers/classroom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ 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').leftJoinAndSelect('classroom.user', 'user');
query.where('classroom.villageId = :villageId', { villageId });

classrooms = await query.getMany();
} 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({
Expand Down
2 changes: 2 additions & 0 deletions server/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -51,6 +52,7 @@ const controllers = [
pelicoController,
mediathequeController,
notificationsController,
phaseHistoryController,
];

for (let i = 0, n = controllers.length; i < n; i++) {
Expand Down
50 changes: 35 additions & 15 deletions server/controllers/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ import {
getConnectedClassroomsCount,
getContributedClassroomsCount,
getRegisteredClassroomsCount,
getChildrenCodesCountForClassroom,
getConnectedFamiliesCountForClassroom,
getFamiliesWithoutAccountForClassroom,
} from '../stats/classroomStats';
import {
getChildrenCodesCountForGlobal,
getConnectedFamiliesCountForGlobal,
getFamiliesWithoutAccountForGlobal,
getFamilyAccountsCountForGlobal,
getFloatingAccountsForGlobal,
} from '../stats/globalStats';
import {
getAverageConnections,
getAverageDuration,
Expand All @@ -17,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';

Expand Down Expand Up @@ -53,22 +63,32 @@ 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(),
});
});

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),
});
});

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),
});
});
35 changes: 35 additions & 0 deletions server/stats/classroomStats.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { UserType } from '../../types/user.type';
import { Activity } from '../entities/activity';
import { Classroom } from '../entities/classroom';
import type { VillagePhase } from '../entities/village';
import { AppDataSource } from '../utils/data-source';
import { generateEmptyFilterParams, getChildrenCodesCount, getConnectedFamiliesCount, getFamiliesWithoutAccount } from './queryStatsByFilter';

const classroomRepository = AppDataSource.getRepository(Classroom);

Expand Down Expand Up @@ -86,3 +88,36 @@ export const getContributedClassroomsCount = async (phase: number | null) => {
// return parseInt(result.contributedUsersCount);
return 10;
};

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;

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 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;
let filterParams = generateEmptyFilterParams();
filterParams = { ...filterParams, villageId, classroomId, phase };
return await getConnectedFamiliesCount(filterParams);
};

export const getFamiliesWithoutAccountForClassroom = async (classroomId: number) => {
return getFamiliesWithoutAccount('classroom.id = :classroomId', { classroomId });
};
31 changes: 31 additions & 0 deletions server/stats/globalStats.ts
Original file line number Diff line number Diff line change
@@ -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);
};
149 changes: 149 additions & 0 deletions server/stats/queryStatsByFilter.ts
Original file line number Diff line number Diff line change
@@ -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);
};
Loading

0 comments on commit 88958ad

Please sign in to comment.