mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 08:34:20 +02:00
Add on site info on home page
This commit is contained in:
parent
42f3e86381
commit
4c704e87d4
98
src/components/Asks/OnSiteInfo.tsx
Normal file
98
src/components/Asks/OnSiteInfo.tsx
Normal 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]
|
@ -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(
|
||||
<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> :)
|
||||
<br />
|
||||
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 />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
asks.push(onSiteInfoElement)
|
||||
asks.push(asksEnd())
|
||||
}
|
||||
|
||||
if (volunteerAsks === undefined) {
|
||||
@ -65,10 +50,33 @@ const Asks = (): JSX.Element | 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> :)
|
||||
<br />
|
||||
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 />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Asks)
|
||||
|
||||
// Fetch server-side data here
|
||||
export const fetchFor = [
|
||||
...fetchForOnSiteInfo,
|
||||
// ...fetchForBrunch,
|
||||
// ...fetchForRetex,
|
||||
...fetchForDiscord,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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[]
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
65
src/store/volunteerOnSiteInfo.ts
Normal file
65
src/store/volunteerOnSiteInfo.ts
Normal 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
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user