From 8ac3b1d595afa46d5558fd9a15b1794c415c21d3 Mon Sep 17 00:00:00 2001 From: Mihoub Date: Thu, 13 Jun 2024 15:18:41 +0200 Subject: [PATCH] fix(context): update context by using publication id instead of index, rework copy button --- package-lock.json | 30 ++- package.json | 1 + .../contributor-requests.tsx | 59 +++-- .../data-list-context/index.tsx | 16 +- .../link-publications/export-to-xlsx.tsx | 203 +++++++++--------- .../link-publications/message-preview.tsx | 68 ++++-- .../link-publications/name-selector.tsx | 126 +++++++---- .../link-publications/styles.scss | 78 ++++++- 8 files changed, 373 insertions(+), 208 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63c9901..fad0b02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@getbrevo/brevo": "^2.0.0-beta.4", "@tanstack/react-query": "^4.36.1", "@tanstack/react-query-devtools": "^4.29.6", + "@types/react-select": "^5.0.1", "classnames": "^2.3.2", "highcharts": "^11.4.1", "highcharts-react-official": "^3.2.1", @@ -3011,6 +3012,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-select": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-5.0.1.tgz", + "integrity": "sha512-h5Im0AP0dr4AVeHtrcvQrLV+gmPa7SA0AGdxl2jOhtwiE6KgXBFSogWw8az32/nusE6AQHlCOHQWjP1S/+oMWA==", + "deprecated": "This is a stub types definition. react-select provides its own type definitions, so you do not need this installed.", + "dependencies": { + "react-select": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -3794,9 +3804,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.801", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.801.tgz", - "integrity": "sha512-PnlUz15ii38MZMD2/CEsAzyee8tv9vFntX5nhtd2/4tv4HqY7C5q2faUAjmkXS/UFpVooJ/5H6kayRKYWoGMXQ==", + "version": "1.4.802", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz", + "integrity": "sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA==", "dev": true }, "node_modules/error-ex": { @@ -8431,6 +8441,14 @@ "@types/react": "*" } }, + "@types/react-select": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-5.0.1.tgz", + "integrity": "sha512-h5Im0AP0dr4AVeHtrcvQrLV+gmPa7SA0AGdxl2jOhtwiE6KgXBFSogWw8az32/nusE6AQHlCOHQWjP1S/+oMWA==", + "requires": { + "react-select": "*" + } + }, "@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -8968,9 +8986,9 @@ } }, "electron-to-chromium": { - "version": "1.4.801", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.801.tgz", - "integrity": "sha512-PnlUz15ii38MZMD2/CEsAzyee8tv9vFntX5nhtd2/4tv4HqY7C5q2faUAjmkXS/UFpVooJ/5H6kayRKYWoGMXQ==", + "version": "1.4.802", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz", + "integrity": "sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA==", "dev": true }, "error-ex": { diff --git a/package.json b/package.json index 0f39c53..370af6c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@getbrevo/brevo": "^2.0.0-beta.4", "@tanstack/react-query": "^4.36.1", "@tanstack/react-query-devtools": "^4.29.6", + "@types/react-select": "^5.0.1", "classnames": "^2.3.2", "highcharts": "^11.4.1", "highcharts-react-official": "^3.2.1", diff --git a/src/pages/api-operation-page/link-publications/contributor-requests.tsx b/src/pages/api-operation-page/link-publications/contributor-requests.tsx index a236ab7..1251978 100644 --- a/src/pages/api-operation-page/link-publications/contributor-requests.tsx +++ b/src/pages/api-operation-page/link-publications/contributor-requests.tsx @@ -1,18 +1,19 @@ import { Col } from "@dataesr/dsfr-plus"; -import React, { ReactNode, useState } from "react"; -import { FaShoppingCart } from "react-icons/fa"; +import React, { useState } from "react"; +import { FaShoppingCart, FaCopy } from "react-icons/fa"; import { Production } from "../../../types"; import SelectWithNames from "./name-selector"; import { ExternalLinks } from "./external-links"; import { useDataList } from "./data-list-context"; +import "./styles.scss"; // Importez vos styles CSS ou SCSS const ContributorRequests: React.FC<{ data: { id: any; - name: ReactNode; + name: string; productions: Production[]; }; - coloredName; + coloredName: string; }> = ({ data, coloredName }) => { const [copiedId, setCopiedId] = useState(null); const { dataList } = useDataList(); @@ -20,55 +21,53 @@ const ContributorRequests: React.FC<{ const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text).then(() => { setCopiedId(text); + setTimeout(() => { + setCopiedId(null); + }, 2000); // Efface l'ID copié après 2 secondes }); }; return ( <> {data.productions.map((production) => { + const isCopied = copiedId === production.id; + return ( -
{ - copyToClipboard(production.id); - }} - style={{ flex: 2, marginRight: "10px" }} - > +
ID de la publication : {production.id} + {dataList.find((item) => item.publi_id === production.id) ?.export && ( - + )}
- {copiedId === production.id && ( - - ID copié - - )} - +
- - +
+
- +
); })} diff --git a/src/pages/api-operation-page/link-publications/data-list-context/index.tsx b/src/pages/api-operation-page/link-publications/data-list-context/index.tsx index 75f07bb..1afc028 100644 --- a/src/pages/api-operation-page/link-publications/data-list-context/index.tsx +++ b/src/pages/api-operation-page/link-publications/data-list-context/index.tsx @@ -1,17 +1,22 @@ -import { createContext, useState, useContext } from "react"; +import React, { createContext, useState, useContext } from "react"; -const DataListContext = createContext<{ - dataList: any[]; +// Définition du type de contexte +interface DataListContextType { + dataList: any[]; // Type de dataList à ajuster selon votre structure setDataList: React.Dispatch>; -}>({ +} + +// Création du contexte avec une valeur par défaut +const DataListContext = createContext({ dataList: [], setDataList: () => {}, }); +// Composant Provider pour le contexte export const DataListProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const [dataList, setDataList] = useState([]); + const [dataList, setDataList] = useState([]); // Initialisation de dataList avec un tableau vide return ( @@ -20,6 +25,7 @@ export const DataListProvider: React.FC<{ children: React.ReactNode }> = ({ ); }; +// Hook personnalisé pour utiliser le contexte export const useDataList = () => { return useContext(DataListContext); }; diff --git a/src/pages/api-operation-page/link-publications/export-to-xlsx.tsx b/src/pages/api-operation-page/link-publications/export-to-xlsx.tsx index c6554c9..f89a6c5 100644 --- a/src/pages/api-operation-page/link-publications/export-to-xlsx.tsx +++ b/src/pages/api-operation-page/link-publications/export-to-xlsx.tsx @@ -3,45 +3,55 @@ import * as XLSX from "xlsx"; import { AiOutlineDelete } from "react-icons/ai"; import { toast } from "react-toastify"; import { useDataList } from "./data-list-context"; +import { useState } from "react"; +import "./styles.scss"; const ExcelExportButton = () => { const { dataList, setDataList } = useDataList(); + const [isMinimized, setIsMinimized] = useState(false); + const handleExportClick = () => { - const dataToExport = dataList.map((item) => ({ - person_id: item.person_id || "", - publi_id: item.publi_id || "", - full_name: item.fullName || "", - first_name: item.first_name || "", - last_name: item.last_name || "", - })); + const dataToExport = dataList + .filter((item) => item.export === true) + .map((item) => ({ + person_id: item.person_id || "", + publi_id: item.publi_id || "", + full_name: item.fullName || "", + first_name: item.first_name || "", + last_name: item.last_name || "", + })); + if (dataToExport.length === 0) { + toast.error("Aucune publication à exporter !"); + return; + } const worksheet = XLSX.utils.json_to_sheet(dataToExport); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1"); XLSX.writeFile(workbook, "export.xlsx"); }; - const handleRemoveClick = (index: number) => { + const handleRemoveClick = (publiId) => { setDataList((prevState) => { - const newList = prevState.map((item) => { - if ( - item.export === true && - item.publi_id === dataList[index].publi_id - ) { - return { ...item, export: false }; - } else { - return item; - } - }); + const newList = prevState.map((item) => + item.export === true && item.publi_id === publiId + ? { ...item, export: false } + : item + ); + + const removedItem = newList.find((item) => item.publi_id === publiId); + if (removedItem) { + toast(`Element retiré ! : ${removedItem.fullName}`, { + style: { + backgroundColor: "#d64d00", + color: "#fff", + }, + }); + } - toast(`Element retiré ! : ${newList[index].fullName}`, { - style: { - backgroundColor: "#d64d00", - color: "#fff", - }, - }); return newList; }); }; + const handleClearClick = () => { setDataList((prevState) => prevState.map((item) => ({ ...item, export: false })) @@ -56,29 +66,34 @@ const ExcelExportButton = () => { return (
- - Liste des publications à exporter - -
    -
    - - {`${ - dataList - .filter((item) => item.export === true) - .reduce( - (unique, item) => - unique.findIndex( - (obj) => obj.publi_id === item.publi_id - ) > -1 - ? unique - : [...unique, item], - [] - ).length - } publication${ +
    + + Liste des publications à exporter + +
    +
    + + {`${ + dataList.filter((item) => item.export === true).length + } publication${ + dataList.filter((item) => item.export === true).length > 1 + ? "s" + : "" + }`} + + +
    + {!isMinimized && ( + <> +
      + {Array.isArray(dataList) && dataList .filter((item) => item.export === true) .reduce( @@ -89,60 +104,48 @@ const ExcelExportButton = () => { ? unique : [...unique, item], [] - ).length > 1 - ? "s" - : "" - }`} - -
    - - {Array.isArray(dataList) && - dataList - .filter((item) => item.export === true) - .reduce( - (unique, item) => - unique.findIndex((obj) => obj.publi_id === item.publi_id) > -1 - ? unique - : [...unique, item], - [] - ) - .map((item, index) => ( -
  • a.fullName.localeCompare(b.fullName)) // Tri par fullName + .map((item, index) => ( +
  • +
    + + {item.publi_id} + + à lier à + + {item.fullName} + + +
    +
  • + ))} +
