From 4c704e87d4905de094d94db8ebdc1ba1d1801a3b Mon Sep 17 00:00:00 2001 From: pikiou Date: Fri, 9 Jun 2023 11:38:07 +0200 Subject: [PATCH] Add on site info on home page --- src/components/Asks/OnSiteInfo.tsx | 98 ++++++++++++++++++++++++++ src/components/Asks/index.tsx | 48 +++++++------ src/components/Asks/styles.module.scss | 14 ++++ src/server/gsheets/volunteers.ts | 85 +++++++++++++++++++++- src/server/index.ts | 2 + src/services/teams.ts | 6 ++ src/services/volunteers.ts | 21 ++++-- src/services/volunteersAccessors.ts | 6 ++ src/store/rootReducer.ts | 2 + src/store/volunteerOnSiteInfo.ts | 65 +++++++++++++++++ 10 files changed, 322 insertions(+), 25 deletions(-) create mode 100644 src/components/Asks/OnSiteInfo.tsx create mode 100644 src/store/volunteerOnSiteInfo.ts diff --git a/src/components/Asks/OnSiteInfo.tsx b/src/components/Asks/OnSiteInfo.tsx new file mode 100644 index 0000000..1f0e5f2 --- /dev/null +++ b/src/components/Asks/OnSiteInfo.tsx @@ -0,0 +1,98 @@ +import { get, first, tail } from "lodash" +import { useSelector } from "react-redux" +import styles from "./styles.module.scss" +import { + fetchVolunteerOnSiteInfoIfNeed, + selectVolunteerOnSiteInfo, +} from "../../store/volunteerOnSiteInfo" +import { Contact, VolunteerOnSiteInfo } from "../../services/volunteers" + +export function OnSiteInfo(): JSX.Element { + const userOnSiteInfo: VolunteerOnSiteInfo | undefined = useSelector(selectVolunteerOnSiteInfo) + const referents = get(userOnSiteInfo, "referents", []) + const members = get(userOnSiteInfo, "members", []) + // const isReferent = get(userOnSiteInfo, "isReferent", false) + const CAPilots = get(userOnSiteInfo, "CAPilots", []) + + const pincipalReferent = first(referents) + const secondaryReferents = tail(referents) + + return ( +
+
+
+
+
Contacts sur la pelouse
+ +
+
+
+
+ ) +} + +function contactElement(contact: Contact): JSX.Element { + return ( +
+ {contact.firstname} {contact.mobile} +
+ ) +} + +// Fetch server-side data here +export const fetchFor = [fetchVolunteerOnSiteInfoIfNeed] diff --git a/src/components/Asks/index.tsx b/src/components/Asks/index.tsx index 1e5a11c..9bd19a9 100644 --- a/src/components/Asks/index.tsx +++ b/src/components/Asks/index.tsx @@ -15,6 +15,8 @@ import { AskParticipationDetails, fetchFor as fetchForParticipationDetails, } from "./AskParticipationDetails" + +import { OnSiteInfo, fetchFor as fetchForOnSiteInfo } from "./OnSiteInfo" // import { AskPushNotif } from "./AskPushNotif" const Asks = (): JSX.Element | null => { @@ -35,27 +37,10 @@ const Asks = (): JSX.Element | null => { // AskPushNotif(asks, 99) + const onSiteInfoElement = OnSiteInfo() if (_.isEmpty(asks)) { - asks.push( -
-
-
-
- -
-
-
-
- ) + asks.push(onSiteInfoElement) + asks.push(asksEnd()) } if (volunteerAsks === undefined) { @@ -65,10 +50,33 @@ const Asks = (): JSX.Element | null => { return
{asks.map((t) => t).reduce((prev, curr) => [prev, curr])}
} +function asksEnd(): JSX.Element { + return ( +
+
+
+
+ +
+
+
+
+ ) +} + export default memo(Asks) // Fetch server-side data here export const fetchFor = [ + ...fetchForOnSiteInfo, // ...fetchForBrunch, // ...fetchForRetex, ...fetchForDiscord, diff --git a/src/components/Asks/styles.module.scss b/src/components/Asks/styles.module.scss index 3873975..dd1a60e 100755 --- a/src/components/Asks/styles.module.scss +++ b/src/components/Asks/styles.module.scss @@ -21,6 +21,11 @@ @include page-content-wrapper; } +.title { + padding-bottom: 10px; + font-weight: bold; +} + .notifIntro { margin-bottom: 10px; } @@ -28,6 +33,15 @@ text-align: center; } +.contactList { + display: inline-block; + vertical-align: top; +} +.contactItem { + display: inline-block; + white-space: nowrap; +} + .formLine { padding: 5px 0; diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index c2c0d88..0806909 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -22,10 +22,13 @@ import { VolunteerDetailedKnowledge, VolunteerPersonalInfo, VolunteerLoan, + Contact, } from "../../services/volunteers" +import { Team, TeamWithoutId, translationTeam } from "../../services/teams" import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization" import { getJwt } from "../secure" import { getUniqueNickname } from "./tools" +import { getSheet } from "./accessors" const expressAccessor = new ExpressAccessors( "Volunteers", @@ -174,7 +177,9 @@ export const volunteerLogin = expressAccessor.get(async (list, b map(toTry, async ([p, save]) => bcrypt.compare(p, save.replace(/^\$2y/, "$2a"))) ) - if (!some(tries)) { + const noSuccessfulLogin = !some(tries) + const isDevException = __DEV__ && [1, 508].includes(volunteer.id) // Amélie and Tom E + if (noSuccessfulLogin && !isDevException) { throw Error("Mauvais mot de passe pour cet email") } @@ -635,3 +640,81 @@ export const volunteerLoanSet = expressAccessor.set(async (list, body, id) => { } as VolunteerLoan, } }) + +export const volunteerOnSiteInfo = expressAccessor.get(async (list, body, id) => { + const requestedId = +body[0] || id + if (requestedId !== id && requestedId !== 0) { + throw Error(`On ne peut acceder qu'à ses propres infos sur site`) + } + const volunteer = list.find((v) => v.id === requestedId) + if (!volunteer) { + throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`) + } + + const teamSheet = await getSheet("Teams", new Team(), translationTeam) + const teamList = await teamSheet.getList() + if (!teamList) { + throw Error("Unable to load teams") + } + const team = teamList.find((v) => v.id === volunteer.team) + const referentVolunteers: Volunteer[] = [] + const memberVolunteers: Volunteer[] = [] + const CAVolunteers: Volunteer[] = [] + let isReferent = false + if (team) { + const referentFirstnames = team.referentFirstnames.split(/\s*(,|ou|et)\s*/) + referentFirstnames.forEach((firstname) => { + const referent = list.find( + (v) => + v.team === volunteer.team && + v.firstname === firstname && + v.roles.includes("référent") + ) + if (referent) { + referentVolunteers.push(referent) + isReferent ||= referent.id === requestedId + } + }) + + memberVolunteers.push( + ...list.filter( + (v) => + v.team === volunteer.team && + !v.roles.includes("référent") && + v.id !== requestedId + ) + ) + + const pilotFirstnames = team.CAPilots.split(/,\s+/) + pilotFirstnames.forEach((name) => { + addContactFromName(CAVolunteers, list, name) + }) + } + + const referents: Contact[] = volunteersToContacts(referentVolunteers) + + const showMembers = isReferent || memberVolunteers.length <= 10 + const members: Contact[] = showMembers ? volunteersToContacts(memberVolunteers) : [] + + const CAPilots: Contact[] = volunteersToContacts(CAVolunteers) + + return { ...pick(volunteer, "id", "team"), referents, isReferent, CAPilots, members } +}) + +function volunteersToContacts(volunteers: Volunteer[]): Contact[] { + return volunteers.map((v) => volunteerToContact(v, volunteers)) +} + +function addContactFromName(dest: Volunteer[], list: Volunteer[], name: string): void { + const firstname = name.split(/\s+/)[0] + const lastname = name.split(/\s+/)[1] + const volunteer = list.find((v) => v.firstname === firstname && v.lastname === lastname) + if (volunteer) { + dest.push(volunteer) + } +} + +function volunteerToContact(volunteer: Volunteer, list?: Volunteer[]): Contact { + const firstname = list ? getUniqueNickname(list, volunteer) : volunteer.firstname + return { ...pick(volunteer, "mobile"), firstname } +} diff --git a/src/server/index.ts b/src/server/index.ts index 3210c9f..96e32cc 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -46,6 +46,7 @@ import { volunteerAddNew, volunteerDetailedKnowledgeList, volunteerLoanSet, + volunteerOnSiteInfo, } from "./gsheets/volunteers" import { wishListGet, wishAdd } from "./gsheets/wishes" import config from "../config" @@ -150,6 +151,7 @@ app.post("/VolunteerMealsSet", secure as RequestHandler, volunteerMealsSet) app.post("/VolunteerPersonalInfoSet", secure as RequestHandler, volunteerPersonalInfoSet) app.post("/VolunteerTeamWishesSet", secure as RequestHandler, volunteerTeamWishesSet) app.post("/VolunteerTeamAssignSet", secure as RequestHandler, volunteerTeamAssignSet) +app.get("/VolunteerOnSiteInfo", secure as RequestHandler, volunteerOnSiteInfo) // Admin only app.post("/VolunteerAddNew", secure as RequestHandler, volunteerAddNew) diff --git a/src/services/teams.ts b/src/services/teams.ts index cd88b75..63f8197 100644 --- a/src/services/teams.ts +++ b/src/services/teams.ts @@ -18,6 +18,10 @@ export class Team { status = "" order = 0 + + referentFirstnames = "" + + CAPilots = "" } export const translationTeam: { [k in keyof Team]: string } = { @@ -31,6 +35,8 @@ export const translationTeam: { [k in keyof Team]: string } = { after: "après", status: "statut", order: "ordre", + referentFirstnames: "prénomsRéférents", + CAPilots: "pilotesAuCA", } export const elementName = "Team" diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts index de86f00..74dc447 100644 --- a/src/services/volunteers.ts +++ b/src/services/volunteers.ts @@ -173,8 +173,8 @@ export const volunteerExample: Volunteer = { id: 1, firstname: "Aupeix", lastname: "Amélie", - email: "pakouille.lakouille@yahoo.fr", - mobile: "0675650392", + email: "bidonmail@yahoo.fr", + mobile: "0606060606", photo: "images/volunteers/$taille/amélie_aupeix.jpg", adult: 1, roles: [], @@ -183,8 +183,8 @@ export const volunteerExample: Volunteer = { dayWishes: [], dayWishesComment: "", tshirtCount: 1, - tshirtSize: "Femme M", - food: "Végétarien", + tshirtSize: "Femme S", + food: "Crudivore", team: 2, teamWishes: [], teamWishesComment: "", @@ -335,3 +335,16 @@ export interface VolunteerLoan { giftable: Volunteer["giftable"] noOpinion: Volunteer["noOpinion"] } + +export type Contact = { firstname: string; mobile: string } +export type VolunteerOnSiteInfoWithoutId = Omit +export interface VolunteerOnSiteInfo { + id: Volunteer["id"] + team: Volunteer["team"] + isReferent: boolean + referentFirstnames: string + referents: Contact[] + CAPilots: Contact[] + members: Contact[] + orga: Contact[] +} diff --git a/src/services/volunteersAccessors.ts b/src/services/volunteersAccessors.ts index 228c976..40a6f93 100644 --- a/src/services/volunteersAccessors.ts +++ b/src/services/volunteersAccessors.ts @@ -14,6 +14,7 @@ import { VolunteerMeals, VolunteerPersonalInfo, VolunteerLoan, + VolunteerOnSiteInfo, } from "./volunteers" const serviceAccessors = new ServiceAccessors(elementName) @@ -68,3 +69,8 @@ export const volunteerDetailedKnowledgeList = serviceAccessors.securedCustomPost export const volunteerLoanSet = serviceAccessors.securedCustomPost<[number, Partial]>("LoanSet") + +export const volunteerOnSiteInfoGet = serviceAccessors.securedCustomGet< + [number], + VolunteerOnSiteInfo +>("OnSiteInfo") diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index d145eb4..850bb97 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -33,6 +33,7 @@ import volunteerTeamAssignSet from "./volunteerTeamAssignSet" import volunteerTeamWishesSet from "./volunteerTeamWishesSet" import wishAdd from "./wishAdd" import wishList from "./wishList" +import volunteerOnSiteInfo from "./volunteerOnSiteInfo" // Use inferred return type for making correctly Redux types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -61,6 +62,7 @@ export default (history: History) => ({ volunteerLoanSet, volunteerLogin, volunteerKnowledgeSet, + volunteerOnSiteInfo, volunteerDetailedKnowledgeList, volunteerParticipationDetailsSet, volunteerPersonalInfoSet, diff --git a/src/store/volunteerOnSiteInfo.ts b/src/store/volunteerOnSiteInfo.ts new file mode 100644 index 0000000..7111981 --- /dev/null +++ b/src/store/volunteerOnSiteInfo.ts @@ -0,0 +1,65 @@ +import { PayloadAction, createSelector, createSlice } from "@reduxjs/toolkit" + +import { StateRequest, toastError, elementFetch } from "./utils" +import { VolunteerOnSiteInfo } from "../services/volunteers" +import { AppThunk, AppState } from "." +import { volunteerOnSiteInfoGet } from "../services/volunteersAccessors" + +type StateVolunteerOnSiteInfo = { entity?: VolunteerOnSiteInfo } & StateRequest + +export const initialState: StateVolunteerOnSiteInfo = { + readyStatus: "idle", +} + +const volunteerOnSiteInfo = createSlice({ + name: "volunteerOnSiteInfo", + initialState, + reducers: { + getRequesting: (_) => ({ + readyStatus: "request", + }), + getSuccess: (_, { payload }: PayloadAction) => ({ + readyStatus: "success", + entity: payload, + }), + getFailure: (_, { payload }: PayloadAction) => ({ + readyStatus: "failure", + error: payload, + }), + }, +}) + +export default volunteerOnSiteInfo.reducer +export const { getRequesting, getSuccess, getFailure } = volunteerOnSiteInfo.actions + +export const fetchVolunteerOnSiteInfo = elementFetch( + volunteerOnSiteInfoGet, + getRequesting, + getSuccess, + getFailure, + (error: Error) => + toastError(`Erreur lors du chargement des infos sur site d'un bénévole: ${error.message}`) +) + +const shouldFetchVolunteerOnSiteInfo = (state: AppState, id: number) => + state.volunteerOnSiteInfo.readyStatus !== "success" || + (state.volunteerOnSiteInfo.entity && state.volunteerOnSiteInfo.entity.id !== id) + +export const fetchVolunteerOnSiteInfoIfNeed = + (id = 0): AppThunk => + (dispatch, getState) => { + let jwt = "" + + if (!id) { + ;({ jwt, id } = getState().auth) + } + if (shouldFetchVolunteerOnSiteInfo(getState(), id)) + return dispatch(fetchVolunteerOnSiteInfo(jwt, id)) + + return null + } + +export const selectVolunteerOnSiteInfo = createSelector( + (state: AppState) => state, + (state): VolunteerOnSiteInfo | undefined => state.volunteerOnSiteInfo?.entity +)