Add on site info on home page

This commit is contained in:
pikiou 2023-06-09 11:38:07 +02:00
parent 42f3e86381
commit 4c704e87d4
10 changed files with 322 additions and 25 deletions

View File

@ -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 (
<div key="contacts">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine}>
<div className={styles.title}>Contacts sur la pelouse</div>
<label>
{pincipalReferent && (
<div>
Référent.e{secondaryReferents.length > 0 && <> principal.e</>} :{" "}
{contactElement(pincipalReferent)}
<br />
<br />
</div>
)}
{secondaryReferents.length > 0 && (
<div>
Référent.e.s secondaire.s :{" "}
<div className={styles.contactList}>
{secondaryReferents.map((contact) => (
<div key={contact.firstname}>
{contactElement(contact)}
</div>
))}
<br />
</div>
</div>
)}
<div>Ton contact à la Paillante : à définir...</div>
<br />
<div>Si un exposant à une question : à définir...</div>
<br />
{CAPilots.length > 0 && (
<div>
Membre du CA si besoin :{" "}
<div className={styles.contactList}>
{CAPilots.map((contact) => (
<div key={contact.firstname}>
{contactElement(contact)}
</div>
))}
</div>
<br />
<br />
</div>
)}
Croix rouge présente sur le festival : tel prochainement
<br />
{members.length > 0 && (
<div>
Autre membres de l'équipe :{" "}
<div className={styles.contactList}>
{members.map((contact) => (
<div key={contact.firstname}>
{contactElement(contact)}
</div>
))}
<br />
</div>
</div>
)}
</label>
</div>
</div>
</div>
</div>
)
}
function contactElement(contact: Contact): JSX.Element {
return (
<div className={styles.contactList}>
{contact.firstname} <a href={`tel: ${contact.mobile}`}>{contact.mobile}</a>
</div>
)
}
// Fetch server-side data here
export const fetchFor = [fetchVolunteerOnSiteInfoIfNeed]

View File

@ -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,18 +37,30 @@ 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) {
return null
}
return <div>{asks.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}</div>
}
function asksEnd(): JSX.Element {
return (
<div key="pushNotifs">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine}>
<label>
Si tu veux changer la réponse à l'une des questions posées ici, va
dans <a href="/profil">Mon profil</a> :)
Si tu veux changer la réponse à l'une des questions posées ici, va dans{" "}
<a href="/profil">Mon profil</a> :)
<br />
Tu as fait le tour des dernières infos ou questions importantes,
merci !
Tu as fait le tour des dernières infos ou questions importantes, merci !
<br />
Nous te préviendrons quand il y en aura de nouvelles.
<br />
@ -58,17 +72,11 @@ const Asks = (): JSX.Element | null => {
)
}
if (volunteerAsks === undefined) {
return null
}
return <div>{asks.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}</div>
}
export default memo(Asks)
// Fetch server-side data here
export const fetchFor = [
...fetchForOnSiteInfo,
// ...fetchForBrunch,
// ...fetchForRetex,
...fetchForDiscord,

View File

@ -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;

View File

@ -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<VolunteerWithoutId, Volunteer>(
"Volunteers",
@ -174,7 +177,9 @@ export const volunteerLogin = expressAccessor.get<VolunteerLogin>(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<TeamWithoutId, Team>("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 }
}

View File

@ -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)

View File

@ -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"

View File

@ -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<VolunteerOnSiteInfo, "id">
export interface VolunteerOnSiteInfo {
id: Volunteer["id"]
team: Volunteer["team"]
isReferent: boolean
referentFirstnames: string
referents: Contact[]
CAPilots: Contact[]
members: Contact[]
orga: Contact[]
}

View File

@ -14,6 +14,7 @@ import {
VolunteerMeals,
VolunteerPersonalInfo,
VolunteerLoan,
VolunteerOnSiteInfo,
} from "./volunteers"
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
@ -68,3 +69,8 @@ export const volunteerDetailedKnowledgeList = serviceAccessors.securedCustomPost
export const volunteerLoanSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerLoan>]>("LoanSet")
export const volunteerOnSiteInfoGet = serviceAccessors.securedCustomGet<
[number],
VolunteerOnSiteInfo
>("OnSiteInfo")

View File

@ -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,

View File

@ -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<VolunteerOnSiteInfo>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
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
)