diff --git a/next.config.js b/next.config.js index a9b57e513..d2fa5073c 100644 --- a/next.config.js +++ b/next.config.js @@ -12,6 +12,14 @@ module.exports = { }); return config; }, + webpackDevMiddleware: (config) => { + // Enable hot reloading + config.watchOptions = { + poll: 1000, + aggregateTimeout: 300, + }; + return config; + }, experimental: { esmExternals: false }, eslint: { // ESLint is already called before building with nextJS. So no need here. diff --git a/server/controllers/classroom.ts b/server/controllers/classroom.ts index 6ab290520..e78a00193 100644 --- a/server/controllers/classroom.ts +++ b/server/controllers/classroom.ts @@ -75,6 +75,7 @@ classroomController.post({ path: '', userType: UserType.TEACHER }, async (req: R if (!createClassroomValidator(data)) { sendInvalidDataError(createClassroomValidator); } + if (!req.user) { throw new AppError('Forbidden', ErrorCode.UNKNOWN); } diff --git a/server/controllers/user.ts b/server/controllers/user.ts index baa4aa950..a912beee8 100644 --- a/server/controllers/user.ts +++ b/server/controllers/user.ts @@ -824,17 +824,36 @@ userController.delete({ path: '/:userId/linked-students/:studentId' }, async (re }); // Get the visibility parameters for Family members -userController.get({ path: '/visibility-params', userType: UserType.FAMILY }, async (req: Request, res: Response) => { +userController.get({ path: '/:id/visibility-params/', userType: UserType.FAMILY }, async (req: Request, res: Response) => { if (!req.user) { throw new AppError('Forbidden', ErrorCode.UNKNOWN); } - const visibilityParams = await AppDataSource.getRepository(UserToStudent) - .createQueryBuilder('userStudent') - .innerJoinAndSelect('userStudent.student', 'student') - .innerJoinAndSelect('student.classroom', 'classroom') - .where('userStudent.user = :familyId', { familyId: req.user.id }) - .getRawMany(); //* Here it's getRawMany because for some reason we lost 2 attributes otherwise classroom.userId and classroom.villageId - res.json(visibilityParams); + const id = parseInt(req.params.id, 10) || 0; + const user = await AppDataSource.getRepository(User).findOne({ where: { id }, relations: ['userToStudents', 'userToStudents.student'] }); + + if (user && user.type === UserType.TEACHER) { + const teacherClassroom = await AppDataSource.getRepository(Classroom).findOne({ + where: { user: { id: user.id } }, + }); + return res.json(teacherClassroom); + } + + if (user && user.type === UserType.FAMILY) { + const classrooms = []; + + for (const student of user.userToStudents) { + const classroom = await AppDataSource.getRepository(Classroom).findOne({ + where: { students: { id: student.student.id } }, + }); + if (classroom) { + classrooms.push(classroom); + } + } + + return res.json(classrooms); + } + + return res.json([]); }); userController.get({ path: '/get-student-vp', userType: UserType.FAMILY }, async (req: Request, res: Response) => { diff --git a/src/api/user/user.get.ts b/src/api/user/user.get.ts index d98dfcc98..f9496c8c2 100644 --- a/src/api/user/user.get.ts +++ b/src/api/user/user.get.ts @@ -1,5 +1,7 @@ import { axiosRequest } from 'src/utils/axiosRequest'; import type { Student } from 'types/student.type'; +import type { User } from 'types/user.type'; +import { UserType } from 'types/user.type'; export const getUsers = async () => { const response = await axiosRequest({ @@ -25,3 +27,15 @@ export const getLinkedStudentsToUser = async (userId: number) => { } }; +export const getUserVisibilityFamilyParams = async (user: User) => { + if (user.type === UserType.FAMILY || user.type === UserType.TEACHER) { + const response = await axiosRequest({ + method: 'GET', + url: `/users/${user.id}/visibility-params`, + }); + if (response.error) return null; + console.log('User visibility params: ', response.data); + return response.data; + } + return []; +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index aee363a0e..3f2a1897d 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,16 +1,16 @@ // import SearchIcon from '@mui/icons-material/Search'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React, { useEffect } from 'react'; + import SettingsIcon from '@mui/icons-material/Settings'; import { Button, FormControl, InputLabel, Select } from '@mui/material'; import IconButton from '@mui/material/IconButton'; // import InputBase from '@mui/material/InputBase'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import React, { useEffect } from 'react'; -import AccessControl from './AccessControl'; import { getClassroomOfStudent } from 'src/api/student/student.get'; import { getLinkedStudentsToUser } from 'src/api/user/user.get'; import { updateUser } from 'src/api/user/user.put'; @@ -32,7 +32,7 @@ export const Header = () => { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); - + //* NOTE: might be interesting to make a hook for this below const isPelico = (user !== null && user.type === UserType.MEDIATOR) || @@ -85,12 +85,12 @@ export const Header = () => { setUser({ ...user, country: { isoCode: classroomOfStudent?.country?.isoCode, name: '' } }); // updateUser(user?.id, {country: }); - } catch (err) { } + } catch (err) {} } setIsModalOpen(false); }; - useEffect(()=>{ + useEffect(() => { console.log('user ===', user); }, [user]); @@ -187,9 +187,7 @@ export const Header = () => { > goToPage('/mon-compte')}>Mon compte {user.type !== UserType.FAMILY && goToPage('/mes-videos')}>Mes vidéos} - - {user.type === UserType.TEACHER ? goToPage('/familles/1')}>Mes familles : null}{' '} - + {user.type === UserType.TEACHER ? goToPage('/familles/1')}>Mes familles : null}{' '} Se déconnecter @@ -206,8 +204,8 @@ export const Header = () => { {selectedStudent ? `${selectedStudent.firstname} ${selectedStudent.lastname}` : linkedStudents.length > 0 - ? `${linkedStudents[0].firstname} ${linkedStudents[0].lastname}` - : 'Eleve non selectionne'} + ? `${linkedStudents[0].firstname} ${linkedStudents[0].lastname}` + : 'Eleve non selectionne'} - )) - .reverse()} - - )} - + {/* Main */} +
+

Choisissez ce que voient les familles

+

+ Vous allez inviter les familles de vos élèves à se connecter à 1Village. Ainsi, elles pourront observer les échanges qui ont lieu en + ligne. Vous avez la possibilité de définir ce que les familles voient sur la plateforme. Choisissez parmi ces 4 options : +

+ + + } + label="les familles peuvent voir toutes les activités publiées sur 1Village, dès leur publication" + onFocus={() => handleSelectionVisibility('default')} + style={radioValue !== 'default' ? { color: '#CCC' } : {}} + /> + } + label={ + handleDaysDelay('timeDelay', event)} + onBlur={() => handleSelectionVisibility('timeDelay')} + value={state.timeDelay.delayedDays} + disabled={radioValue !== 'timeDelay'} + /> + } + onClick={() => toggleInput('timeDelay', false)} + disabled={isDisabled?.timeDelay} + style={radioValue !== 'timeDelay' ? { color: '#CCC' } : {}} + /> + } + label="les familles peuvent voir toutes les activités publiées sur 1Village, dès leur publication, mais seulement celles publiées par notre classe" + onFocus={() => handleSelectionVisibility('ownClass')} + style={radioValue !== 'ownClass' ? { color: '#CCC' } : {}} + /> + } + label={ + handleDaysDelay('ownClassTimeDelay', event)} + onBlur={() => handleSelectionVisibility('ownClassTimeDelay')} + value={state.ownClassTimeDelay.delayedDays} + disabled={radioValue !== 'ownClassTimeDelay'} + /> + } + onClick={() => toggleInput('ownClassTimeDelay', false)} + disabled={isDisabled?.ownClassTimeDelay} + style={radioValue !== 'ownClassTimeDelay' ? { color: '#CCC' } : {}} + /> + + +
+
+ + {/* Activity Container */} +

