Skip to content

Commit

Permalink
Add classroom tab in statistics dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjyhy committed Nov 26, 2024
1 parent c2ba9e4 commit aa721c3
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 16 deletions.
21 changes: 21 additions & 0 deletions server/controllers/classroom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
86 changes: 73 additions & 13 deletions src/components/admin/dashboard-statistics/ClassroomStats.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
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 = () => {
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 [villageFilter, setVillageFilter] = useState<VillageFilter>({ countryIsoCode: '' });
const [classroomFilter, setClassroomFilter] = useState<ClassroomFilter>({ 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);
Expand All @@ -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<Array<OneVillageTableRow>>([]);
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);
};
Expand All @@ -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 (
<>
<div className={styles.filtersContainer}>
<Box className={styles.filtersContainer}>
<div className={styles.phaseFilter}>
<PhaseDropdown onPhaseChange={handlePhaseChange} />
</div>
Expand All @@ -68,8 +93,43 @@ const ClassroomStats = () => {
<div className={styles.countryFilter}>
<ClassroomDropdown classrooms={classrooms} onClassroomChange={handleClassroomChange} />
</div>
</div>
{!selectedClassroom ? <PelicoCard message={pelicoMessage} /> : null}
</Box>
<Tabs value={value} onChange={handleTabChange} aria-label="basic tabs example" sx={{ py: 3 }}>
<Tab label="En classe" />
<Tab label="En famille" />
</Tabs>
<TabPanel value={value} index={0}>
<p>Statistiques - En classe</p>
</TabPanel>
<TabPanel value={value} index={1}>
{!selectedClassroom ? (
<PelicoCard message={pelicoMessage} />
) : (
<>
<OneVillageTable
admin={false}
emptyPlaceholder={<p>{noDataFoundMessage}</p>}
data={familiesWithoutAccountRows}
columns={FamiliesWithoutAccountHeaders}
titleContent={`À surveiller : comptes non créés (${familiesWithoutAccountRows.length})`}
/>
<Box
className={styles.classroomStats}
sx={{
display: 'flex',
flexDirection: {
xs: 'column',
md: 'row',
},
gap: 2,
}}
>
<StatsCard data={villagesStats.data?.childrenCodesCount}>Nombre de codes enfant créés</StatsCard>
<StatsCard data={villagesStats.data?.connectedFamiliesCount}>Nombre de familles connectées</StatsCard>
</Box>
</>
)}
</TabPanel>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -27,8 +29,8 @@ export default function ClassroomDropdown({ classrooms, onClassroomChange }: Cla
<InputLabel id="classroom-menu-select">Classe</InputLabel>
<Select labelId="classroom-menu-select" id="classroom-select" value={classroom} label="Classe" onChange={handleChange}>
{classrooms.map((classroom) => (
<MenuItem key={classroom} value={classroom}>
{classroom}
<MenuItem key={classroom.id} value={classroom.id}>
{classroom.name}
</MenuItem>
))}
</Select>
Expand Down
50 changes: 50 additions & 0 deletions src/services/useClassrooms.ts
Original file line number Diff line number Diff line change
@@ -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<Classroom[]> = 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<Classroom[], unknown>(
['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,
};
};
4 changes: 4 additions & 0 deletions types/classroom.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ export interface InitialStateOptionsProps {
ownClass: StateOptions;
ownClassTimeDelay: StateOptions;
}

export interface ClassroomFilter {
villageId: string;
}

0 comments on commit aa721c3

Please sign in to comment.