diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 0aa408ca1..d4c30efe5 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts @@ -14,6 +14,7 @@ import { languageController } from './languages'; import { mediathequeController } from './mediatheque'; import { notificationsController } from './notifications'; import { pelicoController } from './pelicoPresentation'; +import { phaseHistoryController } from './phaseHistory'; import { statisticsController } from './statistics'; import { storyController } from './story'; import { studentController } from './student'; @@ -51,6 +52,7 @@ const controllers = [ pelicoController, mediathequeController, notificationsController, + phaseHistoryController, ]; for (let i = 0, n = controllers.length; i < n; i++) { diff --git a/server/controllers/phaseHistory.ts b/server/controllers/phaseHistory.ts new file mode 100644 index 000000000..42c7d64f2 --- /dev/null +++ b/server/controllers/phaseHistory.ts @@ -0,0 +1,47 @@ +import { PhaseHistory } from '../entities/phaseHistory'; +import { Village } from '../entities/village'; +import { AppDataSource } from '../utils/data-source'; +import { Controller } from './controller'; + +export const phaseHistoryController = new Controller('/phase-history'); + +phaseHistoryController.post({ path: '/' }, async (_req, res) => { + const data = _req.body; + try { + //Find village wit ha given id + const villageRepository = AppDataSource.getRepository(Village); + const village = await villageRepository.findOne({ where: { id: data.villageId } }); + if (!village) throw new Error('Village Not found'); + + // Create a PhaseHistory instance and fill it with data + const phaseHistory = new PhaseHistory(); + phaseHistory.village = village; + phaseHistory.phase = data.phase; + + // 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); + console.log('SOFT DELETE'); + console.log(villageId, phase); + 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); + } +}); diff --git a/server/entities/phaseHistory.ts b/server/entities/phaseHistory.ts new file mode 100644 index 000000000..134977b30 --- /dev/null +++ b/server/entities/phaseHistory.ts @@ -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(['village', 'phase'], { unique: true }) +export class PhaseHistory { + @PrimaryGeneratedColumn() + public id: number; + + @ManyToOne(() => Village, (village) => village.phaseHistories) + village: Village; + + @Column({ + type: 'tinyint', + }) + phase: VillagePhase; + + @CreateDateColumn() + public startingOn: Date; + + @DeleteDateColumn() + public endingOn: Date; +} diff --git a/server/entities/village.ts b/server/entities/village.ts index 9081e9f51..9d8b8a487 100644 --- a/server/entities/village.ts +++ b/server/entities/village.ts @@ -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 }; @@ -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; diff --git a/server/utils/data-source.ts b/server/utils/data-source.ts index eae2455cc..9969bec5b 100644 --- a/server/utils/data-source.ts +++ b/server/utils/data-source.ts @@ -38,7 +38,7 @@ function getAppDataSource(): DataSource { entities: [path.join(__dirname, '../entities/*.js')], migrations: [path.join(__dirname, '../migrations/*.js')], subscribers: [], - synchronize: false, + synchronize: true, ...connectionOptions, }); } diff --git a/src/api/phaseHistory/phaseHistory.delete.ts b/src/api/phaseHistory/phaseHistory.delete.ts new file mode 100644 index 000000000..ae33d9fa0 --- /dev/null +++ b/src/api/phaseHistory/phaseHistory.delete.ts @@ -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 { + const response = await axiosRequest({ + 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."); + } + // inviladate le cache de la liste des jeux + return response.data; +} diff --git a/src/api/phaseHistory/phaseHistory.post.ts b/src/api/phaseHistory/phaseHistory.post.ts new file mode 100644 index 000000000..115474646 --- /dev/null +++ b/src/api/phaseHistory/phaseHistory.post.ts @@ -0,0 +1,16 @@ +import type { PhaseHistory } from 'server/entities/phaseHistory'; + +import { axiosRequest } from 'src/utils/axiosRequest'; + +export async function postPhaseHistory(data: Partial & { villageId: number }): Promise { + const response = await axiosRequest({ + 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."); + } + // inviladate le cache de la liste des jeux + return response.data; +} diff --git a/src/components/admin/manage/settings/SavePhasesModal.tsx b/src/components/admin/manage/settings/SavePhasesModal.tsx index 04b942d22..955c318b9 100644 --- a/src/components/admin/manage/settings/SavePhasesModal.tsx +++ b/src/components/admin/manage/settings/SavePhasesModal.tsx @@ -1,17 +1,20 @@ import { useSnackbar } from 'notistack'; import React, { useState, useEffect } from 'react'; +import { softDeletePhaseHistory } from 'src/api/phaseHistory/phaseHistory.delete'; +import { postPhaseHistory } from 'src/api/phaseHistory/phaseHistory.post'; import { useUpdateVillages } from 'src/api/villages/villages.put'; import { Modal } from 'src/components/Modal'; import type { VillagePhase } from 'types/village.type'; interface SavePhasesModalProps { villagePhases: { [villageId: number]: VillagePhase }; + goToNextStep: { [villageId: number]: boolean }; isModalOpen: boolean; setIsModalOpen: (val: boolean) => void; } -export function SavePhasesModal({ villagePhases, isModalOpen, setIsModalOpen }: SavePhasesModalProps) { +export function SavePhasesModal({ villagePhases, goToNextStep, isModalOpen, setIsModalOpen }: SavePhasesModalProps) { const [isModalLoading, setIsModalLoading] = useState(false); const updateVillages = useUpdateVillages(); const { enqueueSnackbar } = useSnackbar(); @@ -20,9 +23,28 @@ export function SavePhasesModal({ villagePhases, isModalOpen, setIsModalOpen }: const promises = []; for (const key in villagePhases) { const villageId: number = +key; - promises.push(updateVillages.mutateAsync({ id: villageId, villageData: { activePhase: villagePhases[villageId] } })); + if (goToNextStep[villageId]) { + const updatedPhase = Math.min(villagePhases[villageId] + 1, 3); + promises.push( + updateVillages.mutateAsync({ + id: villageId, + villageData: { activePhase: updatedPhase }, + }), + ); + promises.push( + postPhaseHistory({ + villageId, + phase: updatedPhase, + }), + ); + promises.push(softDeletePhaseHistory(villageId, updatedPhase - 1)); + } + } + try { + await Promise.allSettled(promises); + } catch (e) { + console.error(e); } - await Promise.allSettled(promises); }; useEffect(() => { @@ -63,7 +85,10 @@ export function SavePhasesModal({ villagePhases, isModalOpen, setIsModalOpen }: ariaDescribedBy="Modal de validation des phases" >
-

Les modifications que tu souhaites apporter vont modifier les phases actives.

+

+ Les modifications que tu souhaites apporter vont modifier les phases actives.
+ Attention, faire passer un village à l'étape suivante est un choix définitif. +

); diff --git a/src/pages/admin/newportal/manage/settings/phases/index.tsx b/src/pages/admin/newportal/manage/settings/phases/index.tsx index 58259eb81..47b48c42e 100644 --- a/src/pages/admin/newportal/manage/settings/phases/index.tsx +++ b/src/pages/admin/newportal/manage/settings/phases/index.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import React, { useState, useEffect } from 'react'; -import { Button, Checkbox, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; +import { Button, Checkbox, Paper, Step, StepLabel, Stepper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; import { useGetVillages } from 'src/api/villages/villages.get'; import { SavePhasesModal } from 'src/components/admin/manage/settings/SavePhasesModal'; @@ -15,17 +15,23 @@ import { VillagePhase } from 'types/village.type'; const Phases = () => { const { user } = React.useContext(UserContext); const hasAccess = user !== null && user.type in [UserType.MEDIATOR, UserType.ADMIN, UserType.SUPER_ADMIN]; - const [villagePhases, setVillagePhases] = useState<{ [villageId: number]: VillagePhase }>({}); const villages = useGetVillages(); + const [villagePhases, setVillagePhases] = useState<{ [villageId: number]: VillagePhase }>({}); + const [goToNextStep, setGoToNextStep] = useState<{ [villageId: number]: boolean }>({}); const [isModalOpen, setIsModalOpen] = useState(false); - + const steps = ['Discover', 'Exchange', 'Imagine']; useEffect(() => { if (villages.data) { - const newVillagePhases: { [villageId: number]: VillagePhase } = {}; villages.data.forEach((village: Village) => { - newVillagePhases[village.id] = village.activePhase; + setVillagePhases((prevState) => ({ + ...prevState, + [village.id]: village.activePhase, + })); + setGoToNextStep({ + [village.id]: false, + }); }); - setVillagePhases(newVillagePhases); + setGoToNextStep(villages.data.reduce((acc, village) => ({ ...acc, [village.id]: false }), {})); } }, [villages.data]); @@ -35,29 +41,19 @@ const Phases = () => { if (villages.isError) return

Error!

; if (villages.isLoading || villages.isIdle) return

Loading...

; - const handleCheckboxChange = (villageId: number, phase: VillagePhase, checked: boolean) => { - if (phase === VillagePhase.EXCHANGE || phase === VillagePhase.IMAGINE) { - setVillagePhases((prevState) => ({ - ...prevState, - [villageId]: checked ? phase : phase - 1, - })); - } else { - setVillagePhases((prevState) => ({ - ...prevState, - [villageId]: 1, - })); - } + const handleCheckboxChange = (villageId: number) => { + setGoToNextStep((prevState) => ({ + ...prevState, + [villageId]: !prevState[villageId], + })); }; - const handleHeaderCheckboxChange = (phase: VillagePhase, checked: boolean) => { + const handleHeaderCheckboxChange = (checked: boolean) => { for (const key in villagePhases) { - if (villagePhases[key] <= phase) { - villagePhases[key] = VillagePhase.DISCOVER; - setVillagePhases((prevState) => ({ - ...prevState, - [key]: checked ? phase : phase - 1, - })); - } + setGoToNextStep((prevState) => ({ + ...prevState, + [key]: checked, + })); } }; @@ -74,10 +70,10 @@ const Phases = () => {
- - +
@@ -108,40 +104,36 @@ const Phases = () => { }} > - Village-Monde - Phase 1 - + phase >= VillagePhase.EXCHANGE)} - onChange={(event: React.ChangeEvent) => - handleHeaderCheckboxChange(VillagePhase.EXCHANGE, event.target.checked) - } + checked={Object.keys(goToNextStep).every((key) => goToNextStep[+key])} + onChange={(event: React.ChangeEvent) => handleHeaderCheckboxChange(event.target.checked)} /> - Phase 2 - - - phase === VillagePhase.IMAGINE)} - onChange={(event: React.ChangeEvent) => - handleHeaderCheckboxChange(VillagePhase.IMAGINE, event.target.checked) - } - /> - Phase 3 + Village-Monde + Phases {villages.data.map((village: Village) => ( + + handleCheckboxChange(village.id)} + /> + {village.name} - {[1, 2, 3].map((phase) => ( - - = phase ? true : false} - onChange={(event: React.ChangeEvent) => handleCheckboxChange(village.id, phase, event.target.checked)} - /> - - ))} + + + {steps.map((label) => ( + + {label} + + ))} + + ))} diff --git a/src/services/useVillages.ts b/src/services/useVillages.ts index af0fde204..203cb7202 100644 --- a/src/services/useVillages.ts +++ b/src/services/useVillages.ts @@ -3,8 +3,10 @@ import React from 'react'; import type { QueryFunction } from 'react-query'; import { useQueryClient, useQuery } from 'react-query'; +import { postPhaseHistory } from 'src/api/phaseHistory/phaseHistory.post'; import { axiosRequest } from 'src/utils/axiosRequest'; import type { Village, VillageFilter } from 'types/village.type'; +import { VillagePhase } from 'types/village.type'; export const useVillages = (options?: VillageFilter): { villages: Village[]; setVillages(newVillages: Village[]): void } => { const queryClient = useQueryClient(); @@ -65,6 +67,10 @@ export const useVillageRequests = () => { countries: newVillage.countries.map((c) => c.isoCode), }, }); + await postPhaseHistory({ + villageId: response.data.id, + phase: VillagePhase.DISCOVER, + }); if (response.error) { enqueueSnackbar('Une erreur est survenue...', { variant: 'error',