+
+ + + - -
- - ))} - -
- - - - -
+ Vider le panier + + +
+ + )}
); diff --git a/src/pages/api-operation-page/link-publications/message-preview.tsx b/src/pages/api-operation-page/link-publications/message-preview.tsx index 3d2701a..ff60a27 100644 --- a/src/pages/api-operation-page/link-publications/message-preview.tsx +++ b/src/pages/api-operation-page/link-publications/message-preview.tsx @@ -1,3 +1,5 @@ +import "react-toastify/dist/ReactToastify.css"; +import { toast } from "react-toastify"; import { Button, Col, Container, Link, Row, Text } from "@dataesr/dsfr-plus"; import type { Contribute_Production } from "../../../types"; import EditModal from "../../../components/edit-modal"; @@ -12,7 +14,7 @@ const MessagePreview = ({ refetch, }: { data: Contribute_Production; - refetch; + refetch: () => void; }) => { const [showModal, setShowModal] = useState(false); const [copySuccess, setCopySuccess] = useState(""); @@ -35,6 +37,48 @@ const MessagePreview = ({ setShowModal(false); }; + const handleExportAllClick = () => { + setDataList((prevState) => { + let addedToCart = false; + const updatedList = prevState.map((item) => { + if (item.person_id === data.id && !item.export) { + addedToCart = true; + return { ...item, export: true }; + } else { + return item; + } + }); + + if (addedToCart) { + const count = updatedList.filter( + (item) => item.person_id === data.id && item.export === true + ).length; + + toast( + `${count} publications de "${data.name}" ont été ajoutées au panier`, + { + style: { + backgroundColor: "#4caf50", + color: "#fff", + }, + } + ); + } else { + toast.warn( + `Les publications de "${data.name}" sont déjà dans le panier !`, + { + style: { + backgroundColor: "#f57c00", + color: "#fff", + }, + } + ); + } + + return updatedList; + }); + }; + return ( {data.comment && ( @@ -61,7 +105,7 @@ const MessagePreview = ({ onClick={() => copyToClipboard(data.id, "ID copié")} className={"fr-icon-user-line"} > - ID de la personne concerné: {data.id}{" "} + ID de la personne concernée : {data.id}{" "} {copySuccess === "ID copié" && ( copyToClipboard(fetchedData, "Nom copié")} > - {fetchedData ? `${fetchedData}` : "Nom non-inéxistant sur scanR"} + {fetchedData ? `${fetchedData}` : "Nom non existant sur scanR"} {copySuccess === "Nom copié" && ( copyToClipboard(data.email, "Email copié")} > - Email: {data.email} + Email : {data.email} {copySuccess === "Email copié" && ( )} - + diff --git a/src/pages/api-operation-page/link-publications/name-selector.tsx b/src/pages/api-operation-page/link-publications/name-selector.tsx index 3afbc65..ed4d534 100644 --- a/src/pages/api-operation-page/link-publications/name-selector.tsx +++ b/src/pages/api-operation-page/link-publications/name-selector.tsx @@ -1,14 +1,15 @@ -import ReactSelect from "react-select"; +import { useEffect } from "react"; import "react-toastify/dist/ReactToastify.css"; +import { toast } from "react-toastify"; import NameFromScanr from "../../../api/contribution-api/getNames"; import { levenshteinDistance } from "../utils/compare"; import { Col, Row } from "@dataesr/dsfr-plus"; import { useDataList } from "./data-list-context"; -import { useEffect } from "react"; +import ReactSelect from "react-select"; export default function SelectWithNames({ productionId, idRef, coloredName }) { const { fullName, firstName, lastName } = NameFromScanr(productionId); - const { dataList, setDataList } = useDataList(); + const { setDataList } = useDataList(); const customStyles = { option: (provided, state) => ({ ...provided, @@ -16,20 +17,29 @@ export default function SelectWithNames({ productionId, idRef, coloredName }) { }), }; const threshold = 7; - console.log(dataList); - const options = fullName.map((name, index) => ({ - value: name, - label: name, - firstName: firstName[index], - lastName: lastName[index], - isColored: levenshteinDistance(name, coloredName) <= threshold, - })); + useEffect(() => { - const selectedIndex = fullName.indexOf(coloredName); - if (selectedIndex !== -1 && options[selectedIndex]) { - const selectedOption = options[selectedIndex]; + let closestIndex = -1; + let minDistance = Infinity; + + fullName.forEach((name, index) => { + const distance = levenshteinDistance(name, coloredName); + if (distance <= threshold && distance < minDistance) { + closestIndex = index; + minDistance = distance; + } + }); + + if (closestIndex !== -1) { + const closestName = fullName[closestIndex]; + const selectedOption = { + value: closestName, + firstName: firstName[closestIndex], + lastName: lastName[closestIndex], + }; + const newElement = { - fullName: coloredName, + fullName: closestName, person_id: idRef, publi_id: productionId, first_name: selectedOption.firstName, @@ -42,7 +52,8 @@ export default function SelectWithNames({ productionId, idRef, coloredName }) { !prevState.some( (e) => e.person_id === newElement.person_id && - e.publi_id === newElement.publi_id + e.publi_id === newElement.publi_id && + e.export === false ) ) { return [...prevState, newElement]; @@ -51,36 +62,61 @@ export default function SelectWithNames({ productionId, idRef, coloredName }) { } }); } - }, []); + }, [ + coloredName, + fullName, + firstName, + lastName, + idRef, + productionId, + setDataList, + ]); - const handleChange = (option: { value: any }) => { + const handleChange = (option) => { const selectedIndex = fullName.indexOf(option.value); setDataList((prevState) => { - // const index = prevState.findIndex( - // (item) => item.fullName === option.value - // ); - // console.log(index); - // if (index !== -1) { - // const newState = [...prevState]; - // newState[index] = { - // ...newState[index], - // export: true, - // }; + const existingItem = prevState.find( + (e) => + e.person_id === idRef && + e.publi_id === productionId && + e.export === true + ); + + if (!existingItem) { + const newList = [ + ...prevState, + { + fullName: option.value, + person_id: idRef, + publi_id: productionId, + first_name: firstName[selectedIndex], + last_name: lastName[selectedIndex], + export: true, + }, + ]; + + toast(`La publication de ${option.value} a été ajoutée au panier`, { + style: { + backgroundColor: "#4caf50", + color: "#fff", + }, + }); + + return newList; + } else { + toast.warn( + `La publication de ${option.value} est déjà dans le panier !`, + { + style: { + backgroundColor: "#f57c00", + color: "#fff", + }, + } + ); - // return newState; - // } - return [ - ...prevState, - { - fullName: option.value, - person_id: idRef, - publi_id: productionId, - first_name: firstName[selectedIndex], - last_name: lastName[selectedIndex], - export: true, - }, - ]; + return prevState; + } }); }; @@ -88,7 +124,13 @@ export default function SelectWithNames({ productionId, idRef, coloredName }) { ({ + value: name, + label: name, + firstName: firstName[index], + lastName: lastName[index], + isColored: levenshteinDistance(name, coloredName) <= threshold, + }))} onChange={handleChange} styles={customStyles} placeholder={coloredName} diff --git a/src/pages/api-operation-page/link-publications/styles.scss b/src/pages/api-operation-page/link-publications/styles.scss index 18207fb..5cf87af 100644 --- a/src/pages/api-operation-page/link-publications/styles.scss +++ b/src/pages/api-operation-page/link-publications/styles.scss @@ -22,30 +22,52 @@ } .basket { position: fixed; - right: 0; + right: 20px; top: 50px; padding: 10px; display: flex; justify-content: center; align-items: center; flex-direction: column; - margin: 0 20px 20px 20px; z-index: 1000; background-color: white; border-radius: 20px; border: 2px solid black; + max-height: 90vh; + overflow-y: auto; +} +.basket-item { + display: flex; + flex-direction: column; + align-items: center; + border-bottom: 1px solid black; } .basket-content { - max-height: 500px; - overflow-y: auto; width: 100%; + overflow-y: auto; + max-height: calc(100% - 40px); } -.basket-item { + +.basket-controls { + position: sticky; + top: 0; + z-index: 1001; + background-color: white; + width: 100%; display: flex; - flex-direction: column; + justify-content: space-between; align-items: center; } + +.badge-count { + margin-right: 10px; +} + +.btn-minimize { + margin-left: 10px; +} + @keyframes flash { 0% { background-color: transparent; @@ -61,3 +83,47 @@ .basket.item-added { animation: flash 0.5s; } +.copy-button { + display: inline-flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 0; + margin-left: 10px; + position: relative; + outline: none; +} + +.copy-icon { + color: #000; + transition: color 0.3s ease; +} + +.copied .copy-icon { + color: #2196f3; +} + +.copied-text { + position: absolute; + top: 0; + left: 100%; + background-color: #3a4cef; + color: #fff; + padding: 4px; + border-radius: 4px; + font-size: 0.8em; + margin-left: 4px; + animation: fadeInOut 2s ease-in-out; +} + +@keyframes fadeInOut { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +}