Skip to content

Commit

Permalink
Create PhaseHistory entity and rework on the UI for phase managers
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjyhy committed Nov 4, 2024
1 parent 6ece7a3 commit ba4e4de
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 59 deletions.
2 changes: 2 additions & 0 deletions server/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -51,6 +52,7 @@ const controllers = [
pelicoController,
mediathequeController,
notificationsController,
phaseHistoryController,
];

for (let i = 0, n = controllers.length; i < n; i++) {
Expand Down
47 changes: 47 additions & 0 deletions server/controllers/phaseHistory.ts
Original file line number Diff line number Diff line change
@@ -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');

Check failure on line 33 in server/controllers/phaseHistory.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.log(villageId, phase);

Check failure on line 34 in server/controllers/phaseHistory.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
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);
}
});
25 changes: 25 additions & 0 deletions server/entities/phaseHistory.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions server/entities/village.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion server/utils/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Expand Down
15 changes: 15 additions & 0 deletions src/api/phaseHistory/phaseHistory.delete.ts
Original file line number Diff line number Diff line change
@@ -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<PhaseHistory> {
const response = await axiosRequest<PhaseHistory>({
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;
}
16 changes: 16 additions & 0 deletions src/api/phaseHistory/phaseHistory.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { PhaseHistory } from 'server/entities/phaseHistory';

import { axiosRequest } from 'src/utils/axiosRequest';

export async function postPhaseHistory(data: Partial<PhaseHistory> & { villageId: number }): Promise<PhaseHistory> {
const response = await axiosRequest<PhaseHistory>({
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;
}
33 changes: 29 additions & 4 deletions src/components/admin/manage/settings/SavePhasesModal.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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(() => {
Expand Down Expand Up @@ -63,7 +85,10 @@ export function SavePhasesModal({ villagePhases, isModalOpen, setIsModalOpen }:
ariaDescribedBy="Modal de validation des phases"
>
<div id="brouillon-desc" style={{ padding: '0.5rem' }}>
<p>Les modifications que tu souhaites apporter vont modifier les phases actives.</p>
<p>
Les modifications que tu souhaites apporter vont modifier les phases actives. <br />
Attention, faire passer un village à l&apos;étape suivante est un choix définitif.
</p>
</div>
</Modal>
);
Expand Down
100 changes: 46 additions & 54 deletions src/pages/admin/newportal/manage/settings/phases/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<boolean>(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]);

Expand All @@ -35,29 +41,19 @@ const Phases = () => {
if (villages.isError) return <p>Error!</p>;
if (villages.isLoading || villages.isIdle) return <p>Loading...</p>;

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,
}));
}
};

Expand All @@ -74,10 +70,10 @@ const Phases = () => {
</div>
</Link>
<div style={{ textAlign: 'right', marginTop: 30, marginBottom: 30 }}>
<Button variant="outlined" onClick={handleLogCheckboxStates}>
Enregistrer les paramètres
<Button variant="outlined" disabled={Object.keys(goToNextStep).every((key) => !goToNextStep[+key])} onClick={handleLogCheckboxStates}>
Passer à l&apos;étape suivante
</Button>
<SavePhasesModal villagePhases={villagePhases} isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
<SavePhasesModal villagePhases={villagePhases} goToNextStep={goToNextStep} isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
</div>
<div style={{ overflowX: 'auto' }}>
<Paper>
Expand Down Expand Up @@ -108,40 +104,36 @@ const Phases = () => {
}}
>
<TableRow>
<TableCell align="center">Village-Monde</TableCell>
<TableCell align="left">Phase 1</TableCell>
<TableCell align="left">
<TableCell align="center">
<Checkbox
checked={Object.values(villagePhases).every((phase) => phase >= VillagePhase.EXCHANGE)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
handleHeaderCheckboxChange(VillagePhase.EXCHANGE, event.target.checked)
}
checked={Object.keys(goToNextStep).every((key) => goToNextStep[+key])}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => handleHeaderCheckboxChange(event.target.checked)}
/>
Phase 2
</TableCell>
<TableCell align="left">
<Checkbox
checked={Object.values(villagePhases).every((phase) => phase === VillagePhase.IMAGINE)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
handleHeaderCheckboxChange(VillagePhase.IMAGINE, event.target.checked)
}
/>
Phase 3
</TableCell>
<TableCell align="center">Village-Monde</TableCell>
<TableCell align="center">Phases</TableCell>
</TableRow>
</TableHead>
<TableBody>
{villages.data.map((village: Village) => (
<TableRow key={village.id}>
<TableCell align="center">
<Checkbox
disabled={villagePhases[village.id] == VillagePhase.IMAGINE}
checked={!!goToNextStep[village.id]}
onChange={() => handleCheckboxChange(village.id)}
/>
</TableCell>
<TableCell align="center">{village.name}</TableCell>
{[1, 2, 3].map((phase) => (
<TableCell align="left" key={phase}>
<Checkbox
checked={villagePhases[village.id] >= phase ? true : false}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => handleCheckboxChange(village.id, phase, event.target.checked)}
/>
</TableCell>
))}
<TableCell align="center">
<Stepper activeStep={village.activePhase} alternativeLabel>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
</TableCell>
</TableRow>
))}
</TableBody>
Expand Down
6 changes: 6 additions & 0 deletions src/services/useVillages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit ba4e4de

Please sign in to comment.