diff --git a/src/client/src/components/App/Breadcrumbs/Breadcrumbs.jsx b/src/client/src/components/App/Breadcrumbs/Breadcrumbs.jsx index 971eca1c7..a9a1f491c 100644 --- a/src/client/src/components/App/Breadcrumbs/Breadcrumbs.jsx +++ b/src/client/src/components/App/Breadcrumbs/Breadcrumbs.jsx @@ -36,7 +36,7 @@ const Breadcrumbs = () => { {location.pathname.includes("establishment") ? "Fiche établissement" - : location.pathname.includes("entreprise") && "Fiche entreprise"} + : location.pathname.includes("enterprise") && "Fiche entreprise"} )} diff --git a/src/client/src/components/App/_variables.scss b/src/client/src/components/App/_variables.scss index 256871607..6d07a679a 100644 --- a/src/client/src/components/App/_variables.scss +++ b/src/client/src/components/App/_variables.scss @@ -81,7 +81,7 @@ $font-size-title: 1.5rem; $segoe: "Segoe Pro", sans-serif; $evolventa: "Evolventa", sans-serif; $roboto: Roboto, sans-serif; -$marianne: Marianne, arial, sans-serif; +$marianne: "Marianne", arial, sans-serif; /* SPACING (values from Bulma) */ $spacing-1: map-get($spacing-values, "1"); // 0.25rem diff --git a/src/client/src/components/DataSheets/Sections/Enterprise/Infos/EnterpriseInfos.js b/src/client/src/components/DataSheets/Sections/Enterprise/Infos/EnterpriseInfos.js index 1845755ef..0593a0467 100644 --- a/src/client/src/components/DataSheets/Sections/Enterprise/Infos/EnterpriseInfos.js +++ b/src/client/src/components/DataSheets/Sections/Enterprise/Infos/EnterpriseInfos.js @@ -1,6 +1,7 @@ import { merge } from "lodash"; import PropTypes from "prop-types"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; +import { useLocation } from "react-router"; import Association from "../../../../../containers/Association/Association"; import { formatSiret, formatTva } from "../../../../../helpers/utils"; @@ -38,6 +39,45 @@ import ObservationRCS from "./ObservationRCS"; const EnterpriseInfos = ({ enterprise: baseEntreprise }) => { const [accordionOpen, setAccordionOpen] = useState(true); + const location = useLocation(); + useEffect(() => { + const hash = window.location.hash; + if (location.pathname.includes("enterprise") && hash === "#mandataires") { + const checkAndScroll = () => { + const element = document.getElementById("mandataires"); + const headerOffset = + document.getElementById("header")?.offsetHeight || 0; // Dynamic header height + console.log(document.getElementById("header")); + if (element) { + const position = element.offsetTop - headerOffset; // Subtract header height + window.scrollTo({ + behavior: "smooth", + top: position, + }); + return true; + } + return false; + }; + + // Check if element is available and scroll to it + if (!checkAndScroll()) { + // If element is not available, set an interval to check again + const interval = setInterval(() => { + if (checkAndScroll()) { + clearInterval(interval); // Clear interval once the element is found + } + }, 100); // Check every 100ms + } + + // Add resize event listener + window.addEventListener("resize", checkAndScroll); + + // Clean up interval and remove event listener on component unmount + return () => { + window.removeEventListener("resize", checkAndScroll); + }; + } + }, [location]); const siren = getSiren(baseEntreprise); diff --git a/src/client/src/components/DataSheets/Sections/Enterprise/Infos/Mandataires/Mandataires.js b/src/client/src/components/DataSheets/Sections/Enterprise/Infos/Mandataires/Mandataires.js index 4b9472072..113ae3129 100644 --- a/src/client/src/components/DataSheets/Sections/Enterprise/Infos/Mandataires/Mandataires.js +++ b/src/client/src/components/DataSheets/Sections/Enterprise/Infos/Mandataires/Mandataires.js @@ -9,7 +9,7 @@ import NonBorderedTable from "../../../SharedComponents/NonBorderedTable/NonBord const Mandataires = ({ mandataires }) => { return mandataires && mandataires.length ? ( -
+
7}> diff --git a/src/client/src/components/DataSheets/Sections/SharedComponents/Subcategory/Subcategory.js b/src/client/src/components/DataSheets/Sections/SharedComponents/Subcategory/Subcategory.js index a0030bf9d..21b9b0bc4 100644 --- a/src/client/src/components/DataSheets/Sections/SharedComponents/Subcategory/Subcategory.js +++ b/src/client/src/components/DataSheets/Sections/SharedComponents/Subcategory/Subcategory.js @@ -16,9 +16,10 @@ const Subcategory = ({ sourceCustom = null, sourceDate = null, className = "", + id = "", }) => { return ( -
+
{subtitle && (

{subtitle}

@@ -61,6 +62,7 @@ Subcategory.propTypes = { className: PropTypes.string, datas: PropTypes.arrayOf(PropTypes.object), hasDateImport: PropTypes.bool, + id: PropTypes.string, sourceCustom: PropTypes.string, sourceDate: PropTypes.string, sourceSi: PropTypes.string, diff --git a/src/client/src/components/Search/Filters/AdministartionFilter.jsx b/src/client/src/components/Search/Filters/AdministartionFilter.jsx index a0a11ae9b..174dcb00f 100644 --- a/src/client/src/components/Search/Filters/AdministartionFilter.jsx +++ b/src/client/src/components/Search/Filters/AdministartionFilter.jsx @@ -6,23 +6,26 @@ import React, { useEffect, useRef, useState } from "react"; import ArrowDown from "../../shared/Icons/ArrowDown.jsx"; const AdministartionFilter = ({ - onFromSubmit, + onFormSubmit, removeFilters, + id, label, children, customFilters, addSaveClearButton = true, + filters, }) => { const [showMenu, setShowMenu] = useState(false); const dropdownRef = useRef(null); + const handleSubmit = (e) => { - setShowMenu(!showMenu); - onFromSubmit(e); + setShowMenu((prevShowMenu) => !prevShowMenu); + onFormSubmit(e); }; const onToggleMenu = (e) => { e.preventDefault(); - setShowMenu(!showMenu); + setShowMenu((prevShowMenu) => !prevShowMenu); }; const handleClickOutside = (e) => { @@ -42,11 +45,27 @@ const AdministartionFilter = ({ const childrenWithProps = React.Children.map(children, (child) => React.cloneElement(child, { onToggleMenu }) ); + + const getButtonLabel = () => { + if (id === "dirigeant") { + if ( + filters["dirigeant"]?.nom !== "" || + filters["dirigeant"]?.prenom !== "" + ) + return ( + (filters["dirigeant"] && + `${filters["dirigeant"].nom} ${filters["dirigeant"].prenom}`) || + label + ); + } + return label; + }; + return (
@@ -103,9 +87,10 @@ DirigeantFromFilter.propTypes = { filters: PropTypes.object.isRequired, id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, + onFormSubmit: PropTypes.func.isRequired, + onToggleMenu: PropTypes.func.isRequired, placeholder: PropTypes.string.isRequired, removeFilter: PropTypes.func.isRequired, - onToggleMenu: PropTypes.func.isRequired, }; export default DirigeantFromFilter; diff --git a/src/client/src/components/Search/Search.js b/src/client/src/components/Search/Search.js index 7dd88afdc..476eaccd3 100644 --- a/src/client/src/components/Search/Search.js +++ b/src/client/src/components/Search/Search.js @@ -11,7 +11,7 @@ import SearchResults from "../SearchResults"; import AdministartionFilter from "./Filters/AdministartionFilter.jsx"; import AutoCompleteFilter from "./Filters/AutoCompleteFilter"; import CheckboxFilter from "./Filters/CheckboxFilter"; -// import DirigeantFromFilter from "./Filters/DirigeantFromFilter"; +import DirigeantFromFilter from "./Filters/DirigeantFromFilter"; import LocationFilter from "./Filters/LocationFilter"; import SearchBar from "./SearchBar"; @@ -53,7 +53,7 @@ const Search = ({ generateXlsx, downloadLoading, }) => { - const onFromSubmit = (e) => { + const onFormSubmit = (e) => { e.preventDefault(); sendRequest(searchTerm, options); }; @@ -67,6 +67,20 @@ const Search = ({ { label: "En activité", value: actif }, { label: "Cessée", value: ferme }, ]; + const categoriesEntreprisesOptions = [ + { + label: "Petite ou Moyenne Entreprise", + value: "PME", + }, + { + label: "Entreprise de Taille Intermédiaire", + value: "ETI", + }, + { + label: "Grande Entreprise", + value: "GE", + }, + ]; return (
@@ -138,7 +152,7 @@ const Search = ({
+
- {/*
- + - -
*/} +
+
diff --git a/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js b/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js index 45f960327..ea625b666 100644 --- a/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js +++ b/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js @@ -18,94 +18,103 @@ const SearchAwesomeTable = ({ isLoading = false, data, fields, - history, -}) => ( - - - - {fields.map((field) => { - return ( - - ); - })} - - - - {isLoading ? ( - - +}) => { + const handleRowClick = (event, element) => { + if (event.target.tagName === "A") { + return; + } + window.location.href = `/establishment/${element.siret}`; + }; + return ( +
- {field.headName} -
- -
+ + + {fields.map((field) => { + return ( + + ); + })} - ) : ( - data.map((element, index) => ( - history.push(`/establishment/${element.siret}`)} - > - {fields.map((field, index) => ( - - ))} + + + {isLoading ? ( + + - )) - )} - - {showPagination && ( - - - handleRowClick(e, element)} + > + {fields.map((field, index) => { + return ( + + ); + })} + + )) + )} + + {showPagination && ( + + + - - - )} -
+ {field.headName} +
- {field.html ? ( - - ) : ( - field.accessor(element) - )} -
+ +
-
- - + ) : ( + data.map((element, index) => ( +
+ {field.html ? ( + + ) : ( + field.accessor(element) + )} +
+
+ + - -
-
-); + +
+ + + + )} + + ); +}; SearchAwesomeTable.propTypes = { data: PropTypes.array.isRequired, diff --git a/src/client/src/components/SearchResults/SearchResults.js b/src/client/src/components/SearchResults/SearchResults.js index 35b4f9e3c..3725e8726 100644 --- a/src/client/src/components/SearchResults/SearchResults.js +++ b/src/client/src/components/SearchResults/SearchResults.js @@ -143,6 +143,41 @@ const SearchResults = ({ html: true, sortKey: "enterprise_name", }, + { + accessor: (fields) => { + const formatName = (dirigeant) => + `${ + dirigeant.prenom.charAt(0).toUpperCase() + + dirigeant.prenom.slice(1).toLowerCase() + } ${ + dirigeant.nom.charAt(0).toUpperCase() + + dirigeant.nom.slice(1).toLowerCase() + }`; + if (fields?.dirigeants == 0) return " - "; + + return ( + <> + {fields.dirigeants + ?.slice(0, 2) + .map((dirigeant, index) => ( + + {index > 0 && ", "} + {formatName(dirigeant)} + + ))} + + {" ..." + "voir plus"} + + + ); + }, + + headName: "Dirigeants", + importantHead: true, + link: ({ fields }) => + `/enterprise/${fields?.siren}#mandataires`, + sortKey: "dirigeants", + }, { accessor: ({ etablissementSiege }) => { return Value({ @@ -221,6 +256,7 @@ const SearchResults = ({ SearchResults.propTypes = { downloadLoading: PropTypes.bool.isRequired, generateXlsx: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, isLoading: PropTypes.bool.isRequired, pagination: PropTypes.object.isRequired, query: PropTypes.string, diff --git a/src/client/src/containers/Search/Search.js b/src/client/src/containers/Search/Search.js index a13dff64e..59730993c 100644 --- a/src/client/src/containers/Search/Search.js +++ b/src/client/src/containers/Search/Search.js @@ -13,6 +13,7 @@ import { useSearchTerms, } from "../../services/Store/hooks/search"; import { normalizeCodeCommunes } from "../../utils/code-commune/code-commune"; +import { normalizeCodeDepartement } from "../../utils/code-departement/code-departement"; import { useFileDownload } from "../../utils/file-download/hooks"; import { useSort } from "../../utils/search-table/hooks"; import divisionsNaf from "./divisions-naf.json"; @@ -30,7 +31,9 @@ const formatLocationFilter = (filters) => { codesCommunes: normalizeCodeCommunes( locationFilters?.commune?.map(prop("value")) || [] ), - departements: locationFilters?.departement?.map(prop("value")) || [], + departements: normalizeCodeDepartement( + locationFilters?.departement?.map(prop("value")) || [] + ), }; }; diff --git a/src/client/src/helpers/Search/Search.js b/src/client/src/helpers/Search/Search.js index 71f04e27a..e4bd8d0ee 100644 --- a/src/client/src/helpers/Search/Search.js +++ b/src/client/src/helpers/Search/Search.js @@ -13,3 +13,18 @@ export const formatSearchInput = (query) => { return isSirenOrSiret ? `"${queryWithoutWhitespace}"` : `"${query}"`; }; + +export const categoriesEntreprisesOptions = [ + { + label: "Petite ou Moyenne Entreprise", + value: "PME", + }, + { + label: "Entreprise de Taille Intermédiaire", + value: "ETI", + }, + { + label: "Grande Entreprise", + value: "GE", + }, +]; diff --git a/src/client/src/helpers/hooks/useScrollToLocationHash.js b/src/client/src/helpers/hooks/useScrollToLocationHash.js index 7c909a2b7..28320b001 100644 --- a/src/client/src/helpers/hooks/useScrollToLocationHash.js +++ b/src/client/src/helpers/hooks/useScrollToLocationHash.js @@ -9,5 +9,5 @@ export const useScrollToLocationHash = ({ location, offset = -50 }) => { ? scrollTarget.getBoundingClientRect().top + window.pageYOffset + offset : 0, }); - }, [location, offset]); + }, [location, location.hash, offset]); }; diff --git a/src/client/src/utils/code-departement/code-departement.js b/src/client/src/utils/code-departement/code-departement.js new file mode 100644 index 000000000..895d1a6e5 --- /dev/null +++ b/src/client/src/utils/code-departement/code-departement.js @@ -0,0 +1,16 @@ +const CORSE_DEP = ["2A", "2B"]; +const CORSE_DEP_CODES = { + "2A": ["2A", "20"], + "2B": ["2B", "21"], +}; + +const getCodeDepartementForCorse = (codeDep) => { + if (CORSE_DEP.includes(codeDep)) { + return CORSE_DEP_CODES[codeDep]; + } + + return [codeDep]; +}; +export const normalizeCodeDepartement = (codedepartement) => { + return codedepartement.flatMap(getCodeDepartementForCorse); +}; diff --git a/src/client/src/utils/entreprise/entreprise.js b/src/client/src/utils/entreprise/entreprise.js index 6cbba69c5..a749e61ff 100644 --- a/src/client/src/utils/entreprise/entreprise.js +++ b/src/client/src/utils/entreprise/entreprise.js @@ -110,6 +110,8 @@ export const formatInteractions = pipe( reverse ); export const formatUpperCase = (data) => { - if (!data) return ""; - return data.toUpperCase(); + if (typeof data === "string" && data.length > 0) { + return data.charAt(0).toUpperCase() + data.slice(1).toLowerCase(); + } + return data; }; diff --git a/src/server/src/utils/elastic.js b/src/server/src/utils/elastic.js index 29a663c11..9fc90f156 100644 --- a/src/server/src/utils/elastic.js +++ b/src/server/src/utils/elastic.js @@ -32,8 +32,59 @@ const codesNafLabelIndex = codesNaf.reduce( new Map() ); -const makeQuery = ({ query, siege, ...filters }) => { +const makeQuery = ({ query, siege, dirigeant, ...filters }) => { const siretOrSirenQuery = query.replace(/\s/g, ""); + const dirigeantConditions = []; + + if (dirigeant) { + if (dirigeant.nom && dirigeant.prenom) { + dirigeantConditions.push({ + nested: { + path: "dirigeants", + query: { + bool: { + must: [ + { + match: { + "dirigeants.nom": { + query: dirigeant.nom, + }, + }, + }, + { + match: { + "dirigeants.prenom": { + query: dirigeant.prenom, + }, + }, + }, + ], + }, + }, + }, + }); + } + if (dirigeant.nom && !dirigeant.prenom) { + dirigeantConditions.push({ + nested: { + path: "dirigeants", + query: { + match: { "dirigeants.nom": dirigeant.nom }, + }, + }, + }); + } + if (dirigeant.prenom && !dirigeant.nom) { + dirigeantConditions.push({ + nested: { + path: "dirigeants", + query: { + match: { "dirigeants.prenom": dirigeant.prenom }, + }, + }, + }); + } + } return { ...(query ? { min_score: 20 } : {}), query: { @@ -41,70 +92,103 @@ const makeQuery = ({ query, siege, ...filters }) => { filter: [ ...(siege !== "" ? [ - { - term: { etablissementSiege: siege === "true" }, - }, - ] + { + term: { etablissementSiege: siege === "true" }, + }, + ] : []), ], must: [ + ...dirigeantConditions, ...Object.keys(filtersFieldMap).flatMap((key) => filters[key]?.length > 0 ? [ - { - terms: { [filtersFieldMap[key]]: filters[key] }, - }, - ] + { + terms: { [filtersFieldMap[key]]: filters[key] }, + }, + ] : [] ), + ...(query ? [ - { - bool: { - should: [ - { - multi_match: { - query, - type: "phrase", - fields: [ - "raisonSociale", - "denominationUsuelleUniteLegale", - "enseigneEtablissement" - ], - } - }, - ...(siretOrSirenQuery - ? [ - { - term: { - siret: { - value: query.replace(/\s/g, ""), - boost: 100, - }, - }, + { + bool: { + should: [ + { + multi_match: { + query, + type: "phrase", + fields: [ + "naming", + "denominationUsuelleUniteLegale", + "raisonSociale", + ], + minimum_should_match: "100%", + boost: 100, }, - { - term: { - siren: { - value: query.replace(/\s/g, ""), - boost: 100, - }, - }, + }, + + { + multi_match: { + query, + fields: [ + "denominationUsuelleUniteLegale", + "raisonSociale", + ], + fuzziness: "AUTO", + minimum_should_match: "100%", }, - ] - : []), - ], + }, + + ...(siretOrSirenQuery + ? [ + { + term: { + siret: { + value: query.replace(/\s/g, ""), + boost: 100, + }, + }, + }, + { + term: { + siren: { + value: query.replace(/\s/g, ""), + boost: 100, + }, + }, + }, + ] + : []), + ], + }, }, - }, - ] + ] : []), ], should: [ - { rank_feature: { boost: 5, field: "trancheEffectifsUniteLegaleRank" } }, - { rank_feature: { boost: 5, field: "etablissementsUniteLegaleRank" } }, - { rank_feature: { boost: 5, field: "caractereEmployeurEtablissementRank" } }, + { + rank_feature: { + boost: 5, + field: "trancheEffectifsUniteLegaleRank", + }, + }, + { + rank_feature: { boost: 5, field: "etablissementsUniteLegaleRank" }, + }, + { + rank_feature: { + boost: 5, + field: "caractereEmployeurEtablissementRank", + }, + }, { match: { etablissementSiege: { boost: 10, query: "true" } } }, - { match: { etatAdministratifEtablissement: { boost: 10, query: "A" } } }, + { + match: { + etatAdministratifEtablissement: { boost: 10, query: "A" }, + }, + }, ], }, }, @@ -132,6 +216,10 @@ export const getElasticQueryParams = (req) => { const codesPostaux = req.query["codesPostaux"] || []; const departements = req.query["departements"] || []; const tranchesEffectifs = req.query["tranchesEffectifs"] || []; + const dirigeant = req.query["dirigeant"] + ? JSON.parse(req.query["dirigeant"]) + : null; + let etats = req.query["etats"] || []; const siege = (req.query["siege"] || "").trim(); @@ -148,6 +236,7 @@ export const getElasticQueryParams = (req) => { departements, codesPostaux, tranchesEffectifs, + dirigeant, }; };