Skip to content

Commit

Permalink
Merge pull request #998 from parlemonde/VIL-586
Browse files Browse the repository at this point in the history
Vil 586
  • Loading branch information
Benjyhy authored Oct 29, 2024
2 parents 47588db + 4ce8259 commit 96ef588
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 39 deletions.
9 changes: 8 additions & 1 deletion server/controllers/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import {
getMinConnections,
getMinDuration,
} from '../stats/sessionStats';
import { getChildrenCodesCount, getFamilyAccountsCount, getConnectedFamiliesCount, getFamiliesWithoutAccount } from '../stats/villageStats';
import {
getChildrenCodesCount,
getFamilyAccountsCount,
getConnectedFamiliesCount,
getFamiliesWithoutAccount,
getFloatingAccounts,
} from '../stats/villageStats';
import { Controller } from './controller';

export const statisticsController = new Controller('/statistics');
Expand Down Expand Up @@ -52,5 +58,6 @@ statisticsController.get({ path: '/villages/:villageId' }, async (_req, res) =>
childrenCodesCount: await getChildrenCodesCount(villageId),
connectedFamiliesCount: await getConnectedFamiliesCount(villageId),
familiesWithoutAccount: await getFamiliesWithoutAccount(villageId),
floatingAccounts: await getFloatingAccounts(villageId),
});
});
5 changes: 4 additions & 1 deletion server/entities/student.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity, Column, ManyToOne, OneToMany, PrimaryGeneratedColumn, JoinColumn } from 'typeorm';
import { Entity, Column, ManyToOne, OneToMany, PrimaryGeneratedColumn, JoinColumn, CreateDateColumn } from 'typeorm';

import { Classroom } from './classroom';
import { UserToStudent } from './userToStudent';
Expand All @@ -8,6 +8,9 @@ export class Student {
@PrimaryGeneratedColumn()
public id: number;

@CreateDateColumn()
createdAt: Date;

@Column({ type: 'varchar', length: 100, nullable: true })
public firstname: string;

Expand Down
5 changes: 4 additions & 1 deletion server/entities/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Column, Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, OneToMany, ManyToMany, JoinTable } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, OneToMany, ManyToMany, JoinTable, CreateDateColumn } from 'typeorm';

import type { Country } from '../../types/country.type';
import { UserType } from '../../types/user.type';
Expand All @@ -19,6 +19,9 @@ export class User implements UserInterface {
@PrimaryGeneratedColumn()
public id: number;

@CreateDateColumn()
createdAt: Date;

@Column({ type: 'varchar', length: 255, unique: true })
public email: string;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TableColumn } from 'typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddCreatedAtColumnToStudentAndUserEntities1729236109909 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'student',
new TableColumn({
name: 'createdAt',
type: 'timestamp',
isNullable: true,
}),
);
await queryRunner.query('UPDATE `student` SET `createdAt` = NULL WHERE `createdAt` IS NOT NULL');
await queryRunner.changeColumn(
'student',
'createdAt',
new TableColumn({
name: 'createdAt',
type: 'timestamp',
isNullable: false,
default: 'CURRENT_TIMESTAMP',
}),
);
await queryRunner.addColumn(
'user',
new TableColumn({
name: 'createdAt',
type: 'timestamp',
isNullable: true,
}),
);
await queryRunner.query('UPDATE `user` SET `createdAt` = NULL WHERE `createdAt` IS NOT NULL');
await queryRunner.changeColumn(
'user',
'createdAt',
new TableColumn({
name: 'createdAt',
type: 'timestamp',
isNullable: false,
default: 'CURRENT_TIMESTAMP',
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('student', 'createdAt');
await queryRunner.dropColumn('user', 'createdAt');
}
}
12 changes: 12 additions & 0 deletions server/stats/villageStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,21 @@ export const getFamiliesWithoutAccount = async (villageId: number) => {
'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',
])
.getRawMany();

return familiesWithoutAccount;
};

