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 (
+
+ {volunteers.map((volunteer: Volunteer) => (
+
+ ))}
+
+ )
+}
+
+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}
+ )
}
}