Skip to content

Commit

Permalink
Merge pull request #1012 from parlemonde/feat/VIL-616
Browse files Browse the repository at this point in the history
feat/VIL-616 (phase history and filter by phase)
  • Loading branch information
Benjyhy authored Nov 26, 2024
2 parents aea45b4 + c2ba9e4 commit 0603acf
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 30 deletions.
67 changes: 67 additions & 0 deletions server/controllers/phaseHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { PhaseHistory } from '../entities/phaseHistory';
import { Village, VillagePhase } from '../entities/village';
import { AppDataSource } from '../utils/data-source';
import { Controller } from './controller';

const handlePhase1toPhase3Case = async (village: Village, phase: number) => {
if (village.activePhase === VillagePhase.DISCOVER && phase === VillagePhase.IMAGINE) {
const phaseHistory = new PhaseHistory();
phaseHistory.village = village;
phaseHistory.phase = VillagePhase.EXCHANGE;
phaseHistory.endingOn = new Date();

const phaseHistoryRepository = AppDataSource.getRepository(PhaseHistory);

await phaseHistoryRepository.save(phaseHistory);

const endPhase1Query = phaseHistoryRepository
.createQueryBuilder()
.softDelete()
.where('villageId = :villageId', { villageId: village.id })
.andWhere('phase = :phase', { phase: VillagePhase.DISCOVER });
await endPhase1Query.execute();
}
};
export const phaseHistoryController = new Controller('/phase-history');

phaseHistoryController.post({ path: '/' }, async (_req, res) => {
const data = _req.body;
try {
//Find village wit a given id
const villageRepository = AppDataSource.getRepository(Village);
const village = await villageRepository.findOne({ where: { id: data.villageId } });
if (!village) throw new Error('Village Not found');

await handlePhase1toPhase3Case(village, data.phase);

// Create a PhaseHistory instance and fill it with data
const phaseHistory = new PhaseHistory();
phaseHistory.village = village;
phaseHistory.phase = data.phase;
phaseHistory.startingOn = new Date();

// Save phase history in db
const phaseHistoryRepository = AppDataSource.getRepository(PhaseHistory);
await phaseHistoryRepository.save(phaseHistory);
res.sendStatus(200);
} catch (e) {
console.error(e);
}
});

phaseHistoryController.delete({ path: '/soft-delete/:villageId/:phase' }, async (_req, res) => {
const { villageId, phase } = _req.params;
const phaseHistoryRepository = AppDataSource.getRepository(PhaseHistory);
const query = phaseHistoryRepository
.createQueryBuilder()
.softDelete()
.where('villageId = :villageId', { villageId })
.andWhere('phase = :phase', { phase });

try {
await query.execute();
res.sendStatus(202);
} catch (e) {
console.error(e);
}
});
7 changes: 4 additions & 3 deletions server/controllers/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ statisticsController.get({ path: '/onevillage' }, async (_req, res) => {

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),
childrenCodesCount: await getChildrenCodesCount(villageId),
connectedFamiliesCount: await getConnectedFamiliesCount(villageId),
familyAccountsCount: await getFamilyAccountsCount(villageId, phase),
childrenCodesCount: await getChildrenCodesCount(villageId, phase),
connectedFamiliesCount: await getConnectedFamiliesCount(villageId, phase),
familiesWithoutAccount: await getFamiliesWithoutAccount(villageId),
floatingAccounts: await getFloatingAccounts(villageId),
});
Expand Down
25 changes: 25 additions & 0 deletions server/entities/phaseHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';

import type { VillagePhase } from './village';
import { Village } from './village';

@Entity()
@Index('IDX_PHASE_HISTORY', ['village', 'phase'], { unique: true })
export class PhaseHistory {
@PrimaryGeneratedColumn()
public id: number;

@ManyToOne(() => Village, (village) => village.phaseHistories, { onDelete: 'CASCADE' })
village: Village;

@Column({
type: 'tinyint',
})
phase: VillagePhase;

@CreateDateColumn({ type: 'datetime' })
public startingOn: Date;

@DeleteDateColumn({ type: 'datetime' })
public endingOn: Date;
}
4 changes: 4 additions & 0 deletions server/entities/village.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Classroom } from './classroom';
import { Game } from './game';
import { GameResponse } from './gameResponse';
import { Image } from './image';
import { PhaseHistory } from './phaseHistory';
import { User } from './user';

export { VillagePhase };
Expand Down Expand Up @@ -39,6 +40,9 @@ export class Village implements VillageInterface {
})
public activePhase: number;

@OneToMany(() => PhaseHistory, (phaseHistory) => phaseHistory.village, { eager: true })
public phaseHistories: PhaseHistory[];