+ Indépendamment de ce réglage, vous pouvez réglez individuellement la visibilité des activités déjà publiées en ligne. +

+ {/* phase is set to 4 to match the array with ALL activities */} + + + {isLoading ? ( +
+ +

Loading activities...

+
+ ) : ( + <> + {sortedActivities + .map((activity) => ( + + )) + .reverse()} + + )} +
- + ); }; diff --git a/src/pages/familles/2.tsx b/src/pages/familles/2.tsx index 4e944c3a6..4c7aa76bf 100644 --- a/src/pages/familles/2.tsx +++ b/src/pages/familles/2.tsx @@ -1,14 +1,14 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck +import { useRouter } from 'next/router'; +import React, { useState, useEffect } from 'react'; + import ModeEditOutlineRoundedIcon from '@mui/icons-material/ModeEditOutlineRounded'; import { TextField } from '@mui/material'; import Button from '@mui/material/Button'; -import { useRouter } from 'next/router'; -import React, { useState, useEffect } from 'react'; import { editStudent } from 'src/api/classroom/student.put'; -import AccessControl from 'src/components/AccessControl'; import { Base } from 'src/components/Base'; import { Modal } from 'src/components/Modal'; import { Steps } from 'src/components/Steps'; @@ -203,168 +203,166 @@ const ClassroomParamStep2 = () => { return ( - -
- -
-

Ajouter un identifiant par élève

-

- Pour sécuriser la connexion des familles, nous allons créer{' '} - un identifiant unique à chaque élève de votre classe.

Ensuite chaque famille - pourra créer jusqu'à 5 accès avec ce même identifiant unique: ainsi les parents divorcés, les grands-parents, les grands-frères ou - les grandes-soeurs pourront accéder à 1Village.

-

Vous devez donc créer autant d'identifiants qu'il y a d'élèves dans votre classe. Vous pourrez rajouter des - identifiants en cours d'années, lorsqu'un nouvel élève arrive dans votre classe. -

- -
- - - -
- {isDuplicateModalOn && ( - { - setIsDuplicateModalOn(false); +
+ +
+

Ajouter un identifiant par élève

+

+ Pour sécuriser la connexion des familles, nous allons créer{' '} + un identifiant unique à chaque élève de votre classe.

Ensuite chaque famille pourra + créer jusqu'à 5 accès avec ce même identifiant unique: ainsi les parents divorcés, les grands-parents, les grands-frères ou les + grandes-soeurs pourront accéder à 1Village.

+

Vous devez donc créer autant d'identifiants qu'il y a d'élèves dans votre classe. Vous pourrez rajouter des + identifiants en cours d'années, lorsqu'un nouvel élève arrive dans votre classe. +

+ +
+ +
); }; diff --git a/src/pages/familles/3.tsx b/src/pages/familles/3.tsx index bf53d0081..58c0bb4e2 100644 --- a/src/pages/familles/3.tsx +++ b/src/pages/familles/3.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useState } from 'react'; import { Box, Button } from '@mui/material'; -import AccessControl from 'src/components/AccessControl'; import { Base } from 'src/components/Base'; import { Modal } from 'src/components/Modal'; import { Steps } from 'src/components/Steps'; @@ -115,56 +114,54 @@ const ClassroomParamStep3 = () => { return ( - -
- + +
+

Communiquer les identifiants aux familles

+

+ Pour inviter les familles à se connecter, nous avons préparé un texte de présentation, que vous pouvez modifier, ou traduire dans une + autre langue. +

+

+ Comme vous pourrez le constater, ce texte contient le mot-variable %identifiant : vous devez + le laisser sous ce format.{' '} +

+

+ Ainsi, vous pourrez générer autant de textes de présentation que d’élèves dans votre classe : à vous ensuite de les imprimer et les + transmettre aux familles. Dans chaque texte, le mot-variable %identifiant aura été remplacé + automatiquement par l’identifiant unique généré à l’étape précédente. +

+ { + setTextValue(value); + }} /> -
-

Communiquer les identifiants aux familles

-

- Pour inviter les familles à se connecter, nous avons préparé un texte de présentation, que vous pouvez modifier, ou traduire dans une - autre langue. -

-

- Comme vous pourrez le constater, ce texte contient le mot-variable %identifiant : vous devez - le laisser sous ce format.{' '} -

-

- Ainsi, vous pourrez générer autant de textes de présentation que d’élèves dans votre classe : à vous ensuite de les imprimer et les - transmettre aux familles. Dans chaque texte, le mot-variable %identifiant aura été remplacé - automatiquement par l’identifiant unique généré à l’étape précédente. -

- { - setTextValue(value); - }} - /> - - - - setIsModalOpen(false)} - ariaDescribedBy={'activate-phase-desc'} - ariaLabelledBy={'activate-phase'} - noTitle - confirmLabel="confirmer" - > -
Votre message doit contenir l'identifiant enfant suivant: %identifiant
-
- -
+ + + + setIsModalOpen(false)} + ariaDescribedBy={'activate-phase-desc'} + ariaLabelledBy={'activate-phase'} + noTitle + confirmLabel="confirmer" + > +
Votre message doit contenir l'identifiant enfant suivant: %identifiant
+
+
- +
); }; diff --git a/src/services/useActivities.ts b/src/services/useActivities.ts index db14cbb04..99ab20d81 100644 --- a/src/services/useActivities.ts +++ b/src/services/useActivities.ts @@ -2,13 +2,12 @@ import React from 'react'; import type { QueryFunction } from 'react-query'; import { useQuery } from 'react-query'; -import { getLinkedStudentsToUser } from 'src/api/user/user.get'; +import { getUserVisibilityFamilyParams } from 'src/api/user/user.get'; import { UserContext } from 'src/contexts/userContext'; import { VillageContext } from 'src/contexts/villageContext'; import { serializeToQueryUrl } from 'src/utils'; import { axiosRequest } from 'src/utils/axiosRequest'; import type { Activity } from 'types/activity.type'; -import type { Student } from 'types/student.type'; import type { UserParamClassroom } from 'types/user.type'; import { UserType } from 'types/user.type'; @@ -22,23 +21,13 @@ export type Args = { status?: number; phase?: number; responseActivityId?: number; - selectedStudent?: Student | null; + selectedStudent?: number; }; export const useActivities = ({ pelico, countries = [], userId, type, ...args }: Args) => { const { village } = React.useContext(VillageContext); const { user, selectedStudent } = React.useContext(UserContext); - const getVisibilityFamilyParams = React.useCallback(async () => { - if (user && user.type !== UserType.FAMILY) return []; - const response = await axiosRequest({ - method: 'GET', - url: '/users/visibility-params', - }); - if (response.error) return null; - return response.data; - }, [user]); - const villageId = village ? village.id : null; const getActivities: QueryFunction = React.useCallback(async () => { @@ -46,19 +35,19 @@ export const useActivities = ({ pelico, countries = [], userId, type, ...args }: return []; } - const students = await getLinkedStudentsToUser(user?.id || 0); - - // console.log('useQuery students', students); + const visibilityFamily = (await getUserVisibilityFamilyParams(user)) as [UserParamClassroom]; - const visibilityFamily = (await getVisibilityFamilyParams()) as [UserParamClassroom]; + console.log('visibilityFamily ===', visibilityFamily); - // console.log('selectedStudent (useActivity)', selectedStudent); + const classroomData = visibilityFamily[selectedStudent || 1]; - const classroomData = visibilityFamily.find((classroom) => classroom.userStudent_studentId === selectedStudent?.id); + console.log('classroomData === ', classroomData); // console.log('data', classroomData); const familyConditions = user.type === UserType.FAMILY && visibilityFamily.length > 0; + console.log('family conditions', familyConditions); + const query: { [key: string]: string | number | boolean | undefined; } = { @@ -89,7 +78,7 @@ export const useActivities = ({ pelico, countries = [], userId, type, ...args }: } return response.data; - }, [user, getVisibilityFamilyParams, args, type, villageId, countries, pelico, userId, selectedStudent]); + }, [user, args, type, villageId, countries, pelico, userId, selectedStudent]); const { data, isLoading, error, refetch } = useQuery( ['activities', { ...args, type, userId, selectedStudent, countries, pelico, villageId }],