export const getFloatingAccounts = async (villageId: number) => {
const floatingAccounts = await userRepository
.createQueryBuilder('user')
.where('user.villageId = :villageId', { villageId })
.andWhere('user.hasStudentLinked = 0')
.andWhere('user.type = 4')
.select(['user.id', 'user.firstname', 'user.lastname', 'user.language', 'user.email', 'user.createdAt'])
.getMany();
return floatingAccounts;
};
30 changes: 17 additions & 13 deletions src/components/admin/OneVillageTable.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react';

import RemoveRedEyeIcon from '@mui/icons-material/RemoveRedEye';
import { Paper, TableContainer } from '@mui/material';
import { Box, Paper, TableContainer, TableSortLabel, useTheme } from '@mui/material';
import NoSsr from '@mui/material/NoSsr';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TablePagination from '@mui/material/TablePagination';
import TableRow from '@mui/material/TableRow';
import TableSortLabel from '@mui/material/TableSortLabel';
import { useTheme } from '@mui/material/styles';

import { primaryColorLight } from 'src/styles/variables.const';

function paginate<T>(array: T[], pageSize: number, pageNumber: number): T[] {
// human-readable page numbers usually start with 1, so we reduce 1 in the first argument
Expand All @@ -37,7 +37,7 @@ interface OneVillageTableProps {
export const OneVillageTable = ({ 'aria-label': ariaLabel, emptyPlaceholder, admin, data, columns, actions, titleContent }: OneVillageTableProps) => {
const theme = useTheme();
const color = admin ? 'white' : 'black';
const backgroundColor = admin ? theme.palette.secondary.main : 'white';
const backgroundColor = admin ? theme.palette.secondary.main : primaryColorLight;
const [options, setTableOptions] = React.useState<TableOptions>({
page: 1,
limit: 10,
Expand Down Expand Up @@ -70,7 +70,12 @@ export const OneVillageTable = ({ 'aria-label': ariaLabel, emptyPlaceholder, adm

return (
<NoSsr>
<TableContainer component={Paper}>
<TableContainer component={Paper} sx={{ mb: '1rem' }}>
{titleContent && (
<Box sx={{ fontWeight: 'bold', display: 'flex', border: 'none', backgroundColor, p: '8px' }}>
<RemoveRedEyeIcon sx={{ mr: '6px' }} /> {titleContent}
</Box>
)}
<Table size="medium" aria-label={ariaLabel}>
{data.length === 0 ? (
<TableBody>
Expand All @@ -84,15 +89,14 @@ export const OneVillageTable = ({ 'aria-label': ariaLabel, emptyPlaceholder, adm
<>
<TableHead
style={{ borderBottom: '1px solid white' }}
sx={{ fontWeight: 'bold', minHeight: 'unset', padding: '8px 8px 8px 16px', color, backgroundColor }}
sx={{
fontWeight: 'bold',
minHeight: 'unset',
padding: '8px 8px 8px 16px',
color,
backgroundColor: admin ? backgroundColor : 'white',
}}
>
{titleContent && (
<TableRow>
<TableCell sx={{ fontWeight: 'bold', display: 'flex', border: 'none' }}>
<RemoveRedEyeIcon sx={{ mr: '6px' }} /> {titleContent}
</TableCell>
</TableRow>
)}
<TableRow>
{columns.map((c) => (
<TableCell key={c.key} style={{ color, fontWeight: 'bold' }}>
Expand Down
40 changes: 17 additions & 23 deletions src/components/admin/dashboard-statistics/VillageStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import styles from './styles/charts.module.css';
import { useGetVillagesStats } from 'src/api/statistics/statistics.get';
import { useCountries } from 'src/services/useCountries';
import { useVillages } from 'src/services/useVillages';
import { formatDate } from 'src/utils';
import type { FamiliesWithoutAccount, OneVillageTableRow } from 'types/statistics.type';
import type { VillageFilter } from 'types/village.type';

const VillageStats = () => {
Expand All @@ -22,6 +24,7 @@ const VillageStats = () => {
const [options, setOptions] = useState<VillageFilter>({ countryIsoCode: '' });

const pelicoMessage = 'Merci de sélectionner un village-monde pour analyser ses statistiques ';
const noDataFoundMessage = 'Pas de données pour le Village-Monde sélectionné';

const { countries } = useCountries();

Expand All @@ -33,13 +36,13 @@ const VillageStats = () => {
});
}, [selectedCountry]);

const [rows, setRows] = React.useState<Array<{ id: string | number; [key: string]: string | boolean | number | React.ReactNode }>>([]);
const [familiesWithoutAccountRows, setFamiliesWithoutAccountRows] = React.useState<Array<OneVillageTableRow>>([]);
React.useEffect(() => {
if (villagesStats.data?.familiesWithoutAccount) {
setRows([]);
setRows(createRows(villagesStats.data?.familiesWithoutAccount));
setFamiliesWithoutAccountRows([]);
setFamiliesWithoutAccountRows(createFamiliesWithoutAccountRows(villagesStats.data?.familiesWithoutAccount));
}
}, [villagesStats.data?.familiesWithoutAccount]);
}, [villagesStats.data?.familiesWithoutAccount, villagesStats.data?.floatingAccounts]);

const handleCountryChange = (country: string) => {
setSelectedCountry(country);
Expand Down Expand Up @@ -86,24 +89,15 @@ const VillageStats = () => {
</div>
);
}
function createRows(
data: Array<{
student_id: string | number;
student_firstname: string;
student_lastname: string;
village_name: string;
classroom_name: string;
classroom_country: string;
}>,
): Array<{ id: string | number; [key: string]: string | boolean | number | React.ReactNode }> {
function createFamiliesWithoutAccountRows(data: Array<FamiliesWithoutAccount>): Array<OneVillageTableRow> {
return data.map((row) => {
return {
id: row.student_id, // id is string | number
student: `${row.student_firstname} ${row.student_lastname}`, // string
vm: row.village_name, // string
classroom: row.classroom_name, // string
country: row.classroom_country, // string
creationDate: 'À venir', // string
id: row.student_id,
student: `${row.student_firstname} ${row.student_lastname}`,
vm: row.village_name,
classroom: row.classroom_name,
country: row.classroom_country,
creationDate: row.student_creation_date ? formatDate(row.student_creation_date) : 'Donnée non disponible',
};
});
}
Expand Down Expand Up @@ -141,10 +135,10 @@ const VillageStats = () => {
<>
<OneVillageTable
admin={false}
emptyPlaceholder={<p>{pelicoMessage}</p>}
data={rows}
emptyPlaceholder={<p>{noDataFoundMessage}</p>}
data={familiesWithoutAccountRows}
columns={FamiliesWithoutAccountHeaders}
titleContent={`À surveiller : comptes non créés (${rows.length})`}
titleContent={`À surveiller : comptes non créés (${familiesWithoutAccountRows.length})`}
/>
<Box
className={styles.classroomStats}
Expand Down
9 changes: 9 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ export function countryToFlag(isoCode: string): string {
: isoCode;
}

export function formatDate(isoString: string): string {
const date = new Date(isoString);
const day = date.getUTCDate().toString().padStart(2, '0');
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
const year = date.getUTCFullYear();
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
}

/**
* Returns a random token. Browser only!
* @param length length of the returned token.
Expand Down
16 changes: 16 additions & 0 deletions types/statistics.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,29 @@ export interface VillageStats {
familyAccountsCount: number;
connectedFamiliesCount: number;
familiesWithoutAccount: FamiliesWithoutAccount[];
floatingAccounts: FloatingAccount[];
}

export interface FamiliesWithoutAccount {
student_id: number;
student_firstname: string;
student_lastname: string;
student_creation_date: string | null;
village_name: string;
classroom_name: string;
classroom_country: string;
}

export interface FloatingAccount {
id: number;
email: string;
firstname: string;
lastname: string;
language: string;
createdAt: string | null;
}

export interface OneVillageTableRow {
id: string | number;
[key: string]: string | boolean | number | React.ReactNode;
}

0 comments on commit 96ef588

Please sign in to comment.