@OneToOne(() => Activity, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'anthemId' })
public anthem: Activity | null;
Expand Down
82 changes: 82 additions & 0 deletions server/migrations/1731486908522-AddPhaseHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Table, TableForeignKey, TableIndex } from 'typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddPhaseHistory1731486908522 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'phase_history',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'villageId',
type: 'int',
isNullable: false,
},
{
name: 'phase',
type: 'tinyint',
},
{
name: 'startingOn',
type: 'datetime',
},
{
name: 'endingOn',
type: 'datetime',
isNullable: true,
},
],
}),
);

await queryRunner.createIndex(
'phase_history',
new TableIndex({
name: 'IDX_PHASE_HISTORY',
columnNames: ['villageId', 'phase'],
isUnique: true,
}),
);

await queryRunner.createForeignKey(
'phase_history',
new TableForeignKey({
columnNames: ['villageId'],
referencedColumnNames: ['id'],
referencedTableName: 'village',
onDelete: 'CASCADE',
}),
);

const villages = await queryRunner.query(`SELECT id FROM village`);
const startingOn2024Phase1Date = '2024-09-30 00:00:00.000000';

for (const village of villages) {
await queryRunner.query(`INSERT INTO phase_history (villageId, phase, startingOn, endingOn) VALUES (?, ?, ?, ?)`, [
village.id,
1,
startingOn2024Phase1Date,
null,
]);
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('phase_history');
if (table) {
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf('villageId') !== -1);
if (foreignKey) {
await queryRunner.dropForeignKey('phase_history', foreignKey);
await queryRunner.dropIndex('phase_history', 'IDX_PHASE_HISTORY');
await queryRunner.dropTable('phase_history');
}
}
}
}
67 changes: 59 additions & 8 deletions server/stats/villageStats.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,89 @@
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 getChildrenCodesCount = async (villageId?: number) => {
const query = studentRepository.createQueryBuilder('student').innerJoin('student.classroom', 'classroom').innerJoin('classroom.village', 'village');
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,
};
};

const phaseWasSelected = (phase: number | undefined): boolean => {
return phase !== undefined && Object.values(VillagePhase).includes(+phase);
};

if (villageId) query.where('classroom.villageId = :villageId', { villageId });
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) => {
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.where('classroom.villageId = :villageId', { 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('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 getConnectedFamiliesCount = async (villageId?: number) => {
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')
.where('student.numLinkedAccount >= 1');
if (villageId) query.andWhere('classroom.villageId = :villageId', { villageId });
.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();

Expand Down
15 changes: 15 additions & 0 deletions src/api/phaseHistory/phaseHistory.delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { PhaseHistory } from 'server/entities/phaseHistory';

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

export async function softDeletePhaseHistory(villageId: number, phase: number): Promise<PhaseHistory> {
const response = await axiosRequest<PhaseHistory>({
method: 'DELETE',
url: `/phase-history/soft-delete/${villageId}/${phase}`,
});
if (response.error) {
throw new Error("Erreur lors de la suppresion de l'historique des phases. Veuillez réessayer.");
}

return response.data;
}
16 changes: 16 additions & 0 deletions src/api/phaseHistory/phaseHistory.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { PhaseHistory } from 'server/entities/phaseHistory';

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

export async function postPhaseHistory(data: Partial<PhaseHistory> & { villageId: number }): Promise<PhaseHistory> {
const response = await axiosRequest<PhaseHistory>({
method: 'POST',
url: '/phase-history',
data,
});
if (response.error) {
throw new Error("Erreur lors de la création de l'historique des phases. Veuillez réessayer.");
}

return response.data;
}
8 changes: 4 additions & 4 deletions src/api/statistics/statistics.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ async function getOneVillageStats(): Promise<VillageStats> {
).data;
}

async function getVillagesStats(villageId: number | null): Promise<VillageStats> {
async function getVillagesStats(villageId: number | null, phase: number): Promise<VillageStats> {
return (
await axiosRequest({
method: 'GET',
baseURL: '/api',
url: `/statistics/villages/${villageId}`,
url: phase ? `/statistics/villages/${villageId}?phase=${phase}` : `/statistics/villages/${villageId}`,
})
).data;
}
Expand All @@ -40,8 +40,8 @@ export const useGetOneVillageStats = () => {
return useQuery(['1v-stats'], () => getOneVillageStats());
};

export const useGetVillagesStats = (villageId: number | null) => {
return useQuery(['villages-stats', villageId], () => getVillagesStats(villageId), {
export const useGetVillagesStats = (villageId: number | null, phase: number) => {
return useQuery(['villages-stats', villageId, phase], () => getVillagesStats(villageId, phase), {
enabled: villageId !== null,
});
};
Expand Down
Loading

0 comments on commit 0603acf

Please sign in to comment.