From 69455ee4a7ffdb098c1085583a1c3cb16565e6fc Mon Sep 17 00:00:00 2001 From: Arthur L-Brjc Date: Tue, 19 Nov 2024 10:16:53 +0100 Subject: [PATCH] feat: commenter action --- .../actions/[idAction]/DetailActionPage.tsx | 5 +- clients/firebase.client.ts | 128 ++++++++++++---- components/Details.tsx | 62 ++++++++ components/action/CommentaireAction.tsx | 106 ++++++++++++++ components/action/HistoriqueAction.tsx | 73 +++------ components/action/InfoAction.tsx | 4 +- components/chat/Conversation.tsx | 13 +- components/chat/ConversationBeneficiaire.tsx | 2 +- components/chat/DisplayMessageConseiller.tsx | 8 + components/chat/LienAction.tsx | 26 ++++ components/chat/LienOffre.tsx | 4 +- components/chat/MessagesDuJour.tsx | 14 +- components/chat/RechercheMessage.tsx | 8 +- fixtures/message.ts | 1 + interfaces/message.ts | 8 + services/messages.service.ts | 138 ++++++++++++------ .../ConversationBeneficiaire.test.tsx | 7 +- tests/components/MessagesDuJour.test.tsx | 8 +- tests/pages/DetailActionPage.test.tsx | 105 +++++++++++-- tests/services/messages.service.test.ts | 126 ++++++++++++---- 20 files changed, 645 insertions(+), 201 deletions(-) create mode 100644 components/Details.tsx create mode 100644 components/action/CommentaireAction.tsx create mode 100644 components/chat/LienAction.tsx diff --git a/app/(connected)/(with-sidebar)/(with-chat)/mes-jeunes/[idJeune]/actions/[idAction]/DetailActionPage.tsx b/app/(connected)/(with-sidebar)/(with-chat)/mes-jeunes/[idJeune]/actions/[idAction]/DetailActionPage.tsx index a138df3da..4f6eba3eb 100644 --- a/app/(connected)/(with-sidebar)/(with-chat)/mes-jeunes/[idJeune]/actions/[idAction]/DetailActionPage.tsx +++ b/app/(connected)/(with-sidebar)/(with-chat)/mes-jeunes/[idJeune]/actions/[idAction]/DetailActionPage.tsx @@ -5,7 +5,8 @@ import { DateTime } from 'luxon' import { useRouter } from 'next/navigation' import React, { useRef, useState } from 'react' -import { HistoriqueAction } from 'components/action/HistoriqueAction' +import CommentaireAction from 'components/action/CommentaireAction' +import HistoriqueAction from 'components/action/HistoriqueAction' import StatutActionForm from 'components/action/StatutActionForm' import Modal, { ModalHandles } from 'components/Modal' import PageActionsPortal from 'components/PageActionsPortal' @@ -242,6 +243,8 @@ function DetailActionPage({ + + {showSuppression && ( diff --git a/clients/firebase.client.ts b/clients/firebase.client.ts index a926160e7..c7e3b8404 100644 --- a/clients/firebase.client.ts +++ b/clients/firebase.client.ts @@ -29,7 +29,8 @@ import { import { DateTime } from 'luxon' import { apiGet } from 'clients/api.client' -import { Chat } from 'interfaces/beneficiaire' +import { Action } from 'interfaces/action' +import { BeneficiaireEtChat, Chat } from 'interfaces/beneficiaire' import { UserType } from 'interfaces/conseiller' import { InfoFichier } from 'interfaces/fichier' import { @@ -51,6 +52,7 @@ type TypeMessageFirebase = | 'MESSAGE_EVENEMENT' | 'MESSAGE_EVENEMENT_EMPLOI' | 'MESSAGE_SESSION_MILO' + | 'MESSAGE_ACTION' | 'NOUVEAU_CONSEILLER_TEMPORAIRE' export type FirebaseMessage = { @@ -63,6 +65,7 @@ export type FirebaseMessage = { type: TypeMessageFirebase | undefined status?: string offre?: InfoOffreFirebase + action?: InfoActionFirebase evenement?: EvenementPartage evenementEmploi?: EvenementEmploi sessionMilo?: SessionMilo @@ -98,6 +101,11 @@ export type InfoOffreFirebase = { type?: string } +export type InfoActionFirebase = { + id: string + titre: string +} + export interface EvenementPartage { id: string titre: string @@ -123,7 +131,7 @@ type BaseCreateFirebaseMessage = { date: DateTime } -type BaseCreateFirebaseMessageImportant = { +type CreateFirebaseMessageImportant = { idConseiller: string dateDebut: DateTime dateFin: DateTime @@ -134,10 +142,15 @@ type BaseCreateFirebaseMessageImportant = { export type CreateFirebaseMessage = BaseCreateFirebaseMessage & { infoPieceJointe?: InfoFichier } -export type CreateFirebaseMessageWithOffre = BaseCreateFirebaseMessage & { +export type CreateFirebaseMessagePartageOffre = BaseCreateFirebaseMessage & { offre: BaseOffre } +export type CreateFirebaseMessageCommentaireAction = + BaseCreateFirebaseMessage & { + action: Action + } + type UpdateFirebaseMessage = { message: string date: DateTime @@ -186,7 +199,7 @@ export async function addMessage( } export async function addMessageImportant( - data: BaseCreateFirebaseMessageImportant + data: CreateFirebaseMessageImportant ): Promise { const firebaseMessage = createFirebaseMessageImportant(data) @@ -359,6 +372,35 @@ export async function getChatsDuConseiller( } } +export async function getChatDuBeneficiaire( + idConseiller: string, + idBeneficiaire: string +): Promise { + try { + const collectionRef = collection( + getDb(), + chatCollection + ) as CollectionReference + + const querySnapshots: QuerySnapshot = + await getDocs( + query( + collectionRef, + where('conseillerId', '==', idConseiller), + where('jeuneId', '==', idBeneficiaire) + ) + ) + if (querySnapshots.empty) return + + const document = querySnapshots.docs[0] + return chatFromFirebase(document.id, document.data()) + } catch (e) { + console.error(e) + captureError(e as Error) + throw e + } +} + export async function getMessagesGroupe( idConseiller: string, idGroupe: string @@ -413,22 +455,24 @@ export function observeChat( } export function observeDerniersMessagesDuChat( - idChat: string, + beneficiaireEtChat: BeneficiaireEtChat, nbMessages: number, onMessagesAntechronologiques: (messages: Message[]) => void ): () => void { try { return onSnapshot( query( - collection(getChatReference(idChat), 'messages') as CollectionReference< - FirebaseMessage, - FirebaseMessage - >, + collection( + getChatReference(beneficiaireEtChat.chatId), + 'messages' + ) as CollectionReference, orderBy('creationDate', 'desc'), limit(nbMessages) ), (querySnapshot: QuerySnapshot) => { - const messages: Message[] = querySnapshot.docs.map(docSnapshotToMessage) + const messages: Message[] = querySnapshot.docs.map((doc) => + docSnapshotToMessage(doc, beneficiaireEtChat.id) + ) if (messages.length && !messages.at(-1)!.creationDate) { return } @@ -443,12 +487,9 @@ export function observeDerniersMessagesDuChat( } } -export async function getMessageImportantSnapshot( +export async function findMessageImportant( idConseiller: string -): Promise< - | DocumentSnapshot - | undefined -> { +): Promise<(FirebaseMessageImportant & { id: string }) | undefined> { const collectionRef = collection( getDb(), messageImportantCollection @@ -464,7 +505,10 @@ export async function getMessageImportantSnapshot( ) ) - if (querySnapshots.docs.length > 0) return querySnapshots.docs[0] + if (!querySnapshots.empty) { + const document = querySnapshots.docs[0] + return { ...document.data(), id: document.id } + } } export async function rechercherMessages( @@ -497,18 +541,19 @@ export async function rechercherMessages( message.creationDate._seconds * 1000 ), }, - id + id, + idBeneficiaire ), } }) } export async function getMessagesPeriode( - idChat: string, + beneficiaireEtChat: BeneficiaireEtChat, debut: DateTime, fin: DateTime ): Promise { - const chatRef = getChatReference(idChat) + const chatRef = getChatReference(beneficiaireEtChat.chatId) const querySnapshots: QuerySnapshot = await getDocs( query( @@ -522,7 +567,9 @@ export async function getMessagesPeriode( ) ) - return querySnapshots.docs.map(docSnapshotToMessage) + return querySnapshots.docs.map((doc) => + docSnapshotToMessage(doc, beneficiaireEtChat.id) + ) } function retrieveApp() { @@ -560,7 +607,7 @@ async function getGroupeSnapshot( ) ) - if (querySnapshots.docs.length > 0) return querySnapshots.docs[0] + if (!querySnapshots.empty) return querySnapshots.docs[0] } async function getChatsSnapshot( @@ -623,7 +670,10 @@ function getMessageReference( } function createFirebaseMessage( - data: CreateFirebaseMessage | CreateFirebaseMessageWithOffre + data: + | CreateFirebaseMessage + | CreateFirebaseMessagePartageOffre + | CreateFirebaseMessageCommentaireAction ): FirebaseMessage { const type: TypeMessage = TypeMessage.MESSAGE let { encryptedText, iv } = data.message @@ -649,26 +699,31 @@ function createFirebaseMessage( id, titre, type: typeOffre, - } = (data as CreateFirebaseMessageWithOffre).offre + } = (data as CreateFirebaseMessagePartageOffre).offre firebaseMessage.offre = { id, titre, type: typeToFirebase(typeOffre) } } + if (Object.prototype.hasOwnProperty.call(data, 'action')) { + firebaseMessage.type = TypeMessage.MESSAGE_ACTION + const { id, titre } = (data as CreateFirebaseMessageCommentaireAction) + .action + firebaseMessage.action = { id, titre } + } + return firebaseMessage } function createFirebaseMessageImportant( - data: BaseCreateFirebaseMessageImportant + data: CreateFirebaseMessageImportant ): FirebaseMessageImportant { let { encryptedText, iv } = data.message - const firebaseMessage: FirebaseMessageImportant = { + return { content: encryptedText, iv, idConseiller: data.idConseiller, dateDebut: toTimestamp(data.dateDebut), dateFin: toTimestamp(data.dateFin), } - - return firebaseMessage } type FirebaseChat = { @@ -763,10 +818,15 @@ export function chatToFirebase(chat: Partial): Partial { } export function docSnapshotToMessage( - docSnapshot: QueryDocumentSnapshot + docSnapshot: QueryDocumentSnapshot, + idBeneficiaire: string ): Message { const firebaseMessage = docSnapshot.data() - return firebaseMessageToMessage(firebaseMessage, docSnapshot.id) + return firebaseMessageToMessage( + firebaseMessage, + docSnapshot.id, + idBeneficiaire + ) } function chatFromFirebase(chatId: string, firebaseChat: FirebaseChat): Chat { @@ -792,10 +852,12 @@ function chatFromFirebase(chatId: string, firebaseChat: FirebaseChat): Chat { function firebaseMessageToMessage( firebaseMessage: FirebaseMessage, - id: string + id: string, + idBeneficiaire: string ): Message { const message: Message = { id, + idBeneficiaire, sentBy: firebaseMessage.sentBy, content: firebaseMessage.content, iv: firebaseMessage.iv, @@ -813,6 +875,10 @@ function firebaseMessageToMessage( message.infoOffre = offreFromFirebase(firebaseMessage.offre) } + if (message.type === TypeMessage.MESSAGE_ACTION && firebaseMessage.action) { + message.infoAction = firebaseMessage.action + } + if ( message.type === TypeMessage.MESSAGE_EVENEMENT && firebaseMessage.evenement @@ -879,6 +945,8 @@ function firebaseToMessageType( return TypeMessage.MESSAGE_PJ case 'MESSAGE_OFFRE': return TypeMessage.MESSAGE_OFFRE + case 'MESSAGE_ACTION': + return TypeMessage.MESSAGE_ACTION case 'MESSAGE_EVENEMENT': return TypeMessage.MESSAGE_EVENEMENT case 'MESSAGE_EVENEMENT_EMPLOI': diff --git a/components/Details.tsx b/components/Details.tsx new file mode 100644 index 000000000..75c0ee28a --- /dev/null +++ b/components/Details.tsx @@ -0,0 +1,62 @@ +import React, { + ForwardedRef, + forwardRef, + ReactNode, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react' + +import IconComponent, { IconName } from 'components/ui/IconComponent' + +interface DetailsProps { + summary: string + children: ReactNode +} + +function Details( + { summary, children }: DetailsProps, + ref: ForwardedRef<{ focusSummary: () => void }> +) { + const detailsRef = useRef(null) + useImperativeHandle(ref, () => ({ + focusSummary: () => detailsRef.current!.querySelector('summary')!.focus(), + })) + + const [expanded, setExpanded] = useState() + + useEffect(() => { + function toggleExpanded() { + setExpanded(detailsRef.current!.open) + } + + const detailsNode = detailsRef.current! + detailsNode.addEventListener('toggle', toggleExpanded) + return () => detailsNode.removeEventListener('toggle', toggleExpanded) + }, []) + + return ( +
+ + {summary} + + + + {children} +
+ ) +} + +export default forwardRef(Details) diff --git a/components/action/CommentaireAction.tsx b/components/action/CommentaireAction.tsx new file mode 100644 index 000000000..b604bef5b --- /dev/null +++ b/components/action/CommentaireAction.tsx @@ -0,0 +1,106 @@ +import React, { useRef, useState } from 'react' + +import Details from 'components/Details' +import Button from 'components/ui/Button/Button' +import { InputError } from 'components/ui/Form/InputError' +import Textarea from 'components/ui/Form/Textarea' +import SuccessAlert from 'components/ui/Notifications/SuccessAlert' +import { ValueWithError } from 'components/ValueWithError' +import { Action } from 'interfaces/action' +import { BaseBeneficiaire } from 'interfaces/beneficiaire' +import { commenterAction as _commenterAction } from 'services/messages.service' +import { useChatCredentials } from 'utils/chat/chatCredentialsContext' + +interface CommentaireActionProps { + beneficiaire: BaseBeneficiaire + action: Action +} + +export default function CommentaireAction({ + beneficiaire, + action, +}: CommentaireActionProps) { + const chatCredentials = useChatCredentials() + const detailsRef = useRef<{ focusSummary: () => void }>(null) + const inputRef = useRef(null) + + const [message, setMessage] = useState>({ + value: undefined, + }) + const [envoiEnCours, setEnvoiEnCours] = useState(false) + const [succesEnvoi, setSuccesEnvoi] = useState(undefined) + + async function commenterAction() { + setSuccesEnvoi(undefined) + if (!message.value) { + setMessage({ + ...message, + error: 'Veuillez saisir un message à envoyer au bénéficiaire.', + }) + return + } + + try { + setEnvoiEnCours(true) + await _commenterAction({ + idDestinataire: beneficiaire.id, + action, + message: message.value, + cleChiffrement: chatCredentials!.cleChiffrement, + }) + setSuccesEnvoi(true) + inputRef.current!.value = '' + } catch { + setSuccesEnvoi(false) + } finally { + setEnvoiEnCours(false) + } + } + + return ( +
+ {succesEnvoi && ( + { + setSuccesEnvoi(undefined) + detailsRef.current!.focusSummary() + }} + /> + )} + + + {message.error && ( + e?.focus()}> + {message.error} + + )} +