Skip to content

Commit

Permalink
Merge pull request #995 from parlemonde/VIL-67
Browse files Browse the repository at this point in the history
Vil 67
  • Loading branch information
nathan7594 authored Oct 14, 2024
2 parents ed913c4 + b8f0c69 commit d8b9922
Show file tree
Hide file tree
Showing 14 changed files with 341 additions and 229 deletions.
3 changes: 2 additions & 1 deletion server/controllers/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
getMinConnections,
getMinDuration,
} from '../stats/sessionStats';
import { getChildrenCodesCount, getFamilyAccountsCount, getConnectedFamiliesCount } from '../stats/villageStats';
import { getChildrenCodesCount, getFamilyAccountsCount, getConnectedFamiliesCount, getFamiliesWithoutAccount } from '../stats/villageStats';
import { Controller } from './controller';

export const statisticsController = new Controller('/statistics');
Expand Down Expand Up @@ -51,5 +51,6 @@ statisticsController.get({ path: '/villages/:villageId' }, async (_req, res) =>
familyAccountsCount: await getFamilyAccountsCount(villageId),
childrenCodesCount: await getChildrenCodesCount(villageId),
connectedFamiliesCount: await getConnectedFamiliesCount(villageId),
familiesWithoutAccount: await getFamiliesWithoutAccount(villageId),
});
});
21 changes: 21 additions & 0 deletions server/stats/villageStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,24 @@ export const getConnectedFamiliesCount = async (villageId: number) => {

return connectedFamiliesCount;
};

export const getFamiliesWithoutAccount = async (villageId: number) => {
const familiesWithoutAccount = await studentRepository
.createQueryBuilder('student')
.innerJoin('student.classroom', 'classroom')
.innerJoin('classroom.user', 'user')
.innerJoin('user.village', 'village')
.where('classroom.villageId = :villageId', { villageId })
.andWhere('student.numLinkedAccount < 1')
.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',
'village.name AS village_name',
])
.getRawMany();

return familiesWithoutAccount;
};
164 changes: 0 additions & 164 deletions src/components/admin/AdminTable.tsx

This file was deleted.

175 changes: 175 additions & 0 deletions src/components/admin/OneVillageTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React from 'react';

import RemoveRedEyeIcon from '@mui/icons-material/RemoveRedEye';
import { Paper, TableContainer } 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';

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
return array.slice((pageNumber - 1) * pageSize, pageNumber * pageSize);
}

export interface TableOptions {
limit?: number;
page?: number;
order?: string;
sort?: 'asc' | 'desc';
search?: string;
}
interface OneVillageTableProps {
'aria-label'?: string;
admin: boolean;
emptyPlaceholder: React.ReactNode | React.ReactNodeArray;
data: Array<{ id: string | number; [key: string]: string | boolean | number | React.ReactNode }>;
columns: Array<{ key: string; label: string; sortable?: boolean }>;
actions?(id: string | number, index: number): React.ReactNode | React.ReactNodeArray;
titleContent?: string;
}

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 [options, setTableOptions] = React.useState<TableOptions>({
page: 1,
limit: 10,
sort: 'asc',
});
const handleChangePage = (_event: unknown, newPage: number) => {
setTableOptions({ ...options, page: newPage + 1 });
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setTableOptions({ ...options, page: 1, limit: parseInt(event.target.value, 10) });
};
const onSortBy = (name: string) => () => {
if (options.order === name) {
setTableOptions({ ...options, page: 1, sort: options.sort === 'asc' ? 'desc' : 'asc' });
} else {
setTableOptions({ ...options, page: 1, order: name });
}
};

const usePagination = options.page !== undefined && options.limit !== undefined;
const displayedData = React.useMemo(() => {
const useSort = options.sort !== undefined && options.order !== undefined;
const sortedData = useSort
? data.sort((a, b) => {
return (a[options.order || 0] || 0) >= (b[options.order || 0] || 0) ? (options.sort === 'asc' ? 1 : -1) : options.sort === 'asc' ? -1 : 1;
})
: data;
return usePagination ? paginate(sortedData, options.limit || 10, options.page || 1) : sortedData;
}, [data, options.sort, options.order, options.limit, options.page, usePagination]);

return (
<NoSsr>
<TableContainer component={Paper}>
<Table size="medium" aria-label={ariaLabel}>
{data.length === 0 ? (
<TableBody>
<TableRow>
<TableCell colSpan={columns.length + (actions ? 1 : 0)} align="center">
{emptyPlaceholder || 'Cette liste est vide !'}
</TableCell>
</TableRow>
</TableBody>
) : (
<>
<TableHead
style={{ borderBottom: '1px solid white' }}
sx={{ fontWeight: 'bold', minHeight: 'unset', padding: '8px 8px 8px 16px', color, backgroundColor }}
>
{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' }}>
{c.sortable ? (
<TableSortLabel active={options.order === c.key} direction={options.sort} onClick={onSortBy(c.key)}>
{c.label}
{options.order === c.label ? (
<span
style={{
border: 0,
clip: 'rect(0 0 0 0)',
height: 1,
margin: -1,
overflow: 'hidden',
padding: 0,
position: 'absolute',
top: 20,
width: 1,
}}
>
{options.sort === 'desc' ? 'sorted descending' : 'sorted ascending'}
</span>
) : null}
</TableSortLabel>
) : (
c.label
)}
</TableCell>
))}
{actions && (
<TableCell style={{ color: 'white', fontWeight: 'bold' }} align="right">
Actions
</TableCell>
)}
</TableRow>
</TableHead>
<TableBody>
{displayedData.map((d, index) => (
<TableRow
key={d.id}
sx={{
backgroundColor: 'white',
'&:nth-of-type(even)': {
backgroundColor: admin ? 'rgb(224 239 232)' : 'white',
},
'&.sortable-ghost': {
opacity: 0,
},
}}
>
{columns.map((c) => {
return <TableCell key={`${d.id}_${c.key}`}>{d[c.key] !== undefined ? d[c.key] : ''}</TableCell>;
})}
{actions && (
<TableCell align="right" padding="none" style={{ minWidth: '96px' }}>
{actions(d.id, index)}
</TableCell>
)}
</TableRow>
))}
{usePagination && (
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
count={data.length}
rowsPerPage={options.limit || 10}
page={(options.page || 1) - 1}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</TableRow>
)}
</TableBody>
</>
)}
</Table>
</TableContainer>
</NoSsr>
);
};
Loading

0 comments on commit d8b9922

Please sign in to comment.