diff --git a/src/components/Admin/DbEdit.tsx b/src/components/Admin/DbEdit.tsx new file mode 100644 index 0000000..e4fbbea --- /dev/null +++ b/src/components/Admin/DbEdit.tsx @@ -0,0 +1,34 @@ +import { FC, memo } from "react" +import { useSelector } from "react-redux" +import withUserConnected from "../../utils/withUserConnected" +import { fetchVolunteerListIfNeed, selectVolunteerList } from "../../store/volunteerList" +import withUserRole from "../../utils/withUserRole" +import ROLES from "../../utils/roles.constants" +import MemberEdit from "./MemberEdit" +import useAction from "../../utils/useAction" +import { fetchVolunteerSetIfNeed } from "../../store/volunteerSet" +import { Volunteer } from "../../services/volunteers" +import styles from "./styles.module.scss" + +const DbEdit: FC = (): JSX.Element => { + const volunteers = useSelector(selectVolunteerList) + const saveVolunteer = useAction(fetchVolunteerSetIfNeed) + if (!volunteers) { + return <>No member found + } + return ( + + ) +} + +export default withUserRole(ROLES.ADMIN, memo(withUserConnected(DbEdit))) + +export const fetchFor = [fetchVolunteerListIfNeed] diff --git a/src/components/Admin/GameDetailsUpdate.tsx b/src/components/Admin/GameDetailsUpdate.tsx new file mode 100644 index 0000000..719fcef --- /dev/null +++ b/src/components/Admin/GameDetailsUpdate.tsx @@ -0,0 +1,31 @@ +import { FC, memo, useCallback } from "react" +import { useSelector } from "react-redux" +import withUserConnected from "../../utils/withUserConnected" +import withUserRole from "../../utils/withUserRole" +import ROLES from "../../utils/roles.constants" +import { fetchGameDetailsUpdate } from "../../store/gameDetailsUpdate" +import { selectUserJwtToken } from "../../store/auth" +import useAction from "../../utils/useAction" +import styles from "./styles.module.scss" +import FormButton from "../Form/FormButton/FormButton" + +const GameDetailsUpdate: FC = (): JSX.Element => { + const jwtToken = useSelector(selectUserJwtToken) + const save = useAction(fetchGameDetailsUpdate) + + const onSubmit = useCallback(async () => { + await save(jwtToken) + }, [save, jwtToken]) + + return ( + <> +
+ Ok, noté +
+ + ) +} + +export default withUserRole(ROLES.ADMIN, memo(withUserConnected(GameDetailsUpdate))) + +export const fetchFor = [fetchGameDetailsUpdate] diff --git a/src/components/Admin/MemberEdit.tsx b/src/components/Admin/MemberEdit.tsx new file mode 100644 index 0000000..ee20498 --- /dev/null +++ b/src/components/Admin/MemberEdit.tsx @@ -0,0 +1,117 @@ +import { isFinite } from "lodash" +import { FC, memo, useState } from "react" +import withUserConnected from "../../utils/withUserConnected" +import withUserRole from "../../utils/withUserRole" +import ROLES from "../../utils/roles.constants" +import { Volunteer } from "../../services/volunteers" +import styles from "./styles.module.scss" +import { toastError } from "../../store/utils" + +interface Props { + volunteer: Volunteer + saveVolunteer: (newVolunteer: Partial) => void +} + +const MemberEdit: FC = ({ volunteer, saveVolunteer }): JSX.Element => { + const [localVolunteer, setLocalVolunteer] = useState(volunteer) + + const stringDispatch = + (propName: string) => + (e: React.ChangeEvent): void => { + saveVolunteer({ id: localVolunteer.id, [propName]: e.target.value }) + setLocalVolunteer({ ...localVolunteer, [propName]: e.target.value }) + } + + function stringInput(id: string, value: string): JSX.Element { + return ( +
+ {id} +
+ +
+ ) + } + + const numberDispatch = + (propName: string) => + (e: React.ChangeEvent): void => { + const value: number = +e.target.value + if (!isFinite(value)) { + toastError("Should be a number") + return + } + saveVolunteer({ id: localVolunteer.id, [propName]: +value }) + setLocalVolunteer({ ...localVolunteer, [propName]: +value }) + } + + function numberInput(id: string, value: number): JSX.Element { + return ( +
+ {id} +
+ +
+ ) + } + + const booleanDispatch = + (propName: string) => + (e: React.ChangeEvent): void => { + const value: boolean = e.target.value !== "0" && e.target.value !== "" + saveVolunteer({ id: localVolunteer.id, [propName]: value }) + setLocalVolunteer({ ...localVolunteer, [propName]: value }) + } + + function booleanInput(id: string, value: boolean): JSX.Element { + return ( +
+ {id} +
+ +
+ ) + } + + const volunteerDefault = new Volunteer() + const typeHandler: { [id: string]: (id: string, value: any) => JSX.Element } = { + string: stringInput, + number: numberInput, + boolean: booleanInput, + } + const keys = Object.keys(volunteerDefault) as (keyof Volunteer)[] + + return ( +
  • + {keys.map((key) => { + const valueType = typeof volunteerDefault[key] + const value = localVolunteer[key] + return ( + typeHandler[valueType as string]?.(key, value as any) || + stringInput(key, value as any) + ) + })} +
  • + ) +} + +export default withUserRole(ROLES.ADMIN, memo(withUserConnected(MemberEdit))) + +export const fetchFor = [] diff --git a/src/components/Admin/styles.module.scss b/src/components/Admin/styles.module.scss new file mode 100755 index 0000000..05c8804 --- /dev/null +++ b/src/components/Admin/styles.module.scss @@ -0,0 +1,39 @@ +@import "../../theme/variables"; +@import "../../theme/mixins"; + +.title { + padding-bottom: 10px; + font-weight: bold; +} + +.list { + padding: 0; + list-style: none; +} + +.item { + padding: 10px 0; +} + +.formButtons { + margin-top: 10px; + padding: 5px 0; + text-align: center; +} + +.inputContainer { + display: inline-block; +} + +.stringInput { + width: 5em; +} +.numberInput { + width: 3em; +} +.booleanInput { + width: 1.5em; +} +.inputDesc { + font-size: small; +} diff --git a/src/components/GameList/__tests__/GameList.tsx b/src/components/GameList/__tests__/GameList.tsx index 8871d70..dafda10 100644 --- a/src/components/GameList/__tests__/GameList.tsx +++ b/src/components/GameList/__tests__/GameList.tsx @@ -29,8 +29,6 @@ describe("", () => { "5": { id: 5, title: "6 qui prend!", - author: "Wolfgang Kramer", - editor: "(uncredited) , Design Edge , B", playersMin: 2, playersMax: 10, duration: 45, diff --git a/src/components/Knowledge/KnowledgeIntro.tsx b/src/components/Knowledge/KnowledgeIntro.tsx index a68156c..b22ff90 100644 --- a/src/components/Knowledge/KnowledgeIntro.tsx +++ b/src/components/Knowledge/KnowledgeIntro.tsx @@ -10,7 +10,7 @@ const KnowledgeIntro: React.FC = (): JSX.Element => (

    OK signifie que tu peux expliquer le jeu, avec au maximum un coup d'oeil aux règles sur - un nombre de cartes à distribuer, ou un nombre de PV déclancheur de fin de partie. + un nombre de cartes à distribuer, ou un nombre de PV déclencheur de fin de partie.
    Bof signifie que tu seras plus utile que la lecture des règles.

    diff --git a/src/components/RegisterForm/index.tsx b/src/components/RegisterForm/index.tsx index 38ffe08..984823c 100644 --- a/src/components/RegisterForm/index.tsx +++ b/src/components/RegisterForm/index.tsx @@ -1,4 +1,4 @@ -import React, { memo, useEffect, useState } from "react" +import { memo, useEffect, useState } from "react" import { useSelector, shallowEqual } from "react-redux" import { toast } from "react-toastify" import _ from "lodash" @@ -11,6 +11,12 @@ import { fetchVolunteerPartialAdd } from "../../store/volunteerPartialAdd" import FormButton from "../Form/FormButton/FormButton" import { validEmail } from "../../utils/standardization" import { toastError } from "../../store/utils" +import { + sendBooleanRadioboxDispatch, + sendTextareaDispatch, + sendRadioboxDispatch, + sendTextDispatch, +} from "../input.utils" import { fetchMiscMeetingDateListIfNeed, selectMiscMeetingDateList, @@ -57,26 +63,6 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => { }, [changingBackground, setChangingBackground]) const transitionClass = (i: number) => animations[changingBackground][i - 1] - const sendTextDispatch = - (dispatchSetter: React.Dispatch>) => - (e: React.ChangeEvent) => - dispatchSetter(e.target.value) - - const sendTextareaDispatch = - (dispatchSetter: React.Dispatch>) => - (e: React.ChangeEvent) => - dispatchSetter(e.target.value) - - const sendBooleanRadioboxDispatch = - (dispatchSetter: React.Dispatch>, isYes: boolean) => - (e: React.ChangeEvent) => - dispatchSetter(isYes ? !!e.target.value : !e.target.value) - - const sendRadioboxDispatch = - (dispatchSetter: React.Dispatch>) => - (e: React.ChangeEvent) => - dispatchSetter(e.target.value) - const onSubmit = () => { if (!validEmail(email)) { toastError("Cet email est invalid ><") diff --git a/src/components/VolunteerSet/__tests__/VolunteerSet.tsx b/src/components/VolunteerSet/__tests__/VolunteerSet.tsx deleted file mode 100644 index 45ff57d..0000000 --- a/src/components/VolunteerSet/__tests__/VolunteerSet.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { render } from "@testing-library/react" -import { MemoryRouter } from "react-router-dom" -import { volunteerExample } from "../../../services/volunteers" - -import VolunteerSet from "../index" - -describe("", () => { - it("renders", () => { - const dispatch = jest.fn() - const tree = render( - - - - ).container.firstChild - - expect(tree).toMatchSnapshot() - }) -}) diff --git a/src/components/VolunteerSet/__tests__/__snapshots__/VolunteerSet.tsx.snap b/src/components/VolunteerSet/__tests__/__snapshots__/VolunteerSet.tsx.snap deleted file mode 100644 index d49dbe9..0000000 --- a/src/components/VolunteerSet/__tests__/__snapshots__/VolunteerSet.tsx.snap +++ /dev/null @@ -1,51 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders 1`] = ` -
    -

    - Modifier un volunteer -

    -
    - - - - -
    -
    -`; diff --git a/src/components/VolunteerSet/index.tsx b/src/components/VolunteerSet/index.tsx deleted file mode 100644 index 9ba3fd3..0000000 --- a/src/components/VolunteerSet/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useState, memo } from "react" -import { toast } from "react-toastify" - -import { AppDispatch } from "../../store" - -import { fetchVolunteerSet } from "../../store/volunteerSet" -import { Volunteer } from "../../services/volunteers" -import styles from "./styles.module.scss" - -interface Props { - dispatch: AppDispatch - volunteer: Volunteer -} - -const VolunteerSet = ({ dispatch, volunteer }: Props) => { - const [firstname, setFirstname] = useState(volunteer.firstname) - const [lastname, setName] = useState(volunteer.lastname) - const [adult, setAdult] = useState(volunteer.adult) - - const onFirstnameChanged = (e: React.ChangeEvent) => - setFirstname(e.target.value) - const onNameChanged = (e: React.ChangeEvent) => setName(e.target.value) - const onAdultChanged = (e: React.ChangeEvent) => setAdult(+e.target.value) - - const onSavePostClicked = () => { - if (firstname && lastname) { - dispatch( - fetchVolunteerSet({ - ...volunteer, - firstname, - lastname, - adult, - }) - ) - } else { - toast.warning("Il faut au moins préciser un prenom et un nom", { - position: "top-center", - autoClose: 6000, - hideProgressBar: true, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - }) - } - } - return ( -
    -

    Modifier un volunteer

    -
    - - - - -
    -
    - ) -} -export default memo(VolunteerSet) diff --git a/src/components/VolunteerSet/styles.module.scss b/src/components/VolunteerSet/styles.module.scss deleted file mode 100644 index 6275f28..0000000 --- a/src/components/VolunteerSet/styles.module.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import "../../theme/variables"; - -.VolunteerList { - color: $color-white; - - ul { - padding-left: 17px; - } - - li { - margin-bottom: 0.5em; - } - - a { - color: $color-white; - } -} diff --git a/src/components/index.ts b/src/components/index.ts index 66f813f..7532b2c 100755 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,6 @@ import AnnouncementLink from "./AnnouncementLink" +import DbEdit, { fetchFor as fetchForDbEdit } from "./Admin/DbEdit" +import GameDetailsUpdate, { fetchFor as fetchForGameDetailsUpdate } from "./Admin/GameDetailsUpdate" import Board, { fetchFor as fetchForBoard } from "./VolunteerBoard/Board" import DayWishesForm, { fetchFor as fetchForDayWishesForm, @@ -20,11 +22,14 @@ import TeamWishesForm, { } from "./VolunteerBoard/TeamWishesForm/TeamWishesForm" import VolunteerList from "./VolunteerList" import VolunteerInfo from "./VolunteerInfo" -import VolunteerSet from "./VolunteerSet" import WishAdd from "./WishAdd" export { AnnouncementLink, + DbEdit, + fetchForDbEdit, + GameDetailsUpdate, + fetchForGameDetailsUpdate, Board, fetchForBoard, BoxList, @@ -48,6 +53,5 @@ export { fetchForTeamWishesForm, VolunteerInfo, VolunteerList, - VolunteerSet, WishAdd, } diff --git a/src/components/input.utils.ts b/src/components/input.utils.ts new file mode 100755 index 0000000..0220835 --- /dev/null +++ b/src/components/input.utils.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import React from "react" + +export const sendTextDispatch = + (dispatchSetter: React.Dispatch>) => + (e: React.ChangeEvent) => + dispatchSetter(e.target.value) + +export const sendTextareaDispatch = + (dispatchSetter: React.Dispatch>) => + (e: React.ChangeEvent) => + dispatchSetter(e.target.value) + +export const sendBooleanRadioboxDispatch = + (dispatchSetter: React.Dispatch>, isYes: boolean) => + (e: React.ChangeEvent) => + dispatchSetter(isYes ? !!e.target.value : !e.target.value) + +export const sendRadioboxDispatch = + (dispatchSetter: React.Dispatch>) => + (e: React.ChangeEvent) => + dispatchSetter(e.target.value) diff --git a/src/pages/Admin/DbEdit/DbEdit.tsx b/src/pages/Admin/DbEdit/DbEdit.tsx new file mode 100644 index 0000000..62f47bd --- /dev/null +++ b/src/pages/Admin/DbEdit/DbEdit.tsx @@ -0,0 +1,28 @@ +import { FC, memo } from "react" +import { RouteComponentProps } from "react-router-dom" +import { useSelector } from "react-redux" + +import { AppThunk } from "../../../store" +import { selectUserJwtToken } from "../../../store/auth" +import { DbEdit, fetchForDbEdit } from "../../../components" + +export type Props = RouteComponentProps + +const DbEditPage: FC = (): JSX.Element => { + const jwtToken = useSelector(selectUserJwtToken) + + if (jwtToken === undefined) return

    Loading...

    + if (jwtToken) { + return ( + <> + + + ) + } + return
    Besoin d'être identifié
    +} + +// Fetch server-side data here +export const loadData = (): AppThunk[] => [...fetchForDbEdit.map((f) => f())] + +export default memo(DbEditPage) diff --git a/src/pages/Admin/DbEdit/index.tsx b/src/pages/Admin/DbEdit/index.tsx new file mode 100755 index 0000000..e376969 --- /dev/null +++ b/src/pages/Admin/DbEdit/index.tsx @@ -0,0 +1,16 @@ +import loadable from "@loadable/component" + +import { Loading, ErrorBoundary } from "../../../components" +import { Props, loadData } from "./DbEdit" + +const DbEditPage = loadable(() => import("./DbEdit"), { + fallback: , +}) + +export default (props: Props): JSX.Element => ( + + + +) + +export { loadData } diff --git a/src/pages/Admin/GameDetailsUpdate/GameDetailsUpdate.tsx b/src/pages/Admin/GameDetailsUpdate/GameDetailsUpdate.tsx new file mode 100644 index 0000000..df2f952 --- /dev/null +++ b/src/pages/Admin/GameDetailsUpdate/GameDetailsUpdate.tsx @@ -0,0 +1,29 @@ +import { FC, memo } from "react" +import { RouteComponentProps } from "react-router-dom" +import { useSelector } from "react-redux" + +import { AppThunk } from "../../../store" +import { selectUserJwtToken } from "../../../store/auth" +import { GameDetailsUpdate } from "../../../components" +import { fetchGameDetailsUpdateIfNeed } from "../../../store/gameDetailsUpdate" + +export type Props = RouteComponentProps + +const GameDetailsUpdatePage: FC = (): JSX.Element => { + const jwtToken = useSelector(selectUserJwtToken) + + if (jwtToken === undefined) return

    Loading...

    + if (jwtToken) { + return ( + <> + + + ) + } + return
    Besoin d'être identifié
    +} + +// Fetch server-side data here +export const loadData = (): AppThunk[] => [...[fetchGameDetailsUpdateIfNeed].map((f) => f())] + +export default memo(GameDetailsUpdatePage) diff --git a/src/pages/Admin/GameDetailsUpdate/index.tsx b/src/pages/Admin/GameDetailsUpdate/index.tsx new file mode 100755 index 0000000..9641e13 --- /dev/null +++ b/src/pages/Admin/GameDetailsUpdate/index.tsx @@ -0,0 +1,16 @@ +import loadable from "@loadable/component" + +import { Loading, ErrorBoundary } from "../../../components" +import { Props, loadData } from "./GameDetailsUpdate" + +const GameDetailsUpdatePage = loadable(() => import("./GameDetailsUpdate"), { + fallback: , +}) + +export default (props: Props): JSX.Element => ( + + + +) + +export { loadData } diff --git a/src/routes/index.ts b/src/routes/index.ts index 630bdfa..99a253b 100755 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,10 @@ import { RouteConfig } from "react-router-config" import App from "../app" +import AsyncDbEdit, { loadData as loadDbEdit } from "../pages/Admin/DbEdit" +import AsyncGameDetailsUpdate, { + loadData as loadGameDetailsUpdate, +} from "../pages/Admin/GameDetailsUpdate" import AsyncHome, { loadData as loadHomeData } from "../pages/Home" import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements" import AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../pages/TeamAssignment" @@ -24,6 +28,16 @@ export default [ component: AsyncHome, loadData: loadHomeData, }, + { + path: "/edit", + component: AsyncDbEdit, + loadData: loadDbEdit, + }, + { + path: "/updateGameDetails", + component: AsyncGameDetailsUpdate, + loadData: loadGameDetailsUpdate, + }, { path: "/connaissances", component: AsyncKnowledge, diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts index 8df7e77..89589f8 100644 --- a/src/server/gsheets/accessors.ts +++ b/src/server/gsheets/accessors.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line max-classes-per-file import path from "path" -import _ from "lodash" +import _, { assign, pick } from "lodash" import { promises as fs, constants } from "fs" import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadsheet" import { SheetNames, saveLocalDb, loadLocalDb } from "./localDb" @@ -187,6 +187,33 @@ export class Sheet< this._type = db.type as Record } + parseRawPartialElement( + rawPartialElement: Partial> + ): Partial | undefined { + if (this._type === undefined) { + return undefined + } + // else + const rawPartialFrenchElement = _.mapValues( + this.invertedTranslation, + (englishProp: string) => (rawPartialElement as any)[englishProp] + ) as Element + + const rawFrenchElement = this.stringifyElement(this.frenchSpecimen, this._type) + assign(rawFrenchElement, rawPartialFrenchElement) + + const frenchElement = this.parseElement(rawFrenchElement, this._type) + + const element = _.mapValues( + this.translation, + (frenchProp: string) => (frenchElement as any)[frenchProp] + ) as Element + + const partialElement = pick(element, Object.keys(rawPartialElement)) + + return partialElement + } + dbSave(): void { this.saveTimestamp = +new Date() diff --git a/src/server/gsheets/expressAccessors.ts b/src/server/gsheets/expressAccessors.ts index c3dfe18..cba9ab7 100644 --- a/src/server/gsheets/expressAccessors.ts +++ b/src/server/gsheets/expressAccessors.ts @@ -39,6 +39,12 @@ export default class ExpressAccessors< return this.sheet as Sheet } + parseRawPartialElement( + rawPartialElement: Partial> + ): Partial | undefined { + return this.sheet?.parseRawPartialElement(rawPartialElement) + } + listGet() { return async ( _request: Request, @@ -56,6 +62,45 @@ export default class ExpressAccessors< } } + listSet( + custom?: ( + list: Element[], + body: RequestBody, + id: number, + roles: string[] + ) => Promise> | CustomSetReturn + ) { + return async (request: Request, response: Response, _next: NextFunction): Promise => { + try { + const sheet = await this.getSheet() + if (!custom) { + await sheet.setList(request.body) + response.status(200) + } else { + const memberId = response?.locals?.jwt?.id || -1 + const roles: string[] = response?.locals?.jwt?.roles || [] + const list = (await sheet.getList()) || [] + const { toDatabase, toCaller } = await custom( + list, + request.body, + memberId, + roles + ) + if (toDatabase !== undefined) { + await sheet.setList(toDatabase) + } + if (toCaller !== undefined) { + response.status(200).json(toCaller) + } else { + response.status(200) + } + } + } catch (e: any) { + response.status(200).json({ error: e.message }) + } + } + } + // custom can be async get( custom?: ( diff --git a/src/server/gsheets/games.ts b/src/server/gsheets/games.ts index 785379b..d8a7172 100644 --- a/src/server/gsheets/games.ts +++ b/src/server/gsheets/games.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from "lodash" import ExpressAccessors from "./expressAccessors" import { Game, GameWithoutId, translationGame } from "../../services/games" @@ -11,3 +12,21 @@ export const gameListGet = expressAccessor.listGet() // export const gameGet = expressAccessor.get() // export const gameAdd = expressAccessor.add() // export const gameSet = expressAccessor.set() + +export const gameDetailsUpdate = expressAccessor.listSet(async (list, _body, _id, roles) => { + if (!roles.includes("admin")) { + throw Error( + `À moins d'être admin, on ne peut pas modifier n'importe quel jeu, ${JSON.stringify( + roles + )}` + ) + } + const newList = cloneDeep(list) + + // TODO update game list details from BGG + + return { + toDatabase: newList, + toCaller: newList, + } +}) diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index 21e44ec..5349c28 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -1,4 +1,4 @@ -import { assign, cloneDeep, keys, omit, pick } from "lodash" +import { assign, cloneDeep, omit, pick } from "lodash" import bcrypt from "bcrypt" import sgMail from "@sendgrid/mail" @@ -30,7 +30,30 @@ export const volunteerListGet = expressAccessor.get(async (list, _body, id) => { } return list }) -export const volunteerSet = expressAccessor.set() + +export const volunteerSet = expressAccessor.set(async (list, body, _id, roles) => { + if (!roles.includes("admin")) { + throw Error(`À moins d'être admin, on ne peut pas modifier n'importe quel utilisateur`) + } + const newPartialVolunteer = body[0] as Partial> & { id: number } + const volunteer: Volunteer | undefined = list.find((v) => v.id === newPartialVolunteer.id) + if (!volunteer) { + throw Error(`Il n'y a aucun bénévole avec cet identifiant ${newPartialVolunteer.id}`) + } + const newVolunteer: Volunteer = cloneDeep(volunteer) + + const parsedPartialVolunteer = expressAccessor.parseRawPartialElement(newPartialVolunteer) + if (parsedPartialVolunteer === undefined) { + throw Error(`Erreur au parsing dans volunteerSet`) + } + + assign(newVolunteer, parsedPartialVolunteer) + + return { + toDatabase: newVolunteer, + toCaller: newVolunteer, + } +}) export const volunteerDiscordId = expressAccessor.get(async (list, body, id) => { const requestedId = +body[0] || id @@ -132,7 +155,7 @@ export const volunteerLogin = expressAccessor.get(async (list, b const lastForgot: { [id: string]: number } = {} export const volunteerForgot = expressAccessor.set(async (list, bodyArray) => { const [body] = bodyArray - const volunteer = getByEmail(list, body.email) + const volunteer: Volunteer | undefined = getByEmail(list, body.email) if (!volunteer) { throw Error("Il n'y a aucun bénévole avec cet email") } @@ -199,15 +222,17 @@ export const volunteerAsksSet = expressAccessor.set(async (list, body, id) => { } const newVolunteer = cloneDeep(volunteer) - assign(newVolunteer, pick(notifChanges, keys(newVolunteer))) + if (notifChanges.teamWishes !== undefined) newVolunteer.hiddenAsks = notifChanges.hiddenAsks + if (notifChanges.acceptsNotifs !== undefined) + newVolunteer.acceptsNotifs = notifChanges.acceptsNotifs + if (notifChanges.pushNotifSubscription !== undefined) + newVolunteer.pushNotifSubscription = notifChanges.pushNotifSubscription return { toDatabase: newVolunteer, toCaller: { id: newVolunteer.id, firstname: newVolunteer.firstname, - adult: newVolunteer.adult, - active: newVolunteer.active, hiddenAsks: newVolunteer.hiddenAsks, pushNotifSubscription: newVolunteer.pushNotifSubscription, acceptsNotifs: newVolunteer.acceptsNotifs, @@ -223,11 +248,11 @@ export const volunteerTeamWishesSet = expressAccessor.set(async (list, body, id, ) } const wishes = body[1] as VolunteerTeamWishes - const volunteer = list.find((v) => v.id === requestedId) + const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId) if (!volunteer) { throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`) } - const newVolunteer = cloneDeep(volunteer) + const newVolunteer: Volunteer = cloneDeep(volunteer) if (wishes.teamWishes !== undefined) { newVolunteer.teamWishes = wishes.teamWishes @@ -252,7 +277,7 @@ export const volunteerDayWishesSet = expressAccessor.set(async (list, body, id) throw Error(`On ne peut acceder qu'à ses propres envies de jours`) } const wishes = body[1] as VolunteerDayWishes - const volunteer = list.find((v) => v.id === requestedId) + const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId) if (!volunteer) { throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`) } @@ -318,10 +343,8 @@ export const volunteerParticipationDetailsSet = expressAccessor.set(async (list, } }) -export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, id) => { - const requestedId = +body[0] || id - const assigner = list.find((v) => v.id === requestedId) - if (!assigner || !assigner.roles.includes("répartiteur")) { +export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, _id, roles) => { + if (!roles.includes("répartiteur")) { throw Error(`Vous n'avez pas les droits pas assigner les équipes.`) } diff --git a/src/server/index.ts b/src/server/index.ts index f9721a5..c5ecdce 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,7 +19,7 @@ import certbotRouter from "../routes/certbot" import { hasSecret, secure } from "./secure" import { announcementListGet } from "./gsheets/announcements" import { detailedBoxListGet } from "./gsheets/boxes" -import { gameListGet } from "./gsheets/games" +import { gameListGet, gameDetailsUpdate } from "./gsheets/games" import { postulantAdd } from "./gsheets/postulants" import { teamListGet } from "./gsheets/teams" import { @@ -100,7 +100,6 @@ app.get("/VolunteerListGet", secure as RequestHandler, volunteerListGet) // Secured APIs app.get("/AnnouncementListGet", secure as RequestHandler, announcementListGet) app.get("/MiscDiscordInvitationGet", secure as RequestHandler, miscDiscordInvitation) -app.post("/VolunteerSet", secure as RequestHandler, volunteerSet) app.get("/TeamListGet", teamListGet) app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId) app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet) @@ -114,6 +113,10 @@ app.post("/VolunteerDayWishesSet", secure as RequestHandler, volunteerDayWishesS app.post("/VolunteerTeamWishesSet", secure as RequestHandler, volunteerTeamWishesSet) app.post("/VolunteerTeamAssignSet", secure as RequestHandler, volunteerTeamAssignSet) +// Admin only +app.post("/VolunteerSet", secure as RequestHandler, volunteerSet) +app.get("/GameDetailsUpdate", secure as RequestHandler, gameDetailsUpdate) + // Push notification subscription app.post("/notifications/subscribe", notificationsSubscribe) diff --git a/src/services/games.ts b/src/services/games.ts index 1d1768c..ef4c19a 100644 --- a/src/services/games.ts +++ b/src/services/games.ts @@ -3,10 +3,6 @@ export class Game { title = "" - author = "" - - editor = "" - playersMin = 0 playersMax = 0 @@ -27,8 +23,6 @@ export class Game { export const translationGame: { [k in keyof Game]: string } = { id: "id", title: "titre", - author: "auteur", - editor: "editeur", playersMin: "minJoueurs", playersMax: "maxJoueurs", duration: "duree", diff --git a/src/services/gamesAccessors.ts b/src/services/gamesAccessors.ts index 09e25c2..959556f 100644 --- a/src/services/gamesAccessors.ts +++ b/src/services/gamesAccessors.ts @@ -7,3 +7,5 @@ export const gameListGet = serviceAccessors.listGet() // export const gameGet = serviceAccessors.get() // export const gameAdd = serviceAccessors.add() // export const gameSet = serviceAccessors.set() + +export const gameDetailsUpdate = serviceAccessors.securedCustomGet<[], Game[]>("DetailsUpdate") diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts index b91065c..60543f4 100644 --- a/src/services/volunteers.ts +++ b/src/services/volunteers.ts @@ -163,8 +163,6 @@ export interface VolunteerDiscordId { export interface VolunteerAsks { id: Volunteer["id"] firstname: Volunteer["firstname"] - adult: Volunteer["adult"] - active: Volunteer["active"] hiddenAsks: Volunteer["hiddenAsks"] pushNotifSubscription: Volunteer["pushNotifSubscription"] acceptsNotifs: Volunteer["acceptsNotifs"] diff --git a/src/services/volunteersAccessors.ts b/src/services/volunteersAccessors.ts index 7f99008..194943e 100644 --- a/src/services/volunteersAccessors.ts +++ b/src/services/volunteersAccessors.ts @@ -20,7 +20,8 @@ export const volunteerDiscordIdGet = serviceAccessors.securedCustomGet< VolunteerDiscordId >("DiscordId") export const volunteerPartialAdd = serviceAccessors.customPost<[Partial]>("PartialAdd") -export const volunteerSet = serviceAccessors.set() + +export const volunteerSet = serviceAccessors.securedCustomPost<[Partial]>("Set") export const volunteerLogin = serviceAccessors.customPost<[{ email: string; password: string }]>("Login") diff --git a/src/store/__tests__/javGameList.ts b/src/store/__tests__/javGameList.ts index ab91913..5b3879e 100644 --- a/src/store/__tests__/javGameList.ts +++ b/src/store/__tests__/javGameList.ts @@ -17,8 +17,6 @@ const mockData: Game[] = [ { id: 5, title: "6 qui prend!", - author: "Wolfgang Kramer", - editor: "(uncredited) , Design Edge , B", playersMin: 2, playersMax: 10, duration: 45, diff --git a/src/store/gameDetailsUpdate.ts b/src/store/gameDetailsUpdate.ts new file mode 100644 index 0000000..2304ee8 --- /dev/null +++ b/src/store/gameDetailsUpdate.ts @@ -0,0 +1,50 @@ +import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" + +import { StateRequest, toastError, toastSuccess, elementListFetch } from "./utils" +import { Game } from "../services/games" +import { gameDetailsUpdate } from "../services/gamesAccessors" +import { AppState, AppThunk } from "." + +const gameAdapter = createEntityAdapter() + +const gameDetailsUpdateSlice = createSlice({ + name: "gameDetailsUpdate", + initialState: gameAdapter.getInitialState({ + readyStatus: "idle", + } as StateRequest), + reducers: { + getRequesting: (state) => { + state.readyStatus = "request" + }, + getSuccess: (state, { payload }: PayloadAction) => { + state.readyStatus = "success" + gameAdapter.setAll(state, payload) + }, + getFailure: (state, { payload }: PayloadAction) => { + state.readyStatus = "failure" + state.error = payload + }, + }, +}) + +export default gameDetailsUpdateSlice.reducer +export const { getRequesting, getSuccess, getFailure } = gameDetailsUpdateSlice.actions + +export const fetchGameDetailsUpdate = elementListFetch( + gameDetailsUpdate, + getRequesting, + getSuccess, + getFailure, + (error: Error) => toastError(`Erreur lors de la modification d'un bénévole: ${error.message}`), + () => toastSuccess("Bénévole modifié !") +) + +const shouldFetchGameDetailsUpdate = (state: AppState) => + state.volunteerList.readyStatus !== "success" + +export const fetchGameDetailsUpdateIfNeed = (): AppThunk => (dispatch, getState) => { + const { jwt } = getState().auth + if (shouldFetchGameDetailsUpdate(getState())) return dispatch(fetchGameDetailsUpdate(jwt)) + + return null +} diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index b935c4c..da91969 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -5,6 +5,7 @@ import announcementList from "./announcementList" import auth from "./auth" import boxList from "./boxList" import gameList from "./gameList" +import gameDetailsUpdate from "./gameDetailsUpdate" import miscDiscordInvitation from "./miscDiscordInvitation" import miscMeetingDateList from "./miscMeetingDateList" import postulantAdd from "./postulantAdd" @@ -32,6 +33,7 @@ export default (history: History) => ({ auth, boxList, gameList, + gameDetailsUpdate, miscDiscordInvitation, miscMeetingDateList, postulantAdd, diff --git a/src/store/utils.ts b/src/store/utils.ts index cf1ad83..395afba 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -118,33 +118,6 @@ export function elementListFetch>( } } -export function elementSet( - elementSetService: (element: Element) => Promise<{ - data?: Element | undefined - error?: Error | undefined - }>, - getRequesting: ActionCreatorWithoutPayload, - getSuccess: ActionCreatorWithPayload, - getFailure: ActionCreatorWithPayload, - errorMessage?: (error: Error) => void, - successMessage?: () => void -): (element: Element) => AppThunk { - return (element: Element): AppThunk => - async (dispatch) => { - dispatch(getRequesting()) - - const { error, data } = await elementSetService(element) - - if (error) { - dispatch(getFailure(error.message)) - errorMessage?.(error) - } else { - dispatch(getSuccess(data as Element)) - successMessage?.() - } - } -} - export function elementValueFetch( elementListService: () => Promise<{ data?: Element | undefined diff --git a/src/store/volunteerSet.ts b/src/store/volunteerSet.ts index ae78474..949f827 100644 --- a/src/store/volunteerSet.ts +++ b/src/store/volunteerSet.ts @@ -1,35 +1,38 @@ -import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" +import { PayloadAction, createSlice, createSelector } from "@reduxjs/toolkit" -import { StateRequest, toastError, toastSuccess, elementSet } from "./utils" +import { StateRequest, toastError, toastSuccess, elementFetch } from "./utils" import { Volunteer } from "../services/volunteers" import { volunteerSet } from "../services/volunteersAccessors" +import { AppState, AppThunk } from "." -const volunteerAdapter = createEntityAdapter() +type StateVolunteer = { entity?: Volunteer } & StateRequest + +export const initialState: StateVolunteer = { + readyStatus: "idle", +} const volunteerSetSlice = createSlice({ name: "volunteerSet", - initialState: volunteerAdapter.getInitialState({ - readyStatus: "idle", - } as StateRequest), + initialState, reducers: { - getRequesting: (state) => { - state.readyStatus = "request" - }, - getSuccess: (state, { payload }: PayloadAction) => { - state.readyStatus = "success" - volunteerAdapter.setOne(state, payload) - }, - getFailure: (state, { payload }: PayloadAction) => { - state.readyStatus = "failure" - state.error = payload - }, + getRequesting: (_) => ({ + readyStatus: "request", + }), + getSuccess: (_, { payload }: PayloadAction) => ({ + readyStatus: "success", + entity: payload, + }), + getFailure: (_, { payload }: PayloadAction) => ({ + readyStatus: "failure", + error: payload, + }), }, }) export default volunteerSetSlice.reducer export const { getRequesting, getSuccess, getFailure } = volunteerSetSlice.actions -export const fetchVolunteerSet = elementSet( +export const fetchVolunteerSet = elementFetch( volunteerSet, getRequesting, getSuccess, @@ -37,3 +40,20 @@ export const fetchVolunteerSet = elementSet( (error: Error) => toastError(`Erreur lors de la modification d'un bénévole: ${error.message}`), () => toastSuccess("Bénévole modifié !") ) + +const shouldFetchVolunteerSet = (_state: AppState) => true + +export const fetchVolunteerSetIfNeed = + (newPartialVolunteer: Partial): AppThunk => + (dispatch, getState) => { + const { jwt } = getState().auth + if (shouldFetchVolunteerSet(getState())) + return dispatch(fetchVolunteerSet(jwt, newPartialVolunteer)) + + return null + } + +export const selectVolunteerSet = createSelector( + (state: AppState) => state, + (state): string | undefined => state.volunteerSet?.entity?.discordId +) diff --git a/src/utils/roles.constants.ts b/src/utils/roles.constants.ts index 6147da8..b765bdd 100644 --- a/src/utils/roles.constants.ts +++ b/src/utils/roles.constants.ts @@ -3,6 +3,7 @@ type rolesType = { } const ROLES: rolesType = { + ADMIN: "admin", ASSIGNER: "répartiteur", TEAMLEAD: "référent", } diff --git a/src/utils/withUserRole.tsx b/src/utils/withUserRole.tsx index b847a09..7e43068 100644 --- a/src/utils/withUserRole.tsx +++ b/src/utils/withUserRole.tsx @@ -5,7 +5,11 @@ import { selectUserRoles } from "../store/auth" function withUserRole(requiredRole: string, Component: React.ComponentType) { return (props: T): JSX.Element => { const roles = useSelector(selectUserRoles) - return roles.includes(requiredRole) ? :
    + return roles.includes(requiredRole) ? ( + + ) : ( +
    Missing role {requiredRole}
    + ) } }