diff --git a/client/router.tsx b/client/router.tsx index 92d5b91..96f6ea0 100644 --- a/client/router.tsx +++ b/client/router.tsx @@ -6,6 +6,7 @@ import Layout from "./src/layout"; import React from "react"; import ContactAndContributionPage from "./src/pages/contact-contributionbyobject-page"; import LocalBSO from "./src/pages/bso/index"; +import LastMailSent from "./src/pages/last-mails-sent"; export default function Router() { return ( @@ -43,6 +44,7 @@ export default function Router() { path="/datasupr-contact" element={} /> + } /> } /> { + const fetchSentEmails = async () => { + const response = await fetch("/api/get-sent-emails", { + headers: postHeaders, + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }; + + const { data, isLoading, isError, refetch } = useQuery( + ["sentEmails"], + fetchSentEmails + ); + console.log(data); + return { data, isLoading, isError, refetch }; +}; + +export default useSentEmails; diff --git a/client/src/api/send-mail/index.tsx b/client/src/api/send-mail/index.tsx index 7d45e29..1818eaa 100644 --- a/client/src/api/send-mail/index.tsx +++ b/client/src/api/send-mail/index.tsx @@ -109,7 +109,7 @@ function EmailSender({ contribution, refetch }: EmailSenderProps) { setShowProfileModal(false)} - onSelectProfile={handleProfileSelect} // Passer la fonction pour sélectionner le profil + onSelectProfile={handleProfileSelect} selectedProfile={selectedProfile} /> setShowPreviewModal(false)}> diff --git a/client/src/layout/header.tsx b/client/src/layout/header.tsx index a87a504..663280a 100644 --- a/client/src/layout/header.tsx +++ b/client/src/layout/header.tsx @@ -157,6 +157,12 @@ const Header: React.FC = () => { > Works magnet + + Dernier mails envoyés + diff --git a/client/src/pages/last-mails-sent/components/item.tsx b/client/src/pages/last-mails-sent/components/item.tsx new file mode 100644 index 0000000..b791b29 --- /dev/null +++ b/client/src/pages/last-mails-sent/components/item.tsx @@ -0,0 +1,94 @@ +import React, { useState } from "react"; +import { Badge, Col, Text, Title } from "@dataesr/dsfr-plus"; +import { FaCopy } from "react-icons/fa"; +import "./styles.scss"; +import { LastMailsSentProps } from "../../../types"; +import collectionNameMapping, { generateLink } from "./utils"; + +const LastMailsSentItem: React.FC = ({ data }) => { + const [copiedId, setCopiedId] = useState(null); + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).then(() => { + setCopiedId(text); + setTimeout(() => setCopiedId(null), 2000); + }); + }; + + return ( + <> + {data.emails.map((email, index) => { + const link = generateLink( + email.collectionName, + email.fromApplication, + email.contributionId + ); + + const sentDate = new Date(email.sentAt); + const formattedDate = sentDate.toLocaleDateString("fr-FR"); + const formattedTime = sentDate.toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }); + + return ( + +
+ + {collectionNameMapping[email.collectionName]} + + + {email.fromApplication && ( + + {email.fromApplication} + + )} +
+ +
+ + Réponse de <i>{email.selectedProfile}</i> à{" "} + <i> + {email?.name} ({email?.to}) + </i> + + + + {email?.contributionId} + + + + + + Envoyé le {formattedDate} à {formattedTime} + + + {email.userResponse} +
+ + ); + })} + + ); +}; + +export default LastMailsSentItem; diff --git a/client/src/pages/last-mails-sent/components/selectors.tsx b/client/src/pages/last-mails-sent/components/selectors.tsx new file mode 100644 index 0000000..c642e68 --- /dev/null +++ b/client/src/pages/last-mails-sent/components/selectors.tsx @@ -0,0 +1,24 @@ +import { Col } from "@dataesr/dsfr-plus"; + +const Selectors = ({ uniqueProfiles, setStatus }) => { + const handleProfileChange = (event) => { + setStatus(event.target.value); + }; + + return ( + + + + + + ); +}; + +export default Selectors; diff --git a/client/src/pages/last-mails-sent/components/styles.scss b/client/src/pages/last-mails-sent/components/styles.scss new file mode 100644 index 0000000..bfb36be --- /dev/null +++ b/client/src/pages/last-mails-sent/components/styles.scss @@ -0,0 +1,6 @@ +.email-item { + background-color: #f1f4f8; + padding: 1.5rem; + border-radius: 8px; + margin-top: 1rem; +} diff --git a/client/src/pages/last-mails-sent/components/utils.tsx b/client/src/pages/last-mails-sent/components/utils.tsx new file mode 100644 index 0000000..7c12841 --- /dev/null +++ b/client/src/pages/last-mails-sent/components/utils.tsx @@ -0,0 +1,42 @@ +const collectionNameMapping: { [key: string]: string } = { + contribute: "Contribution par objets", + contacts: "Formulaire de contact", + contribute_productions: "Lier des publications", + "remove-user": "Retirer de la base de données", + "update-user-data": "Changement de nom", +}; + +export default collectionNameMapping; +// utils/generateLink.ts +export function generateLink( + collectionName: string, + fromApplication?: string, + id?: string +): string { + const basePathMap: { [key: string]: { [key: string]: string } | string } = { + contacts: { + scanr: "/scanr-contact", + paysage: "/paysage-contact", + bso: "/bso-contact", + curiexplore: "/curiexplore-contact", + "works-magnet": "/works-magnet-contact", + datasupr: "/datasupr-contact", + }, + contribute_production: "/scanr-apioperations", + "remove-user": "/scanr-removeuser", + "update-user-data": "/scanr-namechange", + contribute: "/scanr-contributionPage", + }; + + let basePath = ""; + + if (collectionName === "contacts" && fromApplication) { + basePath = basePathMap[collectionName][fromApplication] || ""; + } else { + basePath = (basePathMap[collectionName] as string) || ""; + } + + return id + ? `${basePath}?page=1&query=${id}&searchInMessage=false&sort=DESC&status=choose` + : basePath; +} diff --git a/client/src/pages/last-mails-sent/index.tsx b/client/src/pages/last-mails-sent/index.tsx new file mode 100644 index 0000000..8015574 --- /dev/null +++ b/client/src/pages/last-mails-sent/index.tsx @@ -0,0 +1,93 @@ +import { useState, useEffect } from "react"; +import { useLocation } from "react-router-dom"; +import { Col, Container, Row, Text, Title } from "@dataesr/dsfr-plus"; +import { Contribution } from "../../types"; +import BottomPaginationButtons from "../../components/pagination/bottom-buttons"; +import TopPaginationButtons from "../../components/pagination/top-buttons"; +import useSentEmails from "../../api/contribution-api/getSentMails"; +import LastMailsSentItem from "./components/item"; +import Selectors from "./components/selectors"; + +const LastMailsSent: React.FC = () => { + const location = useLocation(); + const [sort, setSort] = useState("DESC"); + const [status, setStatus] = useState("choose"); + const [query, setQuery] = useState([]); + const [page, setPage] = useState(1); + const [searchInMessage, setSearchInMessage] = useState(false); + + const { data, isLoading, isError } = useSentEmails(); + const sentEmails: Contribution[] = data ? data.emails : []; + const maxPage = Math.ceil((data?.emails.length || 0) / 10); + + const uniqueProfiles = Array.from( + new Set(sentEmails.map((email) => email.selectedProfile)) + ); + + useEffect(() => { + const params = new URLSearchParams(location.search); + setPage(parseInt(params.get("page") || "1")); + setSearchInMessage(params.get("searchInMessage") === "true"); + const queryParam = params.get("query") || ""; + setQuery(queryParam ? queryParam.split(",") : []); + setSort(params.get("sort") || "DESC"); + }, [location.search]); + + useEffect(() => { + const newSearchParams = new URLSearchParams(); + newSearchParams.set("page", page.toString()); + newSearchParams.set("query", query.join(",")); + newSearchParams.set("searchInMessage", searchInMessage.toString()); + newSearchParams.set("sort", sort); + if (status !== "choose") { + newSearchParams.set("status", status); + } + const newURL = `${window.location.pathname}?${newSearchParams.toString()}`; + window.history.pushState({}, "", newURL); + }, [page, query, searchInMessage, sort, status]); + + if (isLoading) + return ( + + Chargement... + + ); + + if (isError) + return ( + + Erreur lors du chargement des emails envoyés. + + ); + + return ( + + Derniers mails envoyés + + + + + + + + + + + + + + + + ); +}; + +export default LastMailsSent; diff --git a/client/src/types/index.ts b/client/src/types/index.ts index d6f7df4..22d6efc 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -17,6 +17,10 @@ export interface Contribution { threads?: Thread[]; fromApplication: string; } + +export interface LastMailsSentProps { + data: Contribution; +} export interface ContributorSummaryProps { contributions: Contribution[]; onSelectContribution: (id: string) => void; diff --git a/server/index.ts b/server/index.ts index d06df82..c378e01 100644 --- a/server/index.ts +++ b/server/index.ts @@ -11,6 +11,7 @@ import updateUserDataRoutes from "./routes/update-user-data"; import contactsRoutes from "./routes/contacts"; import sendMail from "./routes/reply/replyRoutes"; import { fetchEmails } from "./routes/receive-email"; +import getLastMailsSentRoutes from "./routes/last-mails-sent"; dotenv.config(); @@ -62,6 +63,7 @@ app app.use(removeUserRoutes); app.use(updateUserDataRoutes); app.use(sendMail); + app.use(getLastMailsSentRoutes); return app; }) .use( diff --git a/server/routes/last-mails-sent/get/index.ts b/server/routes/last-mails-sent/get/index.ts new file mode 100644 index 0000000..c3d6dfc --- /dev/null +++ b/server/routes/last-mails-sent/get/index.ts @@ -0,0 +1,39 @@ +import { Elysia } from "elysia"; +import { MongoClient } from "mongodb"; +import { errorSchema } from "../../../schemas/errors/errorSchema"; + +const MONGO_URI = process.env.MONGO_URI || ""; +const DB_NAME = process.env.MONGO_DATABASE || ""; + +const client = new MongoClient(MONGO_URI); +await client.connect(); +const db = client.db(DB_NAME); + +const lastSentMail = new Elysia(); + +lastSentMail.get( + "/get-sent-emails", + async () => { + const sentEmailsCollection = db.collection("sent_emails"); + + const sentEmails = await sentEmailsCollection.find().toArray(); + + return { + emails: sentEmails, + }; + }, + { + response: { + 401: errorSchema, + 500: errorSchema, + }, + detail: { + summary: "Récupérer les emails envoyés", + description: + "Cette route permet de récupérer la liste des emails envoyés et enregistrés dans la collection 'sent_emails' de MongoDB.", + tags: ["Emails"], + }, + } +); + +export default lastSentMail; diff --git a/server/routes/last-mails-sent/index.ts b/server/routes/last-mails-sent/index.ts new file mode 100644 index 0000000..ee54412 --- /dev/null +++ b/server/routes/last-mails-sent/index.ts @@ -0,0 +1,8 @@ +import { Elysia } from "elysia"; +import lastSentMail from "./get"; + +export const getLastMailsSentRoutes = new Elysia(); + +getLastMailsSentRoutes.use(lastSentMail); + +export default getLastMailsSentRoutes; diff --git a/server/routes/reply/replyRoutes.ts b/server/routes/reply/replyRoutes.ts index fce71e3..fa5ee2c 100644 --- a/server/routes/reply/replyRoutes.ts +++ b/server/routes/reply/replyRoutes.ts @@ -92,6 +92,32 @@ sendMail.post( error: `Erreur d'envoi: ${response.statusText}`, }; } + + let fromApplication = null; + if (collectionName === "contacts") { + const contactDoc = await db.collection("contacts").findOne({ + _id: new ObjectId(contributionId), + }); + if (contactDoc && contactDoc.fromApplication) { + fromApplication = contactDoc.fromApplication; + } + } + + const sentEmailsCollection = db.collection("sent_emails"); + await sentEmailsCollection.insertOne({ + to, + name, + subject, + userResponse, + selectedProfile, + message, + sentAt: new Date(), + contributionId, + collectionName, + status: "sent", + ...(fromApplication && { fromApplication }), + }); + const collection = db.collection(collectionName); const existingDoc = await collection.findOne({ _id: new ObjectId(contributionId), @@ -127,7 +153,8 @@ sendMail.post( return { success: true, - message: "E-mail envoyé et réponse enregistrée", + message: + "E-mail envoyé, réponse enregistrée et email loggé dans sent_emails", collection: collectionName, }; }, @@ -139,7 +166,7 @@ sendMail.post( detail: { summary: "Envoi d'un e-mail", description: - "Cette route permet d'envoyer un e-mail à un destinataire et d'enregistrer la réponse dans MongoDB dans une collection spécifique", + "Cette route permet d'envoyer un e-mail à un destinataire et d'enregistrer la réponse dans MongoDB dans une collection spécifique. Elle log également les emails envoyés dans une nouvelle collection 'sent_emails'.", tags: ["Envoi de mails"], }, } diff --git a/server/schemas/get/lastMailsSent.ts b/server/schemas/get/lastMailsSent.ts new file mode 100644 index 0000000..09714de --- /dev/null +++ b/server/schemas/get/lastMailsSent.ts @@ -0,0 +1,17 @@ +import { t } from "elysia"; + +export const lastMailsSentSchema = t.Object( + { + id: t.String(), + name: t.String(), + subject: t.String(), + userResponse: t.String(), + selectedProfile: t.String(), + message: t.String(), + sentAt: t.Date(), + contributionId: t.String(), + collectionName: t.String(), + status: t.String(), + }, + { additionalProperties: true } +);