diff --git a/packages/code-du-travail-frontend/__tests__/contributions.test.tsx b/packages/code-du-travail-frontend/__tests__/contributions.test.tsx deleted file mode 100644 index c030a03067..0000000000 --- a/packages/code-du-travail-frontend/__tests__/contributions.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { render } from "@testing-library/react"; -import React from "react"; - -import PageContribution from "../pages/contribution/[slug]"; -import { ElasticSearchContribution } from "@socialgouv/cdtn-types"; - -const contribution = { - source: "contributions", - linkedContent: [], - references: [], - idcc: "", - metas: { - title: "SEO Title", - description: "SEO Description", - }, - title: "La période d’essai peut-elle être renouvelée ?", - breadcrumbs: [], -} as Partial as any; - -describe("", () => { - it("should render title with cc name in it", () => { - const { getByRole } = render( - - ); - const titreH1 = getByRole("heading", { level: 1 }); - expect(titreH1.textContent).toBe( - "La période d’essai peut-elle être renouvelée ?" - ); - }); - it("should render title with only question", () => { - contribution.ccnShortTitle = "Ce short title fait plus de 15 caractères"; - const { getByRole } = render( - - ); - const titreH1 = getByRole("heading", { level: 1 }); - expect(titreH1.textContent).toBe( - "La période d’essai peut-elle être renouvelée ?" - ); - }); - it("should render title with linked content with no description", () => { - let contribution = { - source: "contributions", - linkedContent: [{ source: "", title: "My link", slug: "" }], - references: [], - idcc: "", - metas: { - title: "SEO Title", - description: "SEO Description", - }, - title: "La période d’essai peut-elle être renouvelée ?", - } as any; - const { getByRole } = render( - - ); - const titreH3 = getByRole("heading", { level: 3 }); - expect(titreH3.textContent).toBe("My link"); - }); -}); diff --git a/packages/code-du-travail-frontend/app/contribution/[slug]/page.tsx b/packages/code-du-travail-frontend/app/contribution/[slug]/page.tsx new file mode 100644 index 0000000000..bd8334204a --- /dev/null +++ b/packages/code-du-travail-frontend/app/contribution/[slug]/page.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { DsfrLayout } from "../../../src/modules/layout"; +import { notFound } from "next/navigation"; +import { generateDefaultMetadata } from "../../../src/modules/common/metas"; +import { fetchRelatedItems } from "../../../src/modules/documents"; +import { + ContributionLayout, + fetchContributionBySlug, +} from "../../../src/modules/contributions"; + +export async function generateMetadata({ params }) { + const { metas } = await getContribution(params.slug); + + return generateDefaultMetadata({ + title: metas.title, + description: metas.description, + path: `/contribution/${params.slug}`, + }); +} + +async function Contribution({ params }) { + const contribution = await getContribution(params.slug); + return ( + + + + ); +} + +const getContribution = async (slug: string) => { + const contribution = await fetchContributionBySlug(slug); + + if (!contribution) { + return notFound(); + } + return contribution; +}; + +export default Contribution; diff --git a/packages/code-du-travail-frontend/app/outils/convention-collective/entreprise/[slug]/page.tsx b/packages/code-du-travail-frontend/app/outils/convention-collective/entreprise/[slug]/page.tsx index 98552d574e..1a9b9b5b65 100644 --- a/packages/code-du-travail-frontend/app/outils/convention-collective/entreprise/[slug]/page.tsx +++ b/packages/code-du-travail-frontend/app/outils/convention-collective/entreprise/[slug]/page.tsx @@ -7,7 +7,7 @@ import { import { notFound } from "next/navigation"; import { generateDefaultMetadata } from "../../../../../src/modules/common/metas"; import { ElasticTool } from "../../../../../src/modules/outils/type"; -import { EnterpriseAgreementSelection } from "../../../../../src/modules/enterprise"; +import { EnterpriseAgreementSelectionLink } from "../../../../../src/modules/enterprise"; import { searchEnterprises } from "../../../../../src/modules/enterprise/queries"; import { agreementRelatedItems } from "../../../../../src/modules/convention-collective/agreementRelatedItems"; import { SITE_URL } from "../../../../../src/config"; @@ -38,7 +38,7 @@ async function AgreementSelectionPage({ params }) { relatedItems={agreementRelatedItems} description={tool.description} > - + ); diff --git a/packages/code-du-travail-frontend/app/widgets/convention-collective/entreprise/[slug]/page.tsx b/packages/code-du-travail-frontend/app/widgets/convention-collective/entreprise/[slug]/page.tsx index a4127a5afb..5b93ad5e13 100644 --- a/packages/code-du-travail-frontend/app/widgets/convention-collective/entreprise/[slug]/page.tsx +++ b/packages/code-du-travail-frontend/app/widgets/convention-collective/entreprise/[slug]/page.tsx @@ -6,7 +6,7 @@ import { import { notFound } from "next/navigation"; import { generateDefaultMetadata } from "../../../../../src/modules/common/metas"; import { ElasticTool } from "../../../../../src/modules/outils/type"; -import { EnterpriseAgreementSelection } from "../../../../../src/modules/enterprise"; +import { EnterpriseAgreementSelectionLink } from "../../../../../src/modules/enterprise"; import { searchEnterprises } from "../../../../../src/modules/enterprise/queries"; import { SITE_URL } from "../../../../../src/config"; @@ -27,7 +27,7 @@ async function AgreementSelectionPage({ params }) { }); return ( - + ); } diff --git a/packages/code-du-travail-frontend/cypress/integration/html-validation/validate-html.spec.ts b/packages/code-du-travail-frontend/cypress/integration/html-validation/validate-html.spec.ts index 716e56758b..21c929e9e2 100644 --- a/packages/code-du-travail-frontend/cypress/integration/html-validation/validate-html.spec.ts +++ b/packages/code-du-travail-frontend/cypress/integration/html-validation/validate-html.spec.ts @@ -13,9 +13,9 @@ export const localConfig: ConfigData = { "aria-label-misuse": "off", "long-title": "off", "script-type": "off", - "no-dup-id": "off", "wcag/h63": "off", - "wcag/h32": "off", + "no-redundant-role": "off", + "no-missing-references": "off", }, }; diff --git a/packages/code-du-travail-frontend/cypress/integration/light/contributions/contributions.spec.ts b/packages/code-du-travail-frontend/cypress/integration/light/contributions/contributions.spec.ts index b24c0cc4da..016aa1a053 100644 --- a/packages/code-du-travail-frontend/cypress/integration/light/contributions/contributions.spec.ts +++ b/packages/code-du-travail-frontend/cypress/integration/light/contributions/contributions.spec.ts @@ -28,15 +28,8 @@ describe("Contributions", () => { "La période d’essai peut-elle être renouvelée ?" ); - cy.get("div > p > span") - .invoke("text") - .should("match", /Mis à jour le/); - cy.get("div > p > span") - .invoke("text") - .should("match", /\d\d\/\d\d\/\d\d\d\d/); - cy.contains( - "Accéder aux informations générales sans renseigner ma convention collective" + "Afficher les informations sans sélectionner une convention collective" ).click(); cy.get("body").should("contain", "Que dit le code du travail"); cy.get("body").should( @@ -45,18 +38,17 @@ describe("Contributions", () => { ); cy.get("body").should("contain", "Références"); cy.get("body").should("contain", "L1221-21"); - cy.get("body").should("contain", "Pour aller plus loin"); }); it("je vois une page contribution pour une CC", () => { cy.visit("/contribution/675-la-periode-dessai-peut-elle-etre-renouvelee"); cy.get("h1").should( "have.text", - "La période d’essai peut-elle être renouvelée ?" + "La période d’essai peut-elle être renouvelée ? Maisons à succursales de vente au détail d'habillement" ); cy.get("h2").should( "contain", - "Votre convention collective est Maisons à succursales de vente au détail d'habillement (IDCC 0675)" + "Maisons à succursales de vente au détail d'habillement (IDCC 0675)" ); cy.get("body").should( @@ -65,16 +57,14 @@ describe("Contributions", () => { ); cy.get("a") - .contains( - "la convention collective Maisons à succursales de vente au détail d'habillement" - ) + .contains("Maisons à succursales de vente au détail d'habillement") .should( "have.attr", "href", "/convention-collective/675-maisons-a-succursales-de-vente-au-detail-dhabillement" ); - cy.get("h2").should("contain", "Pour aller plus loin"); - cy.get("h3").should( + + cy.get("a").should( "contain", "Demande d’accord du salarié pour le renouvellement d’une période d’essai" ); @@ -86,8 +76,8 @@ describe("Contributions", () => { ); cy.get("h1").should( "have.text", - "Combien de fois le contrat de travail peut-il être renouvelé ?" + "Combien de fois le contrat de travail peut-il être renouvelé ? Métallurgie" ); - cy.get('[aria-expanded="true"]').find("h3").should("contain", "CDD"); + cy.get('[aria-expanded="true"]').should("contain", "CDD"); }); }); diff --git a/packages/code-du-travail-frontend/cypress/integration/light/conventions-collectives.spec.ts b/packages/code-du-travail-frontend/cypress/integration/light/conventions-collectives.spec.ts index 1dcba15c36..de15cff2c3 100644 --- a/packages/code-du-travail-frontend/cypress/integration/light/conventions-collectives.spec.ts +++ b/packages/code-du-travail-frontend/cypress/integration/light/conventions-collectives.spec.ts @@ -1,23 +1,29 @@ describe("Conventions collectives", () => { it("je vois la liste de toutes les cc", () => { cy.visit("/"); - cy.findByRole("heading", { level: 1 }) - .should("have.text", "Bienvenue sur le Code du travail numérique") - .click(); - cy.get("#fr-header-main-navigation") + cy.findByRole("heading", { level: 1 }).should( + "have.text", + "Bienvenue sur le Code du travail numérique" + ); + + cy.get("#fr-header-main-navigation a") .contains("Votre convention collective") .click(); + cy.urlEqual("/convention-collective"); - cy.findByRole("heading", { level: 1 }) - .should("have.text", "Votre convention collective") - .click(); + cy.findByRole("heading", { level: 1 }).should( + "have.text", + "Votre convention collective" + ); cy.get("body").should( "contain", "Les conventions collectives présentées sont les plus représentatives en termes de nombre de salariés" ); cy.get("#content a").should("have.length", 49); + cy.get("#content a").first().click(); + cy.urlEqual( "/convention-collective/2941-aide-accompagnement-soins-et-services-a-domicile-bad" ); @@ -25,6 +31,7 @@ describe("Conventions collectives", () => { .eq(0) .find('[data-accordion-component="AccordionItemButton"]') .should("have.length", 6); + cy.get('[data-accordion-component="Accordion"]') .eq(0) .find('[data-accordion-component="AccordionItemButton"]') @@ -71,9 +78,14 @@ describe("Conventions collectives", () => { .find('[data-accordion-component="AccordionItemButton"]') .first() .click(); - cy.get('[data-accordion-component="AccordionItem"] a').first().click(); + cy.get('[data-accordion-component="AccordionItem"] a') + .first() + .contains( + "Quelles sont les conditions d’indemnisation pendant le congé de maternité" + ) + .click(); cy.urlEqual( - "/convention-collective/2941-aide-accompagnement-soins-et-services-a-domicile-bad" + "/contribution/2941-quelles-sont-les-conditions-dindemnisation-pendant-le-conge-de-maternite" ); }); diff --git a/packages/code-du-travail-frontend/cypress/integration/light/outils/trouver-sa-cc-recherche-entreprise.spec.ts b/packages/code-du-travail-frontend/cypress/integration/light/outils/trouver-sa-cc-recherche-entreprise.spec.ts index 9b6ab0d9cd..eedbdc9584 100644 --- a/packages/code-du-travail-frontend/cypress/integration/light/outils/trouver-sa-cc-recherche-entreprise.spec.ts +++ b/packages/code-du-travail-frontend/cypress/integration/light/outils/trouver-sa-cc-recherche-entreprise.spec.ts @@ -30,7 +30,7 @@ describe("Outil - Trouver sa convention collective", () => { cy.canonicalUrlEqual("/outils/convention-collective"); cy.contains("BOUILLON PIGALLE").click(); - cy.contains("1 convention collective trouvée pour :"); + cy.contains("1 convention collective trouvée :"); cy.contains("Précédent").click(); cy.selectByLabel("Nom de votre entreprise ou numéro Siren/Siret") @@ -54,7 +54,7 @@ describe("Outil - Trouver sa convention collective", () => { cy.selectByLabel("Code postal ou Ville (optionnel)").clear(); cy.get('button[type="submit"]').last().click(); cy.contains("CARREFOUR BANQUE").click(); - cy.contains("2 conventions collectives trouvées pour :"); + cy.contains("2 conventions collectives trouvées :"); cy.contains("Banque") .should("have.prop", "href") .and( diff --git a/packages/code-du-travail-frontend/pages/contribution/[slug].tsx b/packages/code-du-travail-frontend/pages/contribution/[slug].tsx deleted file mode 100644 index d2e960d1a8..0000000000 --- a/packages/code-du-travail-frontend/pages/contribution/[slug].tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from "react"; -import Answer from "../../src/common/Answer"; -import Metas from "../../src/common/Metas"; -import { Layout } from "../../src/layout/Layout"; -import { - ContributionElasticDocument, - ElasticSearchContribution, - ElasticSearchContributionConventionnelle, - ElasticSearchContributionGeneric, -} from "@socialgouv/cdtn-types"; -import ContributionGeneric from "../../src/contributions/ContributionGeneric"; -import ContributionCC from "../../src/contributions/ContributionCC"; -import { getBySourceAndSlugItems } from "../../src/api"; - -type Props = { - contribution: ElasticSearchContribution; -}; - -function PageContribution(props: Props): React.ReactElement { - return ( - - - - {props.contribution.idcc === "0000" ? ( - - ) : ( - - )} - - - ); -} - -export const getServerSideProps = async ({ query }) => { - const data = await getBySourceAndSlugItems( - "contributions", - query.slug - ); - if (!data?._source) { - return { - notFound: true, - }; - } - return { - props: { - contribution: data._source, - }, - }; -}; - -export default PageContribution; diff --git a/packages/code-du-travail-frontend/panda.config.ts b/packages/code-du-travail-frontend/panda.config.ts index de7a0db417..5f0df31fa2 100644 --- a/packages/code-du-travail-frontend/panda.config.ts +++ b/packages/code-du-travail-frontend/panda.config.ts @@ -16,6 +16,11 @@ export default defineConfig({ }, }, }, + globalCss: { + ".fr-table__content table, .fr-table__content table *": { + whiteSpace: "normal", + }, + }, outdir: "src/styled-system", outExtension: "js", importMap: "@styled-system", diff --git a/packages/code-du-travail-frontend/src/contributions/__tests__/ReferencesJuridique.test.tsx b/packages/code-du-travail-frontend/src/contributions/__tests__/ReferencesJuridique.test.tsx index 7d7394965b..768c824f2a 100644 --- a/packages/code-du-travail-frontend/src/contributions/__tests__/ReferencesJuridique.test.tsx +++ b/packages/code-du-travail-frontend/src/contributions/__tests__/ReferencesJuridique.test.tsx @@ -1,5 +1,4 @@ import { render } from "@testing-library/react"; -import DisplayContentContribution from "../DisplayContentContribution"; import { ReferencesJuridiques } from "../References"; describe("ReferencesJuridiques", () => { diff --git a/packages/code-du-travail-frontend/src/contributions/__tests__/__snapshots__/makeArticlesLinks.test.tsx.snap b/packages/code-du-travail-frontend/src/contributions/__tests__/__snapshots__/makeArticlesLinks.test.tsx.snap deleted file mode 100644 index 551451cfba..0000000000 --- a/packages/code-du-travail-frontend/src/contributions/__tests__/__snapshots__/makeArticlesLinks.test.tsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should detect Article D. 3123-5 correctly 1`] = `"Article [D. 3123-5](/code-du-travail/d3123-5)"`; - -exports[`should detect Article L 3123-5 correctly 1`] = `"Article [L 3123-5](/code-du-travail/l3123-5)"`; - -exports[`should detect Article L-3123-5 correctly 1`] = `"Article [L-3123-5](/code-du-travail/l3123-5)"`; - -exports[`should detect Article L-3123-5, L.3123-7, L 3123-52-1, L. 3123-52-1 correctly 1`] = `"Article [L-3123-5](/code-du-travail/l3123-5), [L.3123-7](/code-du-travail/l3123-7), [L 3123-52-1](/code-du-travail/l3123-52-1), [L. 3123-52-1](/code-du-travail/l3123-52-1)"`; - -exports[`should detect Article L3123-5 correctly 1`] = `"Article [L3123-5](/code-du-travail/l3123-5)"`; - -exports[`should detect Article L3123-5 et [L234-12](http://travail.gouv.fr) correctly 1`] = `"Article [L3123-5](/code-du-travail/l3123-5) et [L234-12](http://travail.gouv.fr)"`; - -exports[`should detect Article L3123-5 et L234-12 correctly 1`] = `"Article [L3123-5](/code-du-travail/l3123-5) et [L234-12](/code-du-travail/l234-12)"`; - -exports[`should detect Article L3123-5, L3123-7, L3123-52-1 correctly 1`] = `"Article [L3123-5](/code-du-travail/l3123-5), [L3123-7](/code-du-travail/l3123-7), [L3123-52-1](/code-du-travail/l3123-52-1)"`; - -exports[`should detect Article L3123-5, L3123-7, L3123-52-1 et L3123-7, L3123-52-1 correctly 1`] = `"Article [L3123-5](/code-du-travail/l3123-5), [L3123-7](/code-du-travail/l3123-7), [L3123-52-1](/code-du-travail/l3123-52-1) et [L3123-7](/code-du-travail/l3123-7), [L3123-52-1](/code-du-travail/l3123-52-1)"`; - -exports[`should detect Article R.3123-5 correctly 1`] = `"Article [R.3123-5](/code-du-travail/r3123-5)"`; - -exports[`should detect D12 correctly 1`] = `"D12"`; - -exports[`should detect XD2432-1 correctly 1`] = `"XD2432-1"`; diff --git a/packages/code-du-travail-frontend/src/contributions/__tests__/makeArticlesLinks.test.tsx b/packages/code-du-travail-frontend/src/contributions/__tests__/makeArticlesLinks.test.tsx deleted file mode 100644 index 1e47275b6a..0000000000 --- a/packages/code-du-travail-frontend/src/contributions/__tests__/makeArticlesLinks.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import makeArticlesLinks from "../makeArticlesLinks"; - -const tests = [ - "Article L3123-5", - "Article R.3123-5", - "Article D. 3123-5", - "Article L 3123-5", - "Article L-3123-5", - "Article L3123-5 et L234-12", - "Article L3123-5 et [L234-12](http://travail.gouv.fr)", - "Article L3123-5, L3123-7, L3123-52-1", - "Article L3123-5, L3123-7, L3123-52-1 et L3123-7, L3123-52-1", - "Article L-3123-5, L.3123-7, L 3123-52-1, L. 3123-52-1", - "XD2432-1", - "D12", -]; - -tests.forEach((t) => { - test(`should detect ${t} correctly`, () => { - expect(makeArticlesLinks(t)).toMatchSnapshot(); - }); -}); diff --git a/packages/code-du-travail-frontend/src/contributions/makeArticlesLinks.tsx b/packages/code-du-travail-frontend/src/contributions/makeArticlesLinks.tsx deleted file mode 100644 index 9aa8b0b489..0000000000 --- a/packages/code-du-travail-frontend/src/contributions/makeArticlesLinks.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// normalize article reference for slugs : l123-4-5 -const normalize = (str) => - str - .replace(/[.\s]+/g, "") - .replace(/^([LRD])-/, "$1") - .toLowerCase(); - -// basic cdt article matcher for internal links -// create relative markdown links -const makeArticlesLinks = (markdown) => { - const articleRegex = /([^[\w])([LRD][.-]?\s*\d+[^\s]+)\b/gi; - let match; - let str2 = markdown; - while ((match = articleRegex.exec(markdown))) { - str2 = str2.replace( - new RegExp(`[^[](${match[2]})`), - (_, match2) => - `${match[1]}[${match[2]}](/code-du-travail/${normalize(match2)})` - ); - } - return str2; -}; - -export default makeArticlesLinks; diff --git a/packages/code-du-travail-frontend/src/modules/Location/LocationSearchInput.tsx b/packages/code-du-travail-frontend/src/modules/Location/LocationSearchInput.tsx index a98f688af2..456661f456 100644 --- a/packages/code-du-travail-frontend/src/modules/Location/LocationSearchInput.tsx +++ b/packages/code-du-travail-frontend/src/modules/Location/LocationSearchInput.tsx @@ -20,7 +20,7 @@ export const LocationSearchInput = ({ defaultValue, }: Props) => { const [postalCode, setPostalCode] = useState(); - function itemToString(item: ApiGeoResult | null) { + function itemToString(item: ApiGeoResult | undefined) { return item ? `${item.nom} (${postalCode ?? (item.codesPostaux.length > 1 ? item.codeDepartement : item.codesPostaux[0])})` : ""; diff --git a/packages/code-du-travail-frontend/src/modules/common/Autocomplete/Autocomplete.tsx b/packages/code-du-travail-frontend/src/modules/common/Autocomplete/Autocomplete.tsx index 87025e2a27..352e6c25fe 100644 --- a/packages/code-du-travail-frontend/src/modules/common/Autocomplete/Autocomplete.tsx +++ b/packages/code-du-travail-frontend/src/modules/common/Autocomplete/Autocomplete.tsx @@ -4,7 +4,7 @@ import Image from "next/image"; import Button from "@codegouvfr/react-dsfr/Button"; import Input, { InputProps } from "@codegouvfr/react-dsfr/Input"; import { useCombobox } from "downshift"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Spinner from "../Spinner.svg"; import { css } from "@styled-system/css"; import { redirect } from "next/navigation"; @@ -14,7 +14,7 @@ export type AutocompleteProps = InputProps & { onChange?: (value: K | undefined) => void; onError?: (value: string) => void; onSearch?: (query: string, results: K[]) => void; - displayLabel: (item: K | null) => string; + displayLabel: (item: K | undefined) => string; search: (search: string) => Promise; dataTestId?: string; lineAsLink?: (value: K) => string; @@ -36,18 +36,21 @@ export const Autocomplete = ({ stateRelatedMessage, hintText, dataTestId, - displayNoResult, defaultValue, + displayNoResult, }: AutocompleteProps) => { - const [value, setValue] = useState( - displayLabel(defaultValue ?? null) - ); + const [value, setValue] = useState(displayLabel(defaultValue)); const [loading, setLoading] = useState(false); const [selectedResult, setSelectedResult] = useState( defaultValue ); + useEffect(() => { + if (defaultValue) { + setSelectedResult(defaultValue); + setValue(displayLabel(defaultValue)); + } + }, [defaultValue]); const [inputRef, setInputRef] = useState(); - const [suggestions, setSuggestions] = useState([]); const { isOpen, @@ -56,6 +59,7 @@ export const Autocomplete = ({ highlightedIndex, getItemProps, } = useCombobox({ + defaultInputValue: displayLabel(defaultValue), items: suggestions, itemToString: displayLabel, selectedItem: selectedResult, @@ -63,7 +67,8 @@ export const Autocomplete = ({ setSelectedResult(changes.selectedItem); setValue(changes.inputValue ?? ""); if (onChange) onChange(changes.selectedItem); - if (lineAsLink) redirect(lineAsLink(changes.selectedItem)); + if (lineAsLink && changes.selectedItem) + redirect(lineAsLink(changes.selectedItem)); }, }); return ( @@ -96,7 +101,7 @@ export const Autocomplete = ({ Effacer la sélection )} - {(loading || (lineAsLink && selectedResult)) && ( + {loading && ( ({ onSearch?.(inputValue, results); setSuggestions(results); } catch (error) { - onError?.(error); + onError?.(error.message); setSuggestions([]); } finally { setLoading(false); diff --git a/packages/code-du-travail-frontend/src/modules/common/__tests__/useLocalStorage.test.tsx b/packages/code-du-travail-frontend/src/modules/common/__tests__/useLocalStorage.test.tsx new file mode 100644 index 0000000000..d48c3b8757 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/common/__tests__/useLocalStorage.test.tsx @@ -0,0 +1,36 @@ +import { act, render } from "@testing-library/react"; +import React from "react"; + +import { useLocalStorageForAgreement } from "../useLocalStorage"; + +function renderApp(initialValue) { + function App() { + const [value, setValue] = useLocalStorageForAgreement(initialValue); + return ( +
+

{value}

+ +
+ ); + } + + return render(); +} + +describe("useLocalStorageForAgreement", () => { + it("initializes value", () => { + const { getByTestId } = renderApp("hello cdtn"); + const valueElement = getByTestId("value"); + expect(valueElement.innerHTML).toBe("hello cdtn"); + }); + it("updates value", () => { + const { getByTestId } = renderApp("bar"); + act(() => { + getByTestId("button").click(); + }); + const valueElement = getByTestId("value"); + expect(valueElement.innerHTML).toBe("updated!"); + }); +}); diff --git a/packages/code-du-travail-frontend/src/modules/common/useLocalStorage.ts b/packages/code-du-travail-frontend/src/modules/common/useLocalStorage.ts new file mode 100644 index 0000000000..52c97f405b --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/common/useLocalStorage.ts @@ -0,0 +1,85 @@ +import { Agreement } from "@socialgouv/cdtn-types"; +import { useCallback, useEffect, useState } from "react"; +import { EnterpriseAgreement } from "../enterprise"; + +export const STORAGE_KEY_AGREEMENT = "convention"; + +export function useLocalStorageForAgreementOnPageLoad(): [ + EnterpriseAgreement | undefined, + (a?: EnterpriseAgreement) => void, +] { + const [value, setValue] = useState(null); + + useEffect(() => { + updateValue(getAgreementFromLocalStorage()); + }, []); + + const updateValue = useCallback( + (value) => { + setValue(value); + saveAgreementToLocalStorage(value); + }, + [JSON.stringify(value)] + ); + + return [value ?? undefined, updateValue]; +} + +export function useLocalStorageForAgreement( + defaultValue?: Agreement +): [Agreement | any, (a?: any) => void] { + const [value, setValue] = useState( + getAgreementFromLocalStorage() ?? defaultValue + ); + const updateValue = useCallback( + (value) => { + setValue(value); + saveAgreementToLocalStorage(value); + }, + [value] + ); + + return [value, updateValue]; +} + +export const saveAgreementToLocalStorage = ( + agreement?: EnterpriseAgreement | null +) => { + try { + if (window?.localStorage) { + if (agreement) { + window.localStorage.setItem( + STORAGE_KEY_AGREEMENT, + JSON.stringify(agreement) + ); + } else { + window.localStorage.removeItem(STORAGE_KEY_AGREEMENT); + } + } + } catch (e) { + console.error(e); + } +}; + +export const getAgreementFromLocalStorage = (): + | EnterpriseAgreement + | undefined => { + try { + if (window?.localStorage) { + const data = window.localStorage.getItem(STORAGE_KEY_AGREEMENT); + return data ? JSON.parse(data) : undefined; + } + } catch (e) { + console.error(e); + } +}; + +export const removeAgreementFromLocalStorage = () => { + try { + if (window?.localStorage) { + window.localStorage.removeItem(STORAGE_KEY_AGREEMENT); + } + } catch (e) { + console.error(e); + } +}; diff --git a/packages/code-du-travail-frontend/src/modules/common/utils.ts b/packages/code-du-travail-frontend/src/modules/common/utils.ts new file mode 100644 index 0000000000..0b6e70a5fb --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/common/utils.ts @@ -0,0 +1,2 @@ +export const removeCCNumberFromSlug = (slug: string): string => + slug.split("-").slice(1).join("-"); diff --git a/packages/code-du-travail-frontend/src/modules/contributions/ContributionAgreemeentSelect.tsx b/packages/code-du-travail-frontend/src/modules/contributions/ContributionAgreemeentSelect.tsx new file mode 100644 index 0000000000..93db2df3c2 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/ContributionAgreemeentSelect.tsx @@ -0,0 +1,54 @@ +"use client"; +import React from "react"; +import { Button } from "@codegouvfr/react-dsfr/Button"; +import { Card } from "@codegouvfr/react-dsfr/Card"; +import { css } from "@styled-system/css"; +import { fr } from "@codegouvfr/react-dsfr"; +import { removeCCNumberFromSlug } from "../common/utils"; + +import { Contribution } from "./type"; + +type Props = { + contribution: Contribution; +}; + +export function ContributionAgreementSelect({ contribution }: Props) { + const { slug } = contribution; + + return ( +
+

Votre convention collective

+ + +
+ ); +} + +const cardTitle = css({ + fontWeight: "normal!", +}); + +const block = css({ + background: "var(--background-alt-blue-cumulus)!", +}); diff --git a/packages/code-du-travail-frontend/src/modules/contributions/ContributionAgreementContent.tsx b/packages/code-du-travail-frontend/src/modules/contributions/ContributionAgreementContent.tsx new file mode 100644 index 0000000000..10e6bff945 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/ContributionAgreementContent.tsx @@ -0,0 +1,71 @@ +"use client"; +import React from "react"; +import { fr } from "@codegouvfr/react-dsfr"; +import { ContributionContent } from "./ContributionContent"; +import Html from "../common/Html"; +import Link from "../common/Link"; +import Accordion from "@codegouvfr/react-dsfr/Accordion"; +import { ListWithArrow } from "../common/ListWithArrow"; +import { RelatedItems } from "../common/RelatedItems"; +import { RelatedItem } from "../documents"; +import { Share } from "../common/Share"; +import { Contribution } from "./type"; + +type Props = { + contribution: Contribution; + relatedItems: { + items: RelatedItem[]; + title: string; + }[]; +}; + +export function ContributionAgreementContent({ + contribution, + relatedItems, +}: Props) { + const { title, metas } = contribution; + return ( +
+
+ + {contribution.references.length > 0 && ( + + { + if (!url) return <>; + return ( + + {title} + + ); + })} + /> + + )} +

+ Consultez les questions-réponses fréquentes pour la convention + collective{" "} + + {contribution.ccnShortTitle} + +

+ {contribution.messageBlock && ( +
+ <> +
Attention
+ {contribution.messageBlock} + +
+ )} +
+
+ {relatedItems && relatedItems[0].items.length > 0 && ( + + )} + +
+
+ ); +} diff --git a/packages/code-du-travail-frontend/src/modules/contributions/ContributionContent.tsx b/packages/code-du-travail-frontend/src/modules/contributions/ContributionContent.tsx new file mode 100644 index 0000000000..b0411cda15 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/ContributionContent.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +import DisplayContentContribution, { + ContentSP, + numberLevel, +} from "./DisplayContentContribution"; +import { Contribution } from "./type"; + +type Props = { + contribution: Contribution; + titleLevel: numberLevel; +}; + +export const ContributionContent = ({ contribution, titleLevel }: Props) => { + return ( +
+ {contribution.isFicheSP ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/ContributionGenericAgreementSearch.tsx b/packages/code-du-travail-frontend/src/modules/contributions/ContributionGenericAgreementSearch.tsx new file mode 100644 index 0000000000..6fab7657fd --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/ContributionGenericAgreementSearch.tsx @@ -0,0 +1,126 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { Button } from "@codegouvfr/react-dsfr/Button"; +import { css } from "@styled-system/css"; +import { fr } from "@codegouvfr/react-dsfr"; +import Image from "next/image"; +import AgreementSearch from "../convention-collective/AgreementSearch.svg"; + +import { AgreementSearchForm } from "../convention-collective/AgreementSearch/AgreementSearchForm"; +import { EnterpriseAgreement } from "../enterprise"; +import { + isAgreementSupported, + isAgreementUnextended, + isAgreementValid, +} from "./contributionUtils"; +import { Contribution } from "./type"; +import Link from "../common/Link"; + +type Props = { + onAgreementSelect: (agreement?: EnterpriseAgreement, mode?: string) => void; + onDisplayClick: (ev: React.MouseEvent) => void; + contribution: Contribution; + defaultAgreement?: EnterpriseAgreement; +}; + +export function ContributionGenericAgreementSearch({ + contribution, + onAgreementSelect, + onDisplayClick, + defaultAgreement, +}: Props) { + const { slug, isNoCDT } = contribution; + + const [selectedAgreement, setSelectedAgreement] = + useState(); + const [isValid, setIsValid] = useState( + defaultAgreement ? isAgreementValid(contribution, defaultAgreement) : false + ); + useEffect(() => { + setIsValid( + isAgreementValid(contribution, selectedAgreement ?? defaultAgreement) + ); + }, [selectedAgreement, defaultAgreement]); + const selectedAgreementAlert = (agreement: EnterpriseAgreement) => { + const isSupported = isAgreementSupported(contribution, agreement); + const isUnextended = isAgreementUnextended(contribution, agreement); + if (contribution.isNoCDT) { + if (isUnextended) + return ( + <> + Les dispositions de cette convention n’ont pas été étendues. Cela + signifie qu'elles ne s'appliquent qu'aux entreprises + adhérentes à l'une des organisations signataires de + l'accord. Dans ce contexte, nous ne sommes pas en mesure + d'identifier si cette règle s'applique ou non au sein de + votre entreprise. Vous pouvez toutefois consulter la convention + collective{" "} + + ici + {" "} + dans le cas où elle s'applique à votre situation. + + ); + if (!isSupported) + return ( + <> + Nous vous invitons à consulter votre convention collective qui peut + prévoir une réponse. Vous pouvez consulter votre convention + collective{" "} + + ici + + . +
+ {contribution.messageBlockGenericNoCDT} + + ); + } + if (!isSupported) + return <>Vous pouvez consulter les informations générales ci-dessous.; + }; + return ( +
+
+ Personnalisez la réponse avec votre convention collective +

+ Personnalisez la réponse avec votre convention collective +

+
+
+ { + onAgreementSelect(agreement, mode); + setSelectedAgreement( + isAgreementValid(contribution, agreement) ? agreement : undefined + ); + }} + selectedAgreementAlert={selectedAgreementAlert} + defaultAgreement={defaultAgreement} + /> + {((contribution.isNoCDT && isValid) || !contribution.isNoCDT) && ( + + )} +
+
+ ); +} + +const block = css({ + background: "var(--background-alt-blue-cumulus)!", +}); diff --git a/packages/code-du-travail-frontend/src/modules/contributions/ContributionGenericContent.tsx b/packages/code-du-travail-frontend/src/modules/contributions/ContributionGenericContent.tsx new file mode 100644 index 0000000000..0e583e0e4d --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/ContributionGenericContent.tsx @@ -0,0 +1,114 @@ +"use client"; +import React, { ReactNode, useEffect, useRef, useState } from "react"; +import { Button } from "@codegouvfr/react-dsfr/Button"; +import { fr } from "@codegouvfr/react-dsfr"; +import { Share } from "../common/Share"; +import { ContributionContent } from "./ContributionContent"; +import Html from "../common/Html"; +import Link from "../common/Link"; +import Accordion from "@codegouvfr/react-dsfr/Accordion"; +import { ListWithArrow } from "../common/ListWithArrow"; +import { RelatedItems } from "../common/RelatedItems"; +import { RelatedItem } from "../documents"; +import { Contribution } from "./type"; + +type Props = { + onDisplayClick: () => void; + contribution: Contribution; + alertText?: ReactNode; + relatedItems: { + items: RelatedItem[]; + title: string; + }[]; + displayGeneric: boolean; +}; + +export function ContributionGenericContent({ + contribution, + onDisplayClick, + alertText, + relatedItems, + displayGeneric, +}: Props) { + const { title, metas } = contribution; + const [displayContent, setDisplayContent] = useState(false); + const titleRef = useRef(null); + const scrollToTitle = () => { + setTimeout(() => { + titleRef?.current?.scrollIntoView({ behavior: "smooth" }); + }, 100); + }; + useEffect(() => { + setDisplayContent(displayGeneric); + }, [displayGeneric]); + useEffect(() => { + if (displayContent) { + scrollToTitle(); + } + }, [displayContent]); + return ( + <> + {!displayContent && ( + + )} +
+
+

+ Que dit le code du travail ? +

+ {alertText} + + {contribution.references.length > 0 && ( + + { + return ( + + {title} + + ); + })} + /> + + )} + {contribution.messageBlock && ( +
+
Attention
+ {contribution.messageBlock} +
+ )} +
+
+ + +
+
+ + ); +} diff --git a/packages/code-du-travail-frontend/src/modules/contributions/ContributionLayout.tsx b/packages/code-du-travail-frontend/src/modules/contributions/ContributionLayout.tsx new file mode 100644 index 0000000000..01cd45e931 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/ContributionLayout.tsx @@ -0,0 +1,169 @@ +"use client"; +import React, { useState } from "react"; +import { css } from "@styled-system/css"; +import { fr } from "@codegouvfr/react-dsfr"; +import { sources } from "../documents"; +import { Feedback } from "../layout/feedback"; +import Breadcrumb from "@codegouvfr/react-dsfr/Breadcrumb"; +import { useContributionTracking } from "./tracking"; +import { ContributionGenericAgreementSearch } from "./ContributionGenericAgreementSearch"; +import { isAgreementSupported, isAgreementValid } from "./contributionUtils"; +import { ContributionGenericContent } from "./ContributionGenericContent"; +import { ContributionAgreementSelect } from "./ContributionAgreemeentSelect"; +import { ContributionAgreementContent } from "./ContributionAgreementContent"; +import { Contribution } from "./type"; +import { useLocalStorageForAgreement } from "../common/useLocalStorage"; +import { SourceData } from "../layout/SourceData"; + +type Props = { + contribution: Contribution; +}; + +export type RelatedItem = { + title: string; + items: { + title: string; + url: string; + source: (typeof sources)[number]; + }[]; +}; + +export function ContributionLayout({ contribution }: Props) { + const getTitle = () => `/contribution/${slug}`; + const { date, title, slug, isGeneric, isNoCDT, isFicheSP, relatedItems } = + contribution; + + const [displayGeneric, setDisplayGeneric] = useState(false); + const [selectedAgreement, setSelectedAgreement] = + useLocalStorageForAgreement(); + const { + emitAgreementTreatedEvent, + emitAgreementUntreatedEvent, + emitDisplayAgreementContent, + emitDisplayGeneralContent, + emitDisplayGenericContent, + emitClickP1, + emitClickP2, + emitClickP3, + } = useContributionTracking(); + + return ( +
+ ({ + label: breadcrumb.label, + linkProps: { href: breadcrumb.slug }, + }))} + /> +

+ {title} + {!isGeneric && ( + <> + {" "} + + {contribution.ccnShortTitle} + + + )} +

+

+ {isFicheSP ? ( + + ) : ( + <>Mis à jour le : {contribution.date} + )} +

+ {isGeneric ? ( + { + setSelectedAgreement(agreement); + setDisplayGeneric(false); + if (!agreement) return; + switch (mode) { + case "p1": + emitClickP1(getTitle()); + break; + case "p2": + emitClickP2(getTitle()); + break; + } + if (isAgreementSupported(contribution, agreement)) { + emitAgreementTreatedEvent(agreement?.id); + } else { + emitAgreementUntreatedEvent(agreement?.id); + } + }} + onDisplayClick={(ev) => { + setDisplayGeneric(!displayGeneric); + if ( + !isAgreementValid(contribution, selectedAgreement) || + !selectedAgreement + ) { + ev.preventDefault(); + setDisplayGeneric(true); + if (selectedAgreement) { + emitDisplayGenericContent(getTitle()); + } else { + emitDisplayGeneralContent(getTitle()); + } + } else { + emitDisplayAgreementContent(getTitle()); + } + }} + defaultAgreement={selectedAgreement} + /> + ) : ( + + )} + {isGeneric && + !isNoCDT && + (!selectedAgreement || + !isAgreementValid(contribution, selectedAgreement)) && ( + { + emitClickP3(getTitle()); + }} + relatedItems={relatedItems} + displayGeneric={displayGeneric} + alertText={ + selectedAgreement && + !isAgreementSupported(contribution, selectedAgreement) && ( +

+ + Cette réponse correspond à ce que prévoit le code du + travail, elle ne tient pas compte des spécificités de la{" "} + {selectedAgreement.shortTitle} + +

+ ) + } + /> + )} + {!isGeneric && ( + + )} +
+ +
+
+ ); +} + +const h1Agreement = css({ + display: "block", + fontSize: "1rem", + fontWeight: "normal", + lineHeight: "normal", +}); diff --git a/packages/code-du-travail-frontend/src/modules/contributions/DisplayContentContribution.tsx b/packages/code-du-travail-frontend/src/modules/contributions/DisplayContentContribution.tsx new file mode 100644 index 0000000000..a1245bd2d0 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/DisplayContentContribution.tsx @@ -0,0 +1,309 @@ +import parse, { + DOMNode, + domToReact, + Element, + HTMLReactParserOptions, + Text, +} from "html-react-parser"; +import { xssWrapper } from "../../lib"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import { ElementType } from "react"; +import { AccordionWithAnchor } from "../common/AccordionWithAnchor"; +import { v4 as generateUUID } from "uuid"; +import { fr } from "@codegouvfr/react-dsfr"; +import { FicheServicePublic } from "../fiche-service-public/builder"; + +const DEFAULT_HEADING_LEVEL = 3; +export type numberLevel = 2 | 3 | 4 | 5 | 6; + +export const ContentSP = ({ raw, titleLevel }) => { + return ( + <> + {raw && ( +
+ +
+ )} + + ); +}; + +const mapItem = ( + titleLevel: numberLevel, + domNode: Element, + summary: Element +) => ({ + content: domToReact( + domNode.children as DOMNode[], + options((titleLevel + 1) as numberLevel) + ), + title: domToReact(summary.children as DOMNode[], { + transform: (reactNode, domNode) => { + // @ts-ignore + if (domNode.children) { + // @ts-ignore + return domNode.children[0].data; + } + // @ts-ignore + return domNode.data; + }, + trim: true, + }), +}); +const mapToAccordion = (titleLevel: numberLevel, items) => { + const props = titleLevel <= 6 ? { titleLevel } : {}; + + return ( +
+ ({ + ...item, + ...(titleLevel === DEFAULT_HEADING_LEVEL + ? { id: undefined } + : { id: generateUUID() }), + }))} + titleAs={`h${titleLevel}`} + /> +
+ ); +}; + +function getFirstElementChild(domNode: Element) { + let child = domNode.children.shift(); + while (child && child.type !== "tag") { + child = domNode.children.shift(); + } + return child; +} + +function getNextFirstElement(domNode: Element) { + let next = domNode.next; + while (next && next.type !== "tag") { + next = next.next; + } + return next; +} + +const theadMaxRowspan = (tr: Element) => { + const rowspans = tr.children.map((child) => { + if (child.type === "tag" && child.name === "td") { + return parseInt(child.attribs["rowspan"] ?? -1); + } else { + return -1; + } + }); + const maxRowspan = rowspans.reduce( + (previousValue, currentValue) => + currentValue > previousValue ? currentValue : previousValue, + 0 + ); + return maxRowspan === -1 ? 1 : maxRowspan; +}; + +const getData = (el?: ChildNode) => { + if (!el) return ""; + if (el instanceof Text) { + return el.data; + } else { + let str = ""; + el.childNodes.forEach((node) => { + str += getData(node); + }); + return str; + } +}; + +const mapTbody = (tbody: Element) => { + let theadChildren: Element[] = []; + const firstLine = getFirstElementChild(tbody); + + if (firstLine) { + let maxRowspan = theadMaxRowspan(firstLine); + theadChildren.push(firstLine); + for (let i = 1; i < maxRowspan; i++) { + let child = getFirstElementChild(tbody); + if (child) { + theadChildren.push(child); + } + } + } + + return ( +
+
+
+
+ + {theadChildren.length > 0 && ( + <> + {theadChildren[0].children[0] && ( + + )} + + {theadChildren.map((child, rowIndex) => { + return ( + + {domToReact( + child.children.map((c) => ({ + ...c, + name: "th", + })) as DOMNode[], + { + trim: true, + } + )} + + ); + })} + + + )} + + {domToReact(tbody.children as DOMNode[], { trim: true })} + +
+ {getData(theadChildren[0].childNodes[0] as any)} +
+
+
+
+
+ ); +}; + +function getItem(domNode: Element, titleLevel: numberLevel) { + const summary = getFirstElementChild(domNode); + if (summary && summary.name === "summary") { + const mapI = mapItem(titleLevel, domNode, summary); + return mapI; + } +} + +function renderChildrenWithNoTrim(domNode) { + return domToReact(domNode.children as DOMNode[]); +} + +const getHeadingElement = (titleLevel: numberLevel, domNode) => { + const Tag = ("h" + titleLevel) as ElementType; + return titleLevel <= 6 ? ( + {renderChildrenWithNoTrim(domNode)} + ) : ( + + {renderChildrenWithNoTrim(domNode)} + + ); +}; + +const options = (titleLevel: numberLevel): HTMLReactParserOptions => { + let accordionTitleLevel = titleLevel; + let headingTitleLevel = titleLevel; + + return { + replace(domNode) { + if (domNode instanceof Element) { + if (domNode.name === "span" && domNode.attribs.class === "title") { + accordionTitleLevel = titleLevel + 1; + headingTitleLevel = titleLevel + 1; + return getHeadingElement(titleLevel, domNode); + } + if (domNode.name === "span" && domNode.attribs.class === "sub-title") { + accordionTitleLevel = titleLevel + 1; + return getHeadingElement(headingTitleLevel, domNode); + } + if (domNode.name === "details") { + const items: any[] = []; + let id = 0; + const item = getItem(domNode, accordionTitleLevel); + if (item) { + items.push({ ...item, id }); + } + let next = getNextFirstElement(domNode); + while (next && next.name === "details") { + id = id + 1; + const item = getItem(next, accordionTitleLevel); + if (item) { + items.push({ ...item, id }); + } + next = getNextFirstElement(next); + } + return items.length ? ( + mapToAccordion(accordionTitleLevel, items) + ) : ( + <> + ); + } + if (domNode.name === "table") { + const tableContent = getFirstElementChild(domNode); + if (tableContent?.name === "tbody") { + return mapTbody(tableContent); + } else { + return domNode; + } + } + if (domNode.name === "div" && domNode.attribs.class === "alert") { + return ( + + ); + } + if (domNode.name === "strong") { + // Disable trim on strong + return {renderChildrenWithNoTrim(domNode)}; + } + if (domNode.name === "ul") { + return ( +
    + {renderChildrenWithNoTrim(domNode)} +
+ ); + } + if (domNode.name === "em") { + // Disable trim on em + return {renderChildrenWithNoTrim(domNode)}; + } + if (domNode.name === "p") { + if (!domNode.children.length) { + return
; + } + // Disable trim on p + return ( +

+ {renderChildrenWithNoTrim(domNode)} +

+ ); + } + } + }, + trim: true, + }; +}; + +type Props = { + content: string; + titleLevel: numberLevel; +}; +const DisplayContentContribution = ({ + content, + titleLevel, +}: Props): string | JSX.Element | JSX.Element[] => { + return
{parse(xssWrapper(content), options(titleLevel))}
; +}; + +export default DisplayContentContribution; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/__tests__/DisplayContentContribution.test.tsx b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/DisplayContentContribution.test.tsx new file mode 100644 index 0000000000..3825b60f84 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/DisplayContentContribution.test.tsx @@ -0,0 +1,499 @@ +import { render } from "@testing-library/react"; +import DisplayContentContribution from "../DisplayContentContribution"; + +let count = 0; +jest.mock("uuid", () => ({ + v4: jest.fn(() => { + return "123" + count++; + }), +})); + +describe("DisplayContentContribution", () => { + describe("Headings", () => { + it(`should replace span with class "title" and "sub-titles" with heading`, () => { + const { baseElement } = render( + Mon titre + Mon sous titre`} + titleLevel={2} + > + ); + + expect(baseElement.firstChild).toMatchSnapshot(); + }); + it(`should replace span with with heading according to given title level`, () => { + const { getByText } = render( + Ceci est un titreCeci est un sous titre`} + titleLevel={4} + > + ); + + expect(getByText("Ceci est un titre").tagName).toEqual("H4"); + expect(getByText("Ceci est un sous titre").tagName).toEqual("H5"); + }); + it(`should not add headings higher than h6 for titles`, () => { + const { getByText } = render( + Ceci est un titreCeci est un sous titre`} + titleLevel={6} + > + ); + + expect(getByText("Ceci est un titre").tagName).toEqual("H6"); + expect(getByText("Ceci est un sous titre").tagName).toEqual("STRONG"); + }); + it(`should not add headings higher than h6 for accordion`, () => { + const { getByText } = render( + Ceci est un titre +
+ Ceci est un sous titre + Ceci est un sous sous titre +
+ `} + titleLevel={6} + >
+ ); + expect(getByText("Ceci est un titre")?.tagName).toEqual("BUTTON"); + expect(getByText("Ceci est un titre").parentElement?.tagName).toEqual( + "H6" + ); + expect(getByText("Ceci est un sous titre").tagName).toEqual("STRONG"); + expect(getByText("Ceci est un sous sous titre").tagName).toEqual( + "STRONG" + ); + }); + it(`should handle sub-title in accordion even if no title`, () => { + const { getByText } = render( + Ceci est un titre +
+ Ceci est un sous titre +
+ `} + titleLevel={4} + >
+ ); + expect(getByText("Ceci est un titre").tagName).toEqual("BUTTON"); + expect(getByText("Ceci est un titre").parentElement?.tagName).toEqual( + "H4" + ); + expect(getByText("Ceci est un sous titre").tagName).toEqual("H5"); + }); + }); + + describe("Accordions", () => { + it(`should replace details element by one accordion`, () => { + const { asFragment } = render( + Ceci est un titre +
+

Ceci est le body

+

+
+ `} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + it(`should not fail if no summary tag`, () => { + const { baseElement } = render( + + + Report ou suspension du préavis + +
+

+ En principe, le préavis de licenciement court de date à date sans + interruption, ni suspension. Dans certaines situations, il existe des + exceptions qui peuvent suspendre le déroulement du préavis. +

+
+ + Congés payés + +
+
+ + + Dates des congés fixées avant la notification du licenciement + + +
+

+ Des congés payés qui interviennent pendant le préavis et qui ont + été demandés à l'employeur avant la notification du licenciement + suspendent le préavis. Par conséquent, le préavis est prolongé + d'une durée équivalente à celle des congés. +

+
+
+
+ + + Dates des congés fixées après la notification du licenciement + + +
+

+ Des congés payés qui interviennent pendant le préavis et qui ont + été demandés à l'employeur après la notification du + licenciement ne suspendent pas le préavis. Par conséquent, le + préavis n'est pas prolongé d'une durée équivalente à celle des + congés. +

+
+
+
+ + Licenciement notifié pendant les congés payés + +
+

+ Dans ce cas, le préavis ne commencera qu'après les congés payés. +

+
+
+
+
+
+ `} + titleLevel={3} + >
+ ); + + expect(baseElement.firstChild).toMatchSnapshot(); + }); + it(`should replace multiple details element by one accordion`, () => { + const { baseElement } = render( + Ceci est le titre 1 +
+

Ceci est le body 1

+

+
+ +
Ceci est le titre 2 +
+

Ceci est le body 2

+

+
+
`} + titleLevel={3} + >
+ ); + + expect(baseElement).toMatchSnapshot(); + }); + it(`should replace details element within details element`, () => { + const { asFragment } = render( + Ceci est un titre +
+
Ceci est un sous titre +
+

Ceci est le body

+

+
+
+
+ `} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + it(`should replace details element with rich summary`, () => { + const { getByText } = render( + Ceci est un titre HELLO +
+

Ceci est le body

+

+
+ `} + titleLevel={3} + >
+ ); + + expect(getByText("Ceci est un titre HELLO")).toBeInTheDocument(); + }); + it(`should start title level to 4 if heading 3 before`, () => { + const { getByText } = render( + + HELLO +
+ Ceci est un titre +
+

Ceci est le body

+

+
+
+ `} + titleLevel={3} + >
+ ); + + expect(getByText("HELLO").tagName).toEqual("H3"); + expect(getByText("Ceci est un titre").tagName).toEqual("BUTTON"); + expect(getByText("Ceci est un titre").parentElement?.tagName).toEqual( + "H4" + ); + }); + it(`should handle title within nested accordion`, () => { + const { getByText } = render( + +
+ Ceci est un titre +
+
+ Ceci est un sous titre +
+ Ceci est un titre dans un accordion + Ceci est un sous-titre dans un accordion +
+
+
+
+ `} + titleLevel={4} + >
+ ); + + expect(getByText("Ceci est un titre").tagName).toEqual("BUTTON"); + expect(getByText("Ceci est un titre").parentElement?.tagName).toEqual( + "H4" + ); + expect(getByText("Ceci est un sous titre").tagName).toEqual("BUTTON"); + expect( + getByText("Ceci est un sous titre").parentElement?.tagName + ).toEqual("H5"); + expect(getByText("Ceci est un titre dans un accordion").tagName).toEqual( + "H6" + ); + expect( + getByText("Ceci est un sous-titre dans un accordion").tagName + ).toEqual("STRONG"); + }); + }); + + describe("Tables", () => { + it(`should add thead to table if not present and move table into a Table element`, () => { + const { asFragment } = render( + + + + +

Titre 1

+ + +

Pour les cadres, la prolongation ...

+
  • L’employeur et le salarié donnent par écrit ou par mail.

+ + + `} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + it(`should not change if thead is already present`, () => { + const { asFragment } = render( + + + + +

Titre 1

+ + + + +

Pour les cadres, la prolongation ...

+
  • L’employeur et le salarié donnent par écrit ou par mail.

+ + + `} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + + it(`should replace td by th in thead`, () => { + const { asFragment } = render( + + + + Titre 1 + Titre 2 + + + + +

Pour les cadres, la prolongation ...

+
  • L’employeur et le salarié donnent par écrit ou par mail.

+ + + `} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + + it(`should keep whitespace in specific tag`, () => { + const { asFragment } = render( + Ceci est un texte généré par tiptap avec des résidus de balise

`} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + + it(`should render correctly a table with multiple head lines`, () => { + const { baseElement } = render( + + + + +

+ Nature du contrat de mission +

+ + +

+ Durée maximale +

+ + + + +

+ Contrat de date à date +

+ + +

+ Contrat sans terme certain +

+ + + + +

Remplacement d’un salarié absent ou dont le contrat de travail est suspendu

+ + +

18 mois

+ + +

Fin de l’absence

+ + + + +`} + titleLevel={3} + >
+ ); + + expect(baseElement.firstChild).toMatchSnapshot(); + }); + }); + + it(`should return html`, () => { + const { asFragment } = render( + hello

`} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + it(`should keep whitespace in specific tag`, () => { + const { asFragment } = render( + Ceci est un texte généré par tiptap avec des résidus de balise

`} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + it(`should not remove space between strong and em tag in p tag`, () => { + const { asFragment } = render( + À noter : L'échelon professionnel du salarié est habituellement mentionné

`} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + + describe("Alerts", () => { + it(`should replace div with alert class to Alert component`, () => { + const { asFragment } = render( +

Attention : En l’absence d’écrit, l’employeur peut être condamné à une amende de 3.750 € ou 7.500 € en cas de récidive.

`} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + + it(`should replace div with alert class in li component to Alert component`, () => { + const { asFragment } = render( +

Le contrat de mission (intérim) doit :

  • Être écrit et rédigé en français (si conclu en France) ;

  • Être signé, dans un délai de 2 jours suivant la mise à disposition du salarié auprès de l'entreprise ; si l’employeur transmet le CDD au salarié après le délai de 2 jours, il s'expose au paiement d'une indemnité égale à 1 mois de salaire maximum.

  • Être établi en plusieurs exemplaires ; c'est-à-dire autant d'exemplaires que de parties au contrat. Chaque partie au contrat aura un exemplaire.

    Attention : En l’absence d’écrit, l’employeur peut être condamné à une amende de 3.750 € ou 7.500 € en cas de récidive.

`} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + + it(`should have space in table item for a strong and an other content`, () => { + const { asFragment } = render( +

Repos compensateur 

Majoration de salaire

Salariés

Une journée entière déterminée par roulement et par quinzaine

Pour les commerces avec une surface de vente supérieure à 400 m2,la loi prévoit une majoration de salaire d’au moins 30 % par rapport au salaire normalement dû pour une durée équivalente

Pour les commerces avec une surface de vente inférieure ou égale à 400 m2, la loi ne prévoit aucune majoration de salaire. Mais l’employeur, s’il le souhaite, ou un accord collectif, peuvent le prévoir.

Salariés de moins de 21 ans logés chez leur employeur

Un autre après-midi déterminé par roulement et par quinzaine

`} + titleLevel={3} + >
+ ); + + expect(asFragment().firstChild).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/code-du-travail-frontend/src/modules/contributions/__tests__/__snapshots__/DisplayContentContribution.test.tsx.snap b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/__snapshots__/DisplayContentContribution.test.tsx.snap new file mode 100644 index 0000000000..8e8bc17582 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/__snapshots__/DisplayContentContribution.test.tsx.snap @@ -0,0 +1,930 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DisplayContentContribution Accordions should not fail if no summary tag 1`] = ` +
+
+
+
+
+

+ +

+
+
+

+ + En principe, le préavis de licenciement court de date à date sans + interruption, ni suspension. Dans certaines situations, il existe des + exceptions qui peuvent suspendre le déroulement du préavis. + +

+
+
+
+

+ +

+
+
+
+
+
+
+ +
+
+
+

+ + Des congés payés qui interviennent pendant le préavis et qui ont + été demandés à l'employeur avant la notification du licenciement + suspendent le préavis. Par conséquent, le préavis est prolongé + d'une durée équivalente à celle des congés. + +

+
+
+
+
+
+ +
+
+
+

+ + Des congés payés qui interviennent pendant le préavis et qui ont + été demandés à l'employeur après la notification du + licenciement ne suspendent pas le préavis. Par conséquent, le + préavis n'est pas prolongé d'une durée équivalente à celle des + congés. + +

+
+
+
+
+
+ +
+
+
+

+ + Dans ce cas, le préavis ne commencera qu'après les congés payés. + +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DisplayContentContribution Accordions should replace details element by one accordion 1`] = ` +
+
+
+
+

+ +

+
+
+

+ Ceci est le body +

+
+
+
+
+
+
+
+`; + +exports[`DisplayContentContribution Accordions should replace details element within details element 1`] = ` +
+
+
+
+

+ +

+
+
+
+
+
+

+ +

+
+
+

+ Ceci est le body +

+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DisplayContentContribution Accordions should replace multiple details element by one accordion 1`] = ` + +
+
+
+
+
+

+ +

+
+
+

+ Ceci est le body 1 +

+
+
+
+
+
+

+ +

+
+
+

+ Ceci est le body 2 +

+
+
+
+
+
+
+
+
+ +`; + +exports[`DisplayContentContribution Alerts should have space in table item for a strong and an other content 1`] = ` +
+ + + + + + + + + + + + +
+

+ + Salariés + +

+
+

+ Une journée entière déterminée par roulement et par quinzaine +

+
+

+ Pour les commerces avec une surface de vente + + supérieure à 400 m2 + + , + + la loi prévoit une majoration de salaire d’au moins 30 % par rapport au salaire normalement dû pour une durée équivalente +

+

+ Pour les commerces avec une surface de vente + + inférieure ou égale à 400 m2 + + , la loi ne prévoit aucune majoration de salaire. Mais l’employeur, s’il le souhaite, ou un + + accord collectif + + , peuvent le prévoir. +

+
+

+ + Salariés de moins de 21 ans logés chez leur employeur + +

+
+

+ Un autre après-midi déterminé par roulement et par quinzaine +

+
+
+`; + +exports[`DisplayContentContribution Alerts should replace div with alert class in li component to Alert component 1`] = ` +
+
+

+ Le contrat de mission (intérim) doit : +

+
    +
  • +

    + Être + + écrit + + et + + rédigé + + en français (si conclu en France) ; +

    +
  • +
  • +

    + Être + + signé + + , dans un délai de + + 2 jours + + suivant la mise à disposition du salarié auprès de l'entreprise ; si l’employeur transmet le CDD au salarié après le délai de 2 jours, il s'expose au paiement d'une indemnité égale à 1 mois de salaire maximum. +

    +
  • +
  • +

    + Être établi + + en plusieurs exemplaires + + ; c'est-à-dire autant d'exemplaires que de parties au contrat. Chaque partie au contrat aura un exemplaire. +

    +

    +

    +

    + + Attention : + + En l’absence d’écrit, l’employeur peut être condamné à une amende de 3.750 € ou 7.500 € en cas de récidive. +

    +
    +
  • +
+
+
+`; + +exports[`DisplayContentContribution Alerts should replace div with alert class to Alert component 1`] = ` +
+
+
+

+ + Attention : + + En l’absence d’écrit, l’employeur peut être condamné à une amende de 3.750 € ou 7.500 € en cas de récidive. +

+
+
+
+`; + +exports[`DisplayContentContribution Headings should replace span with class "title" and "sub-titles" with heading 1`] = ` +
+
+

+ Mon titre +

+

+ Mon sous titre +

+
+
+`; + +exports[`DisplayContentContribution Tables should add thead to table if not present and move table into a Table element 1`] = ` +
+
+
+
+
+ + + + + + + + + + + + + +
+ + +
+ +

+ Titre 1 +

+
+

+ Pour les + + cadres + + , la prolongation ... +

+
+
    +
  • +

    + L’employeur et le salarié donnent par écrit ou par mail. +

    +
  • +
+
+
+
+
+
+
+`; + +exports[`DisplayContentContribution Tables should keep whitespace in specific tag 1`] = ` +
+

+ Ceci est un + + + + texte généré + + + + par + + tiptap + + avec des + + + + résidus + + + + de balise +

+
+`; + +exports[`DisplayContentContribution Tables should not change if thead is already present 1`] = ` +
+ + + + + + + +
+

+ Pour les + + cadres + + , la prolongation ... +

+
+
    +
  • +

    + L’employeur et le salarié donnent par écrit ou par mail. +

    +
  • +
+
+
+`; + +exports[`DisplayContentContribution Tables should render correctly a table with multiple head lines 1`] = ` +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+ + +
+

+ + Nature du contrat de mission + +

+
+

+ + Durée maximale + +

+
+

+ + Contrat de date à date + +

+
+

+ + Contrat sans terme certain + +

+
+

+ Remplacement d’un salarié absent ou dont le contrat de travail est suspendu +

+
+

+ 18 mois +

+
+

+ Fin de l’absence +

+
+
+
+
+
+
+
+`; + +exports[`DisplayContentContribution Tables should replace td by th in thead 1`] = ` +
+ + + + + + + +
+

+ Pour les + + cadres + + , la prolongation ... +

+
+
    +
  • +

    + L’employeur et le salarié donnent par écrit ou par mail. +

    +
  • +
+
+
+`; + +exports[`DisplayContentContribution should keep whitespace in specific tag 1`] = ` +
+

+ Ceci est un + + + + texte généré + + + + par + + tiptap + + avec des + + + + résidus + + + + de balise +

+
+`; + +exports[`DisplayContentContribution should not remove space between strong and em tag in p tag 1`] = ` +
+

+ + À noter : + + + + L'échelon professionnel du salarié est habituellement mentionné + +

+
+`; + +exports[`DisplayContentContribution should return html 1`] = ` +
+

+ hello +

+
+`; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/__tests__/contributions.test.tsx b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/contributions.test.tsx new file mode 100644 index 0000000000..fc5dd0a134 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/contributions.test.tsx @@ -0,0 +1,307 @@ +import { act, fireEvent, render, RenderResult } from "@testing-library/react"; +import React from "react"; +import { wait } from "@testing-library/user-event/dist/utils"; + +import { ContributionLayout } from "../ContributionLayout"; +import { ui } from "./ui"; +import { ui as ccUi } from "../../convention-collective/__tests__/ui"; +import { Contribution } from "../type"; +import { searchAgreement } from "../../convention-collective"; +import { sendEvent } from "../../utils"; + +const contribution = { + source: "contributions", + linkedContent: [], + references: [], + idcc: "0000", + date: "05/12/2023", + metas: { + title: "SEO Title", + description: "SEO Description", + }, + title: "La période d’essai peut-elle être renouvelée ?", + breadcrumbs: [], + slug: "slug", + type: "content", + content: "my content", + isGeneric: true, + isNoCdt: false, + ccSupported: ["0016", "3239"], + ccUnextended: ["0029"], + messageBlockGenericNoCDT: "message No CDT", +} as Partial as any; + +jest.mock("../../convention-collective/search", () => ({ + searchAgreement: jest.fn(), +})); + +jest.mock("../../utils", () => ({ + sendEvent: jest.fn(), +})); + +jest.mock("uuid", () => ({ + v4: jest.fn(() => ""), +})); + +jest.mock("next/navigation", () => ({ + redirect: jest.fn(), + usePathname: jest.fn(), +})); + +describe("", () => { + let rendering: RenderResult; + it("should render title only if generic", () => { + rendering = render(); + const titreH1 = rendering.getByRole("heading", { level: 1 }); + expect(titreH1.textContent).toBe( + "La période d’essai peut-elle être renouvelée ?" + ); + expect( + rendering.getByText("Mis à jour le : 05/12/2023") + ).toBeInTheDocument(); + }); + it("should render title with cc short name if contribution with CC", () => { + rendering = render( + + ); + const titreH1 = rendering.getByRole("heading", { level: 1 }); + expect(titreH1.textContent).toBe( + "La période d’essai peut-elle être renouvelée ? Nom de la CC" + ); + expect( + rendering.getByText("Mis à jour le : 05/12/2023") + ).toBeInTheDocument(); + const ccLink = rendering.getByRole("link", { name: "Nom de la CC" }); + expect(ccLink).toHaveAttribute("href", "/convention-collective/cc-slug"); + }); + describe("base", () => { + beforeEach(async () => { + window.localStorage.clear(); + rendering = render(); + }); + it("should display correctly when no agreement is selected", async () => { + fireEvent.click(ccUi.radio.agreementSearchOption.get()); + expect(ccUi.buttonDisplayInfo.query()).toBeInTheDocument(); + expect(ui.generic.linkDisplayInfo.query()).toBeInTheDocument(); + expect(ui.generic.title.query()).toBeInTheDocument(); + expect(rendering.getByText("my content")).toBeInTheDocument(); + fireEvent.click(ui.generic.linkDisplayInfo.get()); + expect(ui.generic.linkDisplayInfo.query()).not.toBeInTheDocument(); + expect(rendering.getByText("my content")).toBeInTheDocument(); + }); + + it("should display correctly when a treated agreement is selected", async () => { + (searchAgreement as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + id: "0016", + num: 16, + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635624", + shortTitle: + "Transports routiers et activités auxiliaires du transport", + slug: "16-transports-routiers-et-activites-auxiliaires-du-transport", + title: + "Convention collective nationale des transports routiers et activités auxiliaires du transport du 21 décembre 1950", + }, + ]) + ); + fireEvent.click(ccUi.radio.agreementSearchOption.get()); + expect(ui.generic.linkDisplayInfo.query()).toBeInTheDocument(); + act(() => { + fireEvent.change(ccUi.searchByName.input.get(), { + target: { value: "16" }, + }); + }); + await wait(); + + fireEvent.click(ccUi.searchByName.autocompleteLines.IDCC16.name.get()); + + expect(ui.generic.linkDisplayInfo.query()).not.toBeInTheDocument(); + expect(ccUi.buttonDisplayInfo.query()).toHaveAttribute( + "href", + "/contribution/16-slug" + ); + expect(ccUi.warning.nonTreatedAgreement.query()).not.toBeInTheDocument(); + expect(sendEvent).toHaveBeenCalledWith({ + action: "cc_select_traitée", + category: "outil", + name: "0016", + }); + }); + + it("should display correctly when a selecting agreement 3239", async () => { + fireEvent.click(ccUi.radio.enterpriseSearchOption.get()); + fireEvent.click(ccUi.searchByEnterprise.noEnterprise.get()); + expect(ccUi.buttonDisplayInfo.query()).toHaveAttribute( + "href", + "/contribution/3239-slug" + ); + expect(sendEvent).toHaveBeenCalledWith({ + action: "cc_select_traitée", + category: "outil", + name: "3239", + }); + }); + + it("should display correctly when a non-treated agreement is selected", async () => { + (searchAgreement as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + num: 1388, + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635267", + effectif: 31273, + shortTitle: "Industrie du pétrole", + cdtnId: "8c50f32b7d", + id: "1388", + slug: "1388-industrie-du-petrole", + title: + "Convention collective nationale deo l'industrie du pétrole du 3 septembre 1985. Etendue par arrêté du 31 juillet 1986 JORF 9 août 1986.", + contributions: false, + }, + ]) + ); + fireEvent.click(ccUi.radio.agreementSearchOption.get()); + fireEvent.change(ccUi.searchByName.input.get(), { + target: { value: "1388" }, + }); + await wait(); + fireEvent.click(ccUi.searchByName.autocompleteLines.IDCC1388.name.get()); + expect(ui.generic.linkDisplayInfo.query()).toBeInTheDocument(); + expect(ccUi.buttonDisplayInfo.query()).toHaveAttribute("href", ""); + expect(ccUi.warning.title.query()).toBeInTheDocument(); + expect(ccUi.warning.nonTreatedAgreement.query()).toBeInTheDocument(); + expect(sendEvent).toHaveBeenCalledWith({ + action: "cc_select_non_traitée", + category: "outil", + name: "1388", + }); + expect(ccUi.warning.title.query()).toBeInTheDocument(); + fireEvent.click(ui.generic.linkDisplayInfo.get()); + expect(ui.generic.nonTreatedInfo.query()).toBeInTheDocument(); + expect(ui.generic.linkDisplayInfo.query()).not.toBeInTheDocument(); + }); + }); + + describe("no CDT", () => { + beforeEach(() => { + window.localStorage.clear(); + rendering = render( + + ); + }); + it("should display correctly when no agreement is selected", () => { + fireEvent.click(ccUi.radio.agreementSearchOption.get()); + expect(ui.generic.linkDisplayInfo.query()).not.toBeInTheDocument(); + expect(ccUi.buttonDisplayInfo.query()).not.toBeInTheDocument(); + }); + + it("should display correctly when a treated agreement is selected", async () => { + (searchAgreement as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + id: "0016", + num: 16, + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635624", + shortTitle: + "Transports routiers et activités auxiliaires du transport", + slug: "16-transports-routiers-et-activites-auxiliaires-du-transport", + title: + "Convention collective nationale des transports routiers et activités auxiliaires du transport du 21 décembre 1950", + }, + ]) + ); + fireEvent.click(ccUi.radio.agreementSearchOption.get()); + expect(ccUi.buttonDisplayInfo.query()).not.toBeInTheDocument(); + fireEvent.change(ccUi.searchByName.input.get(), { + target: { value: "16" }, + }); + await wait(); + fireEvent.click(ccUi.searchByName.autocompleteLines.IDCC16.name.get()); + expect(ccUi.buttonDisplayInfo.query()).toHaveAttribute( + "href", + "/contribution/16-slug" + ); + expect(ccUi.warning.nonTreatedAgreement.query()).not.toBeInTheDocument(); + expect(sendEvent).toHaveBeenCalledWith({ + action: "cc_select_traitée", + category: "outil", + name: "0016", + }); + }); + + it("should display correctly when a non-treated agreement is selected", async () => { + (searchAgreement as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + num: 1388, + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635267", + effectif: 31273, + shortTitle: "Industrie du pétrole", + cdtnId: "8c50f32b7d", + id: "1388", + slug: "1388-industrie-du-petrole", + title: + "Convention collective nationale deo l'industrie du pétrole du 3 septembre 1985. Etendue par arrêté du 31 juillet 1986 JORF 9 août 1986.", + contributions: false, + }, + ]) + ); + fireEvent.click(ccUi.radio.agreementSearchOption.get()); + fireEvent.change(ccUi.searchByName.input.get(), { + target: { value: "1388" }, + }); + await wait(); + fireEvent.click(ccUi.searchByName.autocompleteLines.IDCC1388.name.get()); + expect(ui.generic.linkDisplayInfo.query()).not.toBeInTheDocument(); + expect(ccUi.buttonDisplayInfo.query()).not.toBeInTheDocument(); + expect(ccUi.warning.title.query()).toBeInTheDocument(); + expect(ccUi.warning.noCdtNonTreatedAgreement.query()).toBeInTheDocument(); + expect( + rendering.getByText(new RegExp(contribution.messageBlockGenericNoCDT)) + ).toBeInTheDocument(); + expect(sendEvent).toHaveBeenCalledWith({ + action: "cc_select_non_traitée", + category: "outil", + name: "1388", + }); + }); + + it("should display correctly when a unextended agreement is selected", async () => { + (searchAgreement as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + num: 29, + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635234", + effectif: 1, + shortTitle: + "Hospitalisation privée : établissements privés d'hospitalisation, de soins, de cure et de garde à but non lucratif (FEHAP)", + cdtnId: "394e29a64d", + id: "0029", + slug: "29-hospitalisation-privee-etablissements-prives-dhospitalisation-de-soins-d", + title: + "Convention collective nationale des etablissements privés d'hospitalisation, de soins, de cure et de garde à but non lucratif du 31 octobre 1951.", + contributions: true, + }, + ]) + ); + fireEvent.click(ccUi.radio.agreementSearchOption.get()); + fireEvent.change(ccUi.searchByName.input.get(), { + target: { value: "29" }, + }); + await wait(); + fireEvent.click(ccUi.searchByName.autocompleteLines.IDCC29.name.get()); + expect(ui.generic.linkDisplayInfo.query()).not.toBeInTheDocument(); + expect(ccUi.buttonDisplayInfo.query()).not.toBeInTheDocument(); + expect(ccUi.warning.title.query()).toBeInTheDocument(); + expect(ccUi.warning.noCdtUnextendedAgreement.query()).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/code-du-travail-frontend/src/modules/contributions/__tests__/queries.es.test.ts b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/queries.es.test.ts index 608b15c7ec..db6844e1c4 100644 --- a/packages/code-du-travail-frontend/src/modules/contributions/__tests__/queries.es.test.ts +++ b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/queries.es.test.ts @@ -1,6 +1,6 @@ /** @jest-environment node */ -import { fetchContributions } from "../queries"; +import { fetchContributionBySlug, fetchContributions } from "../queries"; describe("Contributions", () => { it("Récupération de toutes les contributions", async () => { @@ -38,4 +38,149 @@ describe("Contributions", () => { }, ]); }); + + it("Récupération d'une contribution par son slug sans articles liées", async () => { + const result = await fetchContributionBySlug( + "les-conges-pour-evenements-familiaux" + ); + expect(result).toEqual({ + _id: "32", + breadcrumbs: [ + { + label: "Congés et repos", + position: 3, + slug: "/themes/conges-et-repos", + }, + { + label: "Congés", + position: 1, + slug: "/themes/conges", + }, + { + label: "Congés pour événement familial", + position: 3, + slug: "/themes/conges-pour-evenement-familial", + }, + ], + idcc: "0000", + isFicheSP: false, + isGeneric: true, + isNoCDT: false, + relatedItems: [], + slug: "les-conges-pour-evenements-familiaux", + title: "Les congés pour événements familiaux", + }); + }); + + it("Récupération d'une contribution par son slug avec articles liées", async () => { + const result = await fetchContributionBySlug( + "44-quand-le-salarie-a-t-il-droit-a-une-prime-danciennete-quel-est-son-montant" + ); + expect(result).toEqual({ + _id: "37", + breadcrumbs: [ + { + label: "Salaire et Rémunération", + position: 1, + slug: "/themes/salaire-et-remuneration", + }, + { + label: "Primes et avantages", + position: 2, + slug: "/themes/primes-et-avantages", + }, + ], + ccnShortTitle: "Industries chimiques et connexes", + ccnSlug: "44-industries-chimiques-et-connexes", + content: + "Qui est concerné ?

La convention collective prévoit une prime d'ancienneté pour les salariés appartenant à la catégorie des :

  • Ouvriers, employés et techniciens (groupes I à III) et ;

  • Agents de maîtrise et techniciens (groupe IV).

Le salarié doit avoir au moins 3 ans d'ancienneté.

Montant de la prime

La prime d'ancienneté est calculée en appliquant un pourcentage sur le minimum conventionnel correspondant au coefficient hiérarchique du salarié, augmenté, le cas échéant, des majorations pour heures supplémentaires et proportionnellement à l'horaire de travail. Les taux de la prime sont les suivants :

  • 3% après 3 ans d'ancienneté dans l'entreprise ;

  • 6% après 6 ans d'ancienneté dans l'entreprise ;

  • 9% après 9 ans d'ancienneté dans l'entreprise ;

  • 12% après 12 ans d'ancienneté dans l'entreprise ;

  • 15% après 15 ans d'ancienneté dans l'entreprise.

", + idcc: "0044", + isFicheSP: false, + isGeneric: false, + isNoCDT: false, + linkedContent: [ + { + breadcrumbs: [ + { + label: "Salaire et Rémunération", + position: 1, + slug: "/themes/salaire-et-remuneration", + }, + { + label: "Primes et avantages", + position: 2, + slug: "/themes/primes-et-avantages", + }, + ], + description: + "Une convention collective peut prévoir des primes que l'employeur doit verser aux salariés. Elle précise alors leurs conditions d'attribution et montant.", + slug: "quelles-sont-les-primes-prevues-par-la-convention-collective", + source: "contributions", + title: + "Quelles sont les primes prévues par la convention collective ?", + }, + { + breadcrumbs: [ + { + label: "Salaire et Rémunération", + position: 1, + slug: "/themes/salaire-et-remuneration", + }, + { + label: "Primes et avantages", + position: 2, + slug: "/themes/primes-et-avantages", + }, + ], + description: + "Non, la réglementation du code du travail n'impose pas à votre employeur de vous verser une prime d'ancienneté. Nous vous expliquons dans quelles conditions la prime doit être versée si elle existe.", + slug: "salaire-du-secteur-prive-la-prime-danciennete-est-elle-obligatoire", + source: "fiches_service_public", + title: + "Salaire du secteur privé : la prime d'ancienneté est-elle obligatoire ?", + }, + ], + messageBlock: + "

Ces informations sont issues de l’analyse des règles prévues par votre convention collective de branche étendue et par le Code du travail. Elles s’appliqueront sauf si une convention ou un accord d’entreprise (ou de groupe, ou d’établissement) existant dans votre entreprise prévoit également des règles sur le même sujet. En effet, dans ce cas, cette convention ou accord s’appliquera, qu’il soit plus ou moins favorable que la convention de branche, sous réserve d’être au moins aussi favorable que le Code du travail. Dans tous les cas, reportez-vous à votre contrat de travail car s’il contient des règles plus favorables, ce sont ces dernières qui s’appliqueront.

Attention, d’autres règles non étendues peuvent potentiellement vous être applicables.

", + references: [ + { + title: + "Avenant n° 1 Ouvriers et collaborateurs du 11 février 1971 Article 10", + url: "https://legifrance.gouv.fr/conv_coll/id/KALIARTI000005846371/?idConteneur=KALICONT000005635613", + }, + { + title: + "Avenant n° 2 Agents de maîtrise et techniciens du 14 mars 1955 Article 16", + url: "https://legifrance.gouv.fr/conv_coll/id/KALIARTI000005846453/?idConteneur=KALICONT000005635613", + }, + ], + relatedItems: [ + { + items: [], + title: "Modèles et outils liés", + }, + { + items: [ + { + source: "contributions", + title: + "Quelles sont les primes prévues par la convention collective ?", + url: "/contribution/quelles-sont-les-primes-prevues-par-la-convention-collective", + }, + { + source: "fiches_service_public", + title: + "Salaire du secteur privé : la prime d'ancienneté est-elle obligatoire ?", + url: "/fiche-service-public/salaire-du-secteur-prive-la-prime-danciennete-est-elle-obligatoire", + }, + ], + title: "Articles liés", + }, + ], + slug: "44-quand-le-salarie-a-t-il-droit-a-une-prime-danciennete-quel-est-son-montant", + title: + "Quand le salarié a-t-il droit à une prime d’ancienneté ? Quel est son montant ?", + type: "content", + }); + }); }); diff --git a/packages/code-du-travail-frontend/src/modules/contributions/__tests__/ui.ts b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/ui.ts new file mode 100644 index 0000000000..40b560df93 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/__tests__/ui.ts @@ -0,0 +1,13 @@ +import { byText } from "testing-library-selector"; + +export const ui = { + generic: { + linkDisplayInfo: byText( + "Afficher les informations sans sélectionner une convention collective" + ), + title: byText("Que dit le code du travail ?"), + nonTreatedInfo: byText( + /Cette réponse correspond à ce que prévoit le code du travail/ + ), + }, +}; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/contributionUtils.ts b/packages/code-du-travail-frontend/src/modules/contributions/contributionUtils.ts new file mode 100644 index 0000000000..0cc5a52129 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/contributionUtils.ts @@ -0,0 +1,26 @@ +import { EnterpriseAgreement } from "../enterprise"; +import { Contribution } from "./type"; + +export const isAgreementSupported = ( + contribution: Contribution, + agreement: EnterpriseAgreement +) => { + const { ccSupported = [] } = contribution; + return ccSupported.includes(agreement.id); +}; +export const isAgreementUnextended = ( + contribution: Contribution, + agreement: EnterpriseAgreement +) => { + const { ccUnextended = [] } = contribution; + return ccUnextended.includes(agreement?.id); +}; +export const isAgreementValid = ( + contribution: Contribution, + agreement?: EnterpriseAgreement +) => { + if (!agreement) return false; + const isSupported = isAgreementSupported(contribution, agreement); + const isUnextended = isAgreementUnextended(contribution, agreement); + return !isUnextended && isSupported; +}; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/index.ts b/packages/code-du-travail-frontend/src/modules/contributions/index.ts index b69c251208..2a56de9e94 100644 --- a/packages/code-du-travail-frontend/src/modules/contributions/index.ts +++ b/packages/code-du-travail-frontend/src/modules/contributions/index.ts @@ -1 +1,2 @@ export * from "./queries"; +export * from "./ContributionLayout"; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/queries.ts b/packages/code-du-travail-frontend/src/modules/contributions/queries.ts index 2b7311a410..7eeafdefe4 100644 --- a/packages/code-du-travail-frontend/src/modules/contributions/queries.ts +++ b/packages/code-du-travail-frontend/src/modules/contributions/queries.ts @@ -1,6 +1,12 @@ import { elasticDocumentsIndex, elasticsearchClient } from "../../api/utils"; -import { ContributionElasticDocument } from "@socialgouv/cdtn-types"; import { SOURCES } from "@socialgouv/cdtn-utils"; +import { + DocumentElasticResult, + fetchDocument, + formatRelatedItems, + LinkedContent, +} from "../documents"; +import { Contribution, ContributionElasticDocument } from "./type"; export const fetchContributions = async < K extends keyof ContributionElasticDocument, @@ -36,3 +42,65 @@ export const fetchContributions = async < .map(({ _source }) => _source) .filter((source) => source !== undefined); }; + +const formatContribution = ( + contribution: ContributionElasticDocument | undefined +): Contribution | undefined => { + if (!contribution) { + return undefined; + } + return { + ...contribution, + isGeneric: contribution.idcc === "0000", + isNoCDT: contribution?.type === "generic-no-cdt", + isFicheSP: "raw" in contribution, + relatedItems: contribution.linkedContent + ? formatRelatedItems(contribution.linkedContent as LinkedContent[]) + : [], + }; +}; + +export const fetchContributionBySlug = async ( + slug: string +): Promise => { + const response = await fetchDocument< + ContributionElasticDocument, + keyof DocumentElasticResult + >( + [ + "metas", + "idcc", + "date", + "title", + "slug", + "type", + "linkedContent", + "breadcrumbs", + "ccSupported", + "ccUnextended", + "messageBlock", + "references", + "ccnShortTitle", + "ccnSlug", + "raw", + "ficheSpDescription", + "content", + "url", + "messageBlockGenericNoCDT", + ], + { + index: elasticDocumentsIndex, + query: { + bool: { + filter: [ + { term: { source: SOURCES.CONTRIBUTIONS } }, + { term: { slug } }, + { term: { isPublished: true } }, + ], + }, + }, + size: 1, + } + ); + return formatContribution(response); +}; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/tracking.ts b/packages/code-du-travail-frontend/src/modules/contributions/tracking.ts new file mode 100644 index 0000000000..51498cb39e --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/tracking.ts @@ -0,0 +1,96 @@ +import { sendEvent } from "../utils"; + +export enum TrackingContributionCategory { + TOOL = "outil", + CONTRIBUTION = "contribution", + CC_SEARCH_TYPE_OF_USERS = "cc_search_type_of_users", +} + +export enum TrackingAgreementSearchAction { + CC_TREATED = "cc_select_traitée", + CC_UNTREATED = "cc_select_non_traitée", + CC_BLOCK_USER = "user_blocked_info_cc", + CLICK_DISPLAY_AGREEMENT_CONTENT = "click_afficher_les_informations_CC", + CLICK_DISPLAY_GENERIC_CONTENT = "click_afficher_les_informations_sans_CC", + CLICK_DISPLAY_GENERAL_CONTENT = "click_afficher_les_informations_générales", + CLICK_P1 = "click_p1", + CLICK_P2 = "click_p2", + CLICK_P3 = "click_p3", +} + +export const useContributionTracking = () => { + const emitAgreementTreatedEvent = (idcc: string) => { + sendEvent({ + category: TrackingContributionCategory.TOOL, + action: TrackingAgreementSearchAction.CC_TREATED, + name: idcc, + }); + }; + + const emitAgreementUntreatedEvent = (idcc: string) => { + sendEvent({ + category: TrackingContributionCategory.TOOL, + action: TrackingAgreementSearchAction.CC_UNTREATED, + name: idcc, + }); + }; + + const emitDisplayAgreementContent = (path: string) => { + sendEvent({ + category: TrackingContributionCategory.CONTRIBUTION, + action: TrackingAgreementSearchAction.CLICK_DISPLAY_AGREEMENT_CONTENT, + name: path, + }); + }; + + const emitDisplayGenericContent = (path: string) => { + sendEvent({ + category: TrackingContributionCategory.CONTRIBUTION, + action: TrackingAgreementSearchAction.CLICK_DISPLAY_GENERIC_CONTENT, + name: path, + }); + }; + + const emitDisplayGeneralContent = (path: string) => { + sendEvent({ + category: TrackingContributionCategory.CONTRIBUTION, + action: TrackingAgreementSearchAction.CLICK_DISPLAY_GENERAL_CONTENT, + name: path, + }); + }; + + const emitClickP1 = (path: string) => { + sendEvent({ + category: TrackingContributionCategory.CC_SEARCH_TYPE_OF_USERS, + action: TrackingAgreementSearchAction.CLICK_P1, + name: path, + }); + }; + + const emitClickP2 = (path: string) => { + sendEvent({ + category: TrackingContributionCategory.CC_SEARCH_TYPE_OF_USERS, + action: TrackingAgreementSearchAction.CLICK_P2, + name: path, + }); + }; + + const emitClickP3 = (path: string) => { + sendEvent({ + category: TrackingContributionCategory.CC_SEARCH_TYPE_OF_USERS, + action: TrackingAgreementSearchAction.CLICK_P3, + name: path, + }); + }; + + return { + emitAgreementTreatedEvent, + emitAgreementUntreatedEvent, + emitDisplayAgreementContent, + emitDisplayGeneralContent, + emitDisplayGenericContent, + emitClickP1, + emitClickP2, + emitClickP3, + }; +}; diff --git a/packages/code-du-travail-frontend/src/modules/contributions/type.ts b/packages/code-du-travail-frontend/src/modules/contributions/type.ts new file mode 100644 index 0000000000..169a80a4a1 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/contributions/type.ts @@ -0,0 +1,53 @@ +import { + Breadcrumb, + ContributionContentBase, + ContributionConventionnelInfos, + ContributionDocumentJson, + ContributionFicheSpContent, + ContributionGenericInfos, + ContributionGenericNoCDTContent, + ContributionHighlight, + ContributionMetadata, + DocumentElasticWithSource, + ExportContributionFullLinkedContent, +} from "@socialgouv/cdtn-types"; +import { RelatedItem } from "../documents"; + +type ExportContributionInfo = { + breadcrumbs: Breadcrumb[]; + highlight?: ContributionHighlight; + messageBlock?: string; +}; + +export type ContributionContent = Partial & + Partial & + Partial; + +type ContributionElasticDocumentBase = Omit< + DocumentElasticWithSource>, + "breadcrumbs" +> & + ContributionMetadata & + ContributionContent & + ExportContributionFullLinkedContent & + ExportContributionInfo & { + raw: string; + url: string; + content: string; + }; + +export type ContributionElasticDocument = ContributionElasticDocumentBase & + Partial & + Partial; + +export type ContributionRelatedItems = { + title: string; + items: RelatedItem[]; +}; + +export type Contribution = ContributionElasticDocument & { + isGeneric: boolean; + isNoCDT: boolean; + isFicheSP: boolean; + relatedItems: ContributionRelatedItems[]; +}; diff --git a/packages/code-du-travail-frontend/src/modules/convention-collective/AgreementSearch/AgreementSearchForm.tsx b/packages/code-du-travail-frontend/src/modules/convention-collective/AgreementSearch/AgreementSearchForm.tsx new file mode 100644 index 0000000000..f2f0ae18c4 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/convention-collective/AgreementSearch/AgreementSearchForm.tsx @@ -0,0 +1,72 @@ +"use client"; +import { RadioButtons } from "@codegouvfr/react-dsfr/RadioButtons"; +import { ReactNode, useState } from "react"; +import { AgreementSearchInput } from "./AgreementSearchInput"; +import { + EnterpriseAgreement, + EnterpriseAgreementSearchInput, +} from "../../enterprise"; + +type Props = { + onAgreementSelect?: (agreement?: EnterpriseAgreement, mode?: string) => void; + selectedAgreementAlert?: ( + agreement?: EnterpriseAgreement + ) => NonNullable | undefined; + defaultAgreement?: EnterpriseAgreement; +}; + +export const AgreementSearchForm = ({ + onAgreementSelect, + selectedAgreementAlert, + defaultAgreement, +}: Props) => { + const [mode, setMode] = useState< + "agreementSearch" | "enterpriseSearch" | "noSearch" | undefined + >(!!defaultAgreement ? "agreementSearch" : undefined); + + return ( + <> + setMode("agreementSearch"), + }, + }, + { + label: + "Je cherche mon entreprise pour trouver ma convention collective.", + nativeInputProps: { + checked: mode === "enterpriseSearch", + onChange: () => { + if (onAgreementSelect) onAgreementSelect(); + setMode("enterpriseSearch"); + }, + }, + }, + ]} + /> + {mode === "agreementSearch" && ( + { + if (onAgreementSelect) onAgreementSelect(agreement, "p1"); + }} + selectedAgreementAlert={selectedAgreementAlert} + defaultAgreement={defaultAgreement} + /> + )} + {mode === "enterpriseSearch" && ( + { + if (onAgreementSelect) onAgreementSelect(agreement, "p2"); + }} + selectedAgreementAlert={selectedAgreementAlert} + /> + )} + + ); +}; diff --git a/packages/code-du-travail-frontend/src/modules/convention-collective/AgreementSearch/AgreementSearchInput.tsx b/packages/code-du-travail-frontend/src/modules/convention-collective/AgreementSearch/AgreementSearchInput.tsx index 165d18c69e..6d389cddf8 100644 --- a/packages/code-du-travail-frontend/src/modules/convention-collective/AgreementSearch/AgreementSearchInput.tsx +++ b/packages/code-du-travail-frontend/src/modules/convention-collective/AgreementSearch/AgreementSearchInput.tsx @@ -2,18 +2,30 @@ import { fr } from "@codegouvfr/react-dsfr"; import Alert from "@codegouvfr/react-dsfr/Alert"; import { getRouteBySource, SOURCES } from "@socialgouv/cdtn-utils"; -import { useState } from "react"; +import { ReactNode, useState } from "react"; import { Autocomplete } from "../../common/Autocomplete"; import { Agreement } from "../../../outils/types"; import { searchAgreement } from "../search"; +import { EnterpriseAgreement } from "../../enterprise"; import { useAgreementSearchTracking } from "../tracking"; type Props = { onSearch?: (query: string, value?: Agreement[]) => void; + onAgreementSelect?: (agreement?: EnterpriseAgreement) => void; + selectedAgreementAlert?: ( + agreement?: EnterpriseAgreement + ) => NonNullable | undefined; + defaultAgreement?: EnterpriseAgreement; }; -export const AgreementSearchInput = ({ onSearch }: Props) => { +export const AgreementSearchInput = ({ + onSearch, + onAgreementSelect, + selectedAgreementAlert, + defaultAgreement, +}: Props) => { + const [selectedAgreement, setSelectedAgreement] = useState(defaultAgreement); const [searchState, setSearchState] = useState< "noSearch" | "lowSearch" | "notFoundSearch" | "errorSearch" | "fullSearch" >("noSearch"); @@ -57,28 +69,36 @@ export const AgreementSearchInput = ({ onSearch }: Props) => {
- + + defaultValue={selectedAgreement} dataTestId="AgreementSearchAutocomplete" + className={fr.cx("fr-col-12", "fr-mb-0")} hintText="Ex : transport routier ou 1486" label={ <> Nom de la convention collective ou son numéro d’identification - IDCC (4 chiffres) + IDCC (4 chiffres) } state={getInputState()} stateRelatedMessage={getStateMessage()} - displayLabel={(item) => { - return item ? `${item.shortTitle} (IDCC ${item.num})` : ""; - }} - lineAsLink={(item) => { - return `/${getRouteBySource(SOURCES.CCN)}/${item.slug}`; - }} onChange={(agreement) => { + setSelectedAgreement(agreement); + if (onAgreementSelect) onAgreementSelect(agreement); if (agreement) { emitSelectEvent(`idcc${agreement.id}`); } }} + displayLabel={(item) => { + return item ? `${item.shortTitle} (IDCC ${item.num})` : ""; + }} + lineAsLink={ + !onAgreementSelect + ? (item) => { + return `/${getRouteBySource(SOURCES.CCN)}/${item.slug}`; + } + : undefined + } search={searchAgreement} onSearch={(query, agreements) => { if (query) { @@ -130,6 +150,14 @@ export const AgreementSearchInput = ({ onSearch }: Props) => { severity="info" /> )} + {selectedAgreement && selectedAgreementAlert?.(selectedAgreement) && ( + + )}
); diff --git a/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/AgreementSearchByName.test.tsx b/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/AgreementSearchByName.test.tsx index 7f0243768d..92a9c73daf 100644 --- a/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/AgreementSearchByName.test.tsx +++ b/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/AgreementSearchByName.test.tsx @@ -26,11 +26,27 @@ jest.mock("next/navigation", () => ({ describe("Trouver sa CC - recherche par nom de CC", () => { describe("Test de l'autocomplete", () => { - let rendering: RenderResult; let userAction: UserAction; beforeEach(() => { jest.resetAllMocks(); - rendering = render(); + }); + it("Vérifier l'affichage des erreurs", async () => { + (searchAgreement as jest.Mock).mockImplementation(() => + Promise.resolve([]) + ); + render(); + userAction = new UserAction(); + userAction.setInput(ui.searchByName.input.get(), "cccc"); + await wait(); + expect(ui.searchByName.errorNotFound.error.query()).toBeInTheDocument(); + expect(ui.searchByName.errorNotFound.info.query()).toBeInTheDocument(); + userAction.click(ui.searchByName.inputCloseBtn.get()); + expect( + ui.searchByName.errorNotFound.error.query() + ).not.toBeInTheDocument(); + expect( + ui.searchByName.errorNotFound.info.query() + ).not.toBeInTheDocument(); }); it("Vérifier la navigation", async () => { (searchAgreement as jest.Mock).mockImplementation(() => @@ -47,6 +63,7 @@ describe("Trouver sa CC - recherche par nom de CC", () => { }, ]) ); + render(); userAction = new UserAction(); userAction.setInput(ui.searchByName.input.get(), "16"); await wait(); @@ -80,27 +97,12 @@ describe("Trouver sa CC - recherche par nom de CC", () => { name: "Trouver sa convention collective", }); }); - it("Vérifier l'affichage des erreurs", async () => { - (searchAgreement as jest.Mock).mockImplementation(() => - Promise.resolve([]) - ); - userAction = new UserAction(); - userAction.setInput(ui.searchByName.input.get(), "cccc"); - await wait(); - expect(ui.searchByName.errorNotFound.error.query()).toBeInTheDocument(); - expect(ui.searchByName.errorNotFound.info.query()).toBeInTheDocument(); - userAction.click(ui.searchByName.inputCloseBtn.get()); - expect( - ui.searchByName.errorNotFound.error.query() - ).not.toBeInTheDocument(); - expect( - ui.searchByName.errorNotFound.info.query() - ).not.toBeInTheDocument(); - }); + it("Vérifier l'affichage des infos si moins 2 caractères", () => { (searchAgreement as jest.Mock).mockImplementation(() => Promise.resolve([]) ); + render(); act(async () => { userAction = new UserAction(); userAction.setInput(ui.searchByName.input.get(), "cc"); diff --git a/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/AgreementSearchForm.test.tsx b/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/AgreementSearchForm.test.tsx new file mode 100644 index 0000000000..722e525284 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/AgreementSearchForm.test.tsx @@ -0,0 +1,212 @@ +import { render, screen } from "@testing-library/react"; +import React, { useRef } from "react"; +import { wait } from "@testing-library/user-event/dist/utils"; +import { searchEnterprises } from "../../enterprise/queries"; +import { sendEvent } from "../../utils"; +import { ui } from "./ui"; +import { ui as enterpriseUi } from "../../enterprise/EnterpriseAgreementSearch/__tests__/ui"; +import { UserAction } from "src/common"; +import { AgreementSearchForm } from "../AgreementSearch/AgreementSearchForm"; + +jest.mock("../../utils", () => ({ + sendEvent: jest.fn(), +})); + +jest.mock("uuid", () => ({ + v4: jest.fn(() => ""), +})); + +jest.mock("../../enterprise/queries", () => ({ + searchEnterprises: jest.fn(), +})); + +jest.mock("next/navigation", () => ({ + redirect: jest.fn(), + useSearchParams: jest.fn(), + usePathname: jest.fn(), +})); + +const enterprise1CC = { + activitePrincipale: + "Location-bail de propriété intellectuelle et de produits similaires, à l’exception des œuvres soumises à copyright", + etablissements: 1294, + highlightLabel: "CARREFOUR PROXIMITE FRANCE (SHOPI-8 A HUIT)", + label: "CARREFOUR PROXIMITE FRANCE (SHOPI-8 A HUIT)", + simpleLabel: "CARREFOUR PROXIMITE FRANCE (SHOPI-8 A HUIT)", + matching: 1294, + siren: "345130488", + address: "ZI ROUTE DE PARIS 14120 MONDEVILLE", + firstMatchingEtablissement: { + siret: "34513048802674", + address: "N°6639 205 RUE SAINT-HONORE 75001 PARIS", + }, + conventions: [ + { + id: "2216", + contributions: true, + num: 2216, + shortTitle: "Commerce de détail et de gros à prédominance alimentaire", + title: + "Convention collective nationale du commerce de détail et de gros à prédominance alimentaire du 12 juillet 2001. Etendue par arrêté du 26 juillet 2002 JORF 6 août 2002.", + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635085", + slug: "2216-commerce-de-detail-et-de-gros-a-predominance-alimentaire", + }, + ], +}; + +const enterpriseMoreCC = { + activitePrincipale: "Autres intermédiations monétaires", + etablissements: 2032, + highlightLabel: "BNP PARIBAS (HELLO BANK!)", + label: "BNP PARIBAS (HELLO BANK!)", + simpleLabel: "BNP PARIBAS (HELLO BANK!)", + matching: 2032, + siren: "662042449", + address: "16 BOULEVARD DES ITALIENS 75009 PARIS", + firstMatchingEtablissement: { + siret: "66204244908280", + address: "ANGLE DE RUE 19 RUE DES LAVANDIERES 55 RUE DE RIVOLI 75001 PARIS", + }, + conventions: [ + { + id: "2120", + contributions: true, + num: 2120, + shortTitle: "Banque", + title: + "Convention collective nationale de la banque du 10 janvier 2000. Etendue par arrêté du 17 novembre 2004 JORF 11 décembre 2004.", + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635780", + slug: "2120-banque", + }, + { + id: "9999", + num: 9999, + shortTitle: "___Sans convention collective___", + title: "___Sans convention collective___", + contributions: false, + }, + { + id: "2931", + contributions: false, + num: 2931, + shortTitle: "Activités de marchés financiers", + title: + "Convention collective nationale des activités de marchés financiers du 11 juin 2010", + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000025496787", + slug: "2931-activites-de-marches-financiers", + }, + ], +}; + +describe("", () => { + let userAction: UserAction; + it("should track when searching by enterprise name", async () => { + render(); + (searchEnterprises as jest.Mock).mockImplementation(() => + Promise.resolve([enterprise1CC]) + ); + userAction = new UserAction(); + userAction.click(ui.radio.enterpriseSearchOption.get()); + userAction.setInput( + enterpriseUi.enterpriseAgreementSearch.input.get(), + "carrefour" + ); + userAction.click(enterpriseUi.enterpriseAgreementSearch.submitButton.get()); + await wait(); + expect(sendEvent).toHaveBeenCalledWith({ + action: "Trouver sa convention collective", + category: "enterprise_search", + name: '{"query":"carrefour"}', + value: "", + }); + expect( + enterpriseUi.enterpriseAgreementSearch.resultLines.carrefour.title.query() + ).toBeInTheDocument(); + userAction.click( + enterpriseUi.enterpriseAgreementSearch.resultLines.carrefour.link.get() + ); + expect(sendEvent).toHaveBeenCalledWith({ + action: "Trouver sa convention collective", + category: "cc_select_p2", + name: "idcc2216", + value: "", + }); + expect(sendEvent).toHaveBeenCalledWith({ + action: "Trouver sa convention collective", + category: "enterprise_select", + name: JSON.stringify({ + label: enterprise1CC.label, + siren: enterprise1CC.siren, + }), + value: "", + }); + }); + it("should track when searching by enterprise with multiple agreements", async () => { + render(); + (searchEnterprises as jest.Mock).mockImplementation(() => + Promise.resolve([enterpriseMoreCC]) + ); + userAction = new UserAction(); + userAction.click(ui.radio.enterpriseSearchOption.get()); + userAction.setInput( + enterpriseUi.enterpriseAgreementSearch.input.get(), + "bnp" + ); + userAction.click(enterpriseUi.enterpriseAgreementSearch.submitButton.get()); + await wait(); + expect(sendEvent).toHaveBeenCalledWith({ + action: "Trouver sa convention collective", + category: "enterprise_search", + name: '{"query":"bnp"}', + value: "", + }); + expect( + enterpriseUi.enterpriseAgreementSearch.resultLines.bnp.title.query() + ).toBeInTheDocument(); + userAction.click( + enterpriseUi.enterpriseAgreementSearch.resultLines.bnp.link.get() + ); + userAction.click( + enterpriseUi.enterpriseAgreementSearch.resultLines.bnp.ccList.idcc2120.get() + ); + expect(sendEvent).toHaveBeenCalledWith({ + action: "Trouver sa convention collective", + category: "cc_select_p2", + name: "idcc2120", + value: "", + }); + expect(sendEvent).toHaveBeenCalledWith({ + action: "Trouver sa convention collective", + category: "enterprise_select", + name: JSON.stringify({ + label: enterpriseMoreCC.label, + siren: enterpriseMoreCC.siren, + }), + value: "", + }); + expect( + enterpriseUi.enterpriseAgreementSearch.errorNotFound.notTreated.query() + ).not.toBeInTheDocument(); + userAction.click( + enterpriseUi.enterpriseAgreementSearch.resultLines.bnp.ccList.idcc9999.get() + ); + expect( + enterpriseUi.enterpriseAgreementSearch.errorNotFound.notTreated.query() + ).toBeInTheDocument(); + }); + + it("should track when selecting agreement 3239", () => { + render(); + userAction = new UserAction(); + userAction.click(ui.radio.enterpriseSearchOption.get()); + screen.debug(); + userAction.click( + enterpriseUi.enterpriseAgreementSearch.childminder.title.get() + ); + expect(sendEvent).toHaveBeenCalledWith({ + action: "select_je_n_ai_pas_d_entreprise", + category: "cc_search_type_of_users", + name: "Trouver sa convention collective", + }); + }); +}); diff --git a/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/ui.ts b/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/ui.ts index 46caf6f5be..c358d05e03 100644 --- a/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/ui.ts +++ b/packages/code-du-travail-frontend/src/modules/convention-collective/__tests__/ui.ts @@ -6,6 +6,29 @@ import { } from "testing-library-selector"; export const ui = { + radio: { + agreementSearchOption: byLabelText( + /Je sais quelle est ma convention collective et je la saisis\./ + ), + enterpriseSearchOption: byLabelText( + /Je cherche mon entreprise pour trouver ma convention collective\./ + ), + }, + buttonDisplayInfo: byText("Afficher les informations"), + warning: { + title: byText( + "Nous n’avons pas de réponse pour cette convention collective" + ), + nonTreatedAgreement: byText( + /Vous pouvez consulter les informations générales ci-dessous/ + ), + noCdtUnextendedAgreement: byText( + /Les dispositions de cette convention n’ont pas été étendues/ + ), + noCdtNonTreatedAgreement: byText( + /Nous vous invitons à consulter votre convention collective qui peut prévoir une réponse/ + ), + }, searchAgreementIntro: { buttonSearchAgreement: byRole("link", { name: "Je connais ma convention collective je la saisis", @@ -31,6 +54,20 @@ export const ui = { name: "Transports routiers et activités auxiliaires du transport (IDCC 16)", }), }, + IDCC1388: { + name: byText(/Industrie du pétrole \(IDCC 1388\)/), + link: byRole("link", { + name: "Industrie du pétrole (IDCC 1388)", + }), + }, + IDCC29: { + name: byText( + "Hospitalisation privée : établissements privés d'hospitalisation, de soins, de cure et de garde à but non lucratif (FEHAP) (IDCC 29)" + ), + link: byRole("link", { + name: "Hospitalisation privée : établissements privés d'hospitalisation, de soins, de cure et de garde à but non lucratif (FEHAP) (IDCC 29)", + }), + }, }, errorNotFound: { error: byText(/Aucune convention collective n'a été trouvée\./), @@ -52,6 +89,9 @@ export const ui = { }), }, }, + noEnterprise: byRole("link", { + name: "Particuliers employeurs et emploi à domicile", + }), errorNotFound: { error: byText(/Aucune entreprise n'a été trouvée\./), info: byText(/Vous ne trouvez pas votre entreprise \?/), diff --git a/packages/code-du-travail-frontend/src/modules/convention-collective/search.ts b/packages/code-du-travail-frontend/src/modules/convention-collective/search.ts index fa2d6f0fd7..68a51237b5 100644 --- a/packages/code-du-travail-frontend/src/modules/convention-collective/search.ts +++ b/packages/code-du-travail-frontend/src/modules/convention-collective/search.ts @@ -1,7 +1,7 @@ import debounce from "debounce-promise"; import { nafError } from "./error"; import { SITE_URL } from "../../config"; -import { Agreement } from "../../outils/types"; +import { ElasticAgreement } from "@socialgouv/cdtn-types"; const formatCCn = ({ num, id, slug, title, shortTitle, highlight, url }) => ({ ...(highlight ? { highlight } : {}), @@ -16,7 +16,9 @@ const formatCCn = ({ num, id, slug, title, shortTitle, highlight, url }) => ({ export const onlyNumberError = "Numéro d’indentification (IDCC) incorrect. Ce numéro est composé de 4 chiffres uniquement."; -const apiIdcc = function createFetcher(query: string): Promise { +const apiIdcc = function createFetcher( + query: string +): Promise { if (/^\d{4}[A-Za-z]$/.test(query.replace(/\W/g, ""))) { return Promise.reject(nafError); } @@ -37,7 +39,7 @@ const apiIdcc = function createFetcher(query: string): Promise { (results) => results.hits.hits.map(({ _source }) => formatCCn(_source) - ) as Agreement[] + ) as ElasticAgreement[] ); } throw new Error(); diff --git a/packages/code-du-travail-frontend/src/modules/convention-collective/tracking.ts b/packages/code-du-travail-frontend/src/modules/convention-collective/tracking.ts index 946e122a70..edc135a25a 100644 --- a/packages/code-du-travail-frontend/src/modules/convention-collective/tracking.ts +++ b/packages/code-du-travail-frontend/src/modules/convention-collective/tracking.ts @@ -20,6 +20,7 @@ export enum TrackingAgreementSearchAction { BACK_STEP_P1 = "back_step_cc_search_p1", BACK_STEP_P2 = "back_step_cc_search_p2", CLICK_NO_COMPANY = "click_je_n_ai_pas_d_entreprise", + SELECT_NO_COMPANY = "select_je_n_ai_pas_d_entreprise", } export const useAgreementSearchTracking = () => { diff --git a/packages/code-du-travail-frontend/src/modules/documents/fetch-related-items.ts b/packages/code-du-travail-frontend/src/modules/documents/fetch-related-items.ts index cdd1fed739..3e4f1cadc5 100644 --- a/packages/code-du-travail-frontend/src/modules/documents/fetch-related-items.ts +++ b/packages/code-du-travail-frontend/src/modules/documents/fetch-related-items.ts @@ -2,7 +2,7 @@ import { getRouteBySource, SOURCES } from "@socialgouv/cdtn-utils"; import { elasticDocumentsIndex, elasticsearchClient } from "../../api/utils"; import { nonNullable } from "@socialgouv/modeles-social"; -import { RelatedItem, sources } from "./type"; +import { LinkedContent, RelatedItem, sources } from "./type"; import { MAX_RELATED_ITEMS_ARTICLES, MAX_RELATED_ITEMS_MODELS_AND_TOOLS, @@ -58,30 +58,16 @@ const getRelatedItemsBody = ( }; }; -export const fetchRelatedItems = async ( - settings: RelatedItemSettings, - excludedSlug: string -): Promise<{ items: RelatedItem[]; title: string }[]> => { - const searchBasedItems = await getSearchBasedItems(settings); - - const filteredItems = searchBasedItems - // avoid elements already visible within the item as fragments - .filter( - (item: { slug: string }) => - !excludedSlug.startsWith(item.slug.split("#")[0]) - ) - .reduce((acc, related) => { - const key = related.source + related.slug; - if (!acc.has(key)) acc.set(key, related); - return acc; - }, new Map()) - .values(); +const getUrl = (item: LinkedContent): string => + item.source === SOURCES.EXTERNALS + ? item.url! + : `/${getRouteBySource(item.source)}/${item.slug}`; - const formatted: RelatedItem[] = Array.from(filteredItems).map((item) => ({ - url: - item.source === SOURCES.EXTERNALS - ? item.url - : `/${getRouteBySource(item.source)}/${item.slug}`, +export const formatRelatedItems = ( + items: LinkedContent[] +): { items: RelatedItem[]; title: string }[] => { + const formatted: RelatedItem[] = items.map((item) => ({ + url: getUrl(item), source: item.source, title: item.title, })); @@ -98,3 +84,27 @@ export const fetchRelatedItems = async ( { items: relatedArticleItems, title: "Articles liés" }, ]; }; + +export const fetchRelatedItems = async ( + settings: RelatedItemSettings, + excludedSlug: string +): Promise<{ items: RelatedItem[]; title: string }[]> => { + const searchBasedItems = await getSearchBasedItems(settings); + + const filteredItems: (RelatedItem & { slug: string })[] = Array.from( + searchBasedItems + // avoid elements already visible within the item as fragments + .filter( + (item: { slug: string }) => + !excludedSlug.startsWith(item.slug.split("#")[0]) + ) + .reduce((acc, related) => { + const key = related.source + related.slug; + if (!acc.has(key)) acc.set(key, related); + return acc; + }, new Map()) + .values() + ); + + return formatRelatedItems(filteredItems); +}; diff --git a/packages/code-du-travail-frontend/src/modules/documents/type.ts b/packages/code-du-travail-frontend/src/modules/documents/type.ts index 21120d5c33..a165eb5d1b 100644 --- a/packages/code-du-travail-frontend/src/modules/documents/type.ts +++ b/packages/code-du-travail-frontend/src/modules/documents/type.ts @@ -13,9 +13,19 @@ export const sources = [ SOURCES.CONTRIBUTIONS, SOURCES.EXTERNALS, SOURCES.LABOUR_LAW, + SOURCES.SHEET_MT_PAGE, ] as const; +export type Source = (typeof sources)[number]; + export type RelatedItem = Pick & { - source: (typeof sources)[number]; + source: Source; url: string; }; + +// @TODO : utiliser LinkedContent dans cdtn-types une fois le package publié +export type LinkedContent = Pick & { + source: Source; + slug: string; + url?: string; +}; diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSearchInput.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSearchInput.tsx index e863e9d5bd..4046344f22 100644 --- a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSearchInput.tsx +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSearchInput.tsx @@ -6,19 +6,26 @@ import Input from "@codegouvfr/react-dsfr/Input"; import Badge from "@codegouvfr/react-dsfr/Badge"; import Alert from "@codegouvfr/react-dsfr/Alert"; import { Card } from "@codegouvfr/react-dsfr/Card"; -import { useEffect, useRef, useState } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { css } from "@styled-system/css"; import Spinner from "../../common/Spinner.svg"; import { LocationSearchInput } from "../../Location/LocationSearchInput"; import { searchEnterprises } from "../queries"; -import { Enterprise } from "../types"; +import { Enterprise, EnterpriseAgreement } from "../types"; import { ApiGeoResult } from "../../Location/searchCities"; import { CardTitleStyle } from "../../convention-collective/style"; +import { EnterpriseAgreementSelectionForm } from "./EnterpriseAgreementSelectionForm"; +import { EnterpriseAgreementSelectionDetail } from "./EnterpriseAgreementSelectionDetail"; +import { getEnterpriseAgreements } from "./utils"; import { useEnterpriseAgreementSearchTracking } from "./tracking"; type Props = { widgetMode?: boolean; + onAgreementSelect?: (agreement: EnterpriseAgreement) => void; + selectedAgreementAlert?: ( + agreement?: EnterpriseAgreement + ) => NonNullable | undefined; defaultSearch?: string; defaultLocation?: ApiGeoResult; }; @@ -27,14 +34,21 @@ export const EnterpriseAgreementSearchInput = ({ widgetMode = false, defaultSearch, defaultLocation, + onAgreementSelect, + selectedAgreementAlert, }: Props) => { + const [selectedAgreement, setSelectedAgreement] = useState< + EnterpriseAgreement | undefined + >(); const [searchState, setSearchState] = useState< "noSearch" | "notFoundSearch" | "errorSearch" | "fullSearch" | "required" >("noSearch"); const { emitEnterpriseAgreementSearchInputEvent, emitSelectEnterpriseEvent, - emitNoEnterpriseEvent, + emitNoEnterpriseClickEvent, + emitNoEnterpriseSelectEvent, + emitSelectEnterpriseAgreementEvent, } = useEnterpriseAgreementSearchTracking(); const [search, setSearch] = useState(defaultSearch); @@ -43,6 +57,7 @@ export const EnterpriseAgreementSearchInput = ({ defaultLocation ); const [enterprises, setEnterprises] = useState(); + const [selectedEnterprise, setSelectedEnterprise] = useState(); const [error, setError] = useState(""); const resultRef = useRef(null); @@ -119,10 +134,99 @@ export const EnterpriseAgreementSearchInput = ({ onSubmit(); } }, [defaultSearch]); - + useEffect(() => { + if (selectedEnterprise?.conventions?.length === 1) { + const [enterpriseAgreement] = getEnterpriseAgreements( + selectedEnterprise.conventions + ); + setSelectedAgreement(enterpriseAgreement); + } + }, [selectedEnterprise]); useEffect(() => { resultRef.current?.focus(); }, [enterprises]); + if ( + onAgreementSelect && + selectedAgreement && + (selectedEnterprise?.conventions?.length ?? 0) < 2 + ) { + return ( + <> + {selectedEnterprise && ( + + )} + +

+ Vous avez sélectionné la convention collective +

+
+ +
+ +
+
+ + {selectedAgreement && selectedAgreementAlert?.(selectedAgreement) && ( + + )} + + ); + } else if (onAgreementSelect && selectedEnterprise) { + return ( + { + setSelectedEnterprise(undefined); + setSelectedAgreement(undefined); + window.scrollTo(0, 0); + }} + onAgreementSelect={(agreement) => { + emitSelectEnterpriseEvent({ + label: selectedEnterprise.label, + siren: selectedEnterprise.siren, + }); + if (onAgreementSelect) onAgreementSelect(agreement); + setSelectedAgreement(agreement); + }} + /> + ); + } return ( <>

Précisez votre entreprise

@@ -259,14 +363,35 @@ export const EnterpriseAgreementSearchInput = ({ className={fr.cx("fr-mt-2w")} border enlargeLink - linkProps={{ - href: widgetMode - ? `/widgets/convention-collective/entreprise/${enterprise.siren}${getQueries()}` - : `/outils/convention-collective/entreprise/${enterprise.siren}${getQueries()}`, - onClick: () => { - emitSelectEnterpriseEvent(enterprise); - }, - }} + linkProps={ + !onAgreementSelect + ? { + href: `/${widgetMode ? "widgets" : "outils"}/convention-collective/entreprise/${enterprise.siren}${getQueries()}`, + onClick: () => { + emitSelectEnterpriseEvent({ + label: enterprise.label, + siren: enterprise.siren, + }); + }, + } + : { + href: "", + onClick: (ev) => { + ev.preventDefault(); + setSelectedEnterprise(enterprise); + if (enterprise.conventions.length === 1) { + emitSelectEnterpriseAgreementEvent( + `idcc${enterprise.conventions[0].id}` + ); + emitSelectEnterpriseEvent({ + label: enterprise.label, + siren: enterprise.siren, + }); + onAgreementSelect(enterprise.conventions[0]); + } + }, + } + } desc={ enterprise.activitePrincipale ? ( <>Activité : {enterprise.activitePrincipale} @@ -300,13 +425,35 @@ export const EnterpriseAgreementSearchInput = ({ { - emitNoEnterpriseEvent(); - }, - }} + linkProps={ + !onAgreementSelect + ? { + href: `/convention-collective/3239-particuliers-employeurs-et-emploi-a-domicile`, + ...(widgetMode ? { target: "_blank" } : {}), + onClick: () => { + emitNoEnterpriseClickEvent(); + }, + } + : { + href: "", + onClick: (ev) => { + ev.preventDefault(); + const assMatAgreement = { + contributions: true, + num: 3239, + id: "3239", + shortTitle: + "Particuliers employeurs et emploi à domicile", + slug: "3239-particuliers-employeurs-et-emploi-a-domicile", + title: "Particuliers employeurs et emploi à domicile", + url: "/3239-particuliers-employeurs-et-emploi-a-domicile", + }; + emitNoEnterpriseSelectEvent(); + setSelectedAgreement(assMatAgreement); + onAgreementSelect(assMatAgreement); + }, + } + } title="Particuliers employeurs et emploi à domicile" desc="Retrouvez les questions-réponses les plus fréquentes organisées par thème et élaborées par le Ministère du travail concernant cette convention collective" size="small" diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelection.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelection.tsx deleted file mode 100644 index 92d727e14c..0000000000 --- a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelection.tsx +++ /dev/null @@ -1,165 +0,0 @@ -"use client"; -import { fr } from "@codegouvfr/react-dsfr"; -import Card from "@codegouvfr/react-dsfr/Card"; -import Button from "@codegouvfr/react-dsfr/Button"; -import { Enterprise } from "../types"; -import { CardTitleStyle } from "../../convention-collective/style"; -import { css } from "@styled-system/css"; -import { useSearchParams } from "next/navigation"; -import { useEnterpriseAgreementSearchTracking } from "./tracking"; -import { useEffect, useRef } from "react"; - -type Props = { - enterprise: Omit; - widgetMode?: boolean; -}; - -export const EnterpriseAgreementSelection = ({ - enterprise, - widgetMode = false, -}: Props) => { - const searchParams = useSearchParams(); - const { emitSelectEnterpriseAgreementEvent } = - useEnterpriseAgreementSearchTracking(); - - const resultRef = useRef(null); - useEffect(() => { - resultRef.current?.focus(); - }, []); - return ( - <> -

- {enterprise.conventions.length === 0 ? ( - <> - Aucune convention collective n'a été déclarée pour - l'entreprise - - ) : enterprise.conventions.length === 1 ? ( - <>1 convention collective trouvée pour : - ) : ( - <> - {enterprise.conventions.length} conventions collectives trouvées - pour : - - )} -

-

- {enterprise.label} -

- {enterprise.activitePrincipale && ( -

- Activité : {enterprise.activitePrincipale} -

- )} -

{enterprise.address}

- {enterprise.conventions?.map((agreement) => { - const { slug, url, contributions } = agreement; - let disabled = false; - let description; - if (slug && !(url || contributions)) { - description = - "Nous n’avons pas d’informations concernant cette convention collective"; - disabled = true; - } else if (!slug) { - description = - "Cette convention collective déclarée par l’entreprise n’est pas reconnue par notre site"; - disabled = true; - } else { - description = - "Retrouvez les questions-réponses les plus fréquentes organisées par thème et élaborées par le Ministère du travail concernant cette convention collective"; - } - return ( - { - emitSelectEnterpriseAgreementEvent(`idcc${agreement.id}`); - }, - } - : { - href: "#", - onClick: (ev) => { - ev.preventDefault(); - }, - }), - ...(widgetMode - ? { - target: "_blank", - onClick: (ev) => { - if (disabled) ev.preventDefault(); - window.parent?.postMessage( - { - name: "agreement", - kind: "select", - extra: { - idcc: agreement.num, - title: agreement.title, - }, - }, - "*" - ); - }, - } - : {}), - }} - border - enlargeLink - size="large" - desc={description} - title={`${agreement.shortTitle} IDCC ${agreement.id}`} - classes={{ - title: `${fr.cx("fr-h5")} ${CardTitleStyle} ${disabled ? disabledTitle : ""}`, - content: `${fr.cx("fr-px-2w", "fr-pt-1w", "fr-pb-7v")} ${disabled ? disabledContent : ""}`, - desc: fr.cx("fr-mt-1w", "fr-mr-8w"), - end: fr.cx("fr-hidden"), - root: `${disabled ? disabledRoot : ""}`, - }} - /> - ); - })} - -
- -
- - ); -}; - -const disabledRoot = css({ - "&:hover": { - backgroundColor: "unset", - }, - color: `var(--text-disabled-grey) !important`, -}); - -const disabledTitle = css({ - "& a,button": { - color: `var(--text-disabled-grey) !important`, - cursor: "not-allowed !important", - _before: { - cursor: "not-allowed", - }, - }, -}); - -const disabledContent = css({ - cursor: "not-allowed", -}); diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionDetail.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionDetail.tsx new file mode 100644 index 0000000000..e826ece9a1 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionDetail.tsx @@ -0,0 +1,37 @@ +"use client"; +import { fr } from "@codegouvfr/react-dsfr"; +import { Enterprise } from "../types"; +import { useEffect, useRef } from "react"; + +type Props = { + enterprise: Omit; +}; + +export const EnterpriseAgreementSelectionDetail = ({ enterprise }: Props) => { + const titleRef = useRef(null); + const scrollToTitle = () => { + setTimeout(() => { + titleRef?.current?.scrollIntoView({ behavior: "smooth" }); + }, 100); + }; + useEffect(() => { + scrollToTitle(); + }, []); + + return ( + <> +

+ Votre entreprise +

+

+ {enterprise.label} +

+ {enterprise.activitePrincipale && ( +

+ Activité : {enterprise.activitePrincipale} +

+ )} +

{enterprise.address}

+ + ); +}; diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionForm.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionForm.tsx new file mode 100644 index 0000000000..effdba76b0 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionForm.tsx @@ -0,0 +1,97 @@ +"use client"; +import { fr } from "@codegouvfr/react-dsfr"; +import { Enterprise, EnterpriseAgreement } from "../types"; +import RadioButtons from "@codegouvfr/react-dsfr/RadioButtons"; +import { EnterpriseAgreementSelectionDetail } from "./EnterpriseAgreementSelectionDetail"; +import { getEnterpriseAgreements } from "./utils"; +import Button from "@codegouvfr/react-dsfr/Button"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import { useEffect, useRef, useState } from "react"; +import { useEnterpriseAgreementSearchTracking } from "./tracking"; + +type Props = { + enterprise: Omit; + goBack: () => void; + onAgreementSelect?: (agreement: EnterpriseAgreement) => void; +}; + +export const EnterpriseAgreementSelectionForm = ({ + enterprise, + goBack, + onAgreementSelect, +}: Props) => { + const { emitSelectEnterpriseAgreementEvent } = + useEnterpriseAgreementSearchTracking(); + const [agreement, setAgreement] = useState(); + const agreements = getEnterpriseAgreements(enterprise.conventions); + const resultRef = useRef(null); + useEffect(() => { + resultRef.current?.focus(); + }, []); + return ( + <> + + +
+ {!!agreements.length && + (agreements.length === 1 ? ( + <>1 convention collective trouvée : + ) : ( + <>{agreements.length} conventions collectives trouvées : + ))} +
+ ({ + label: `${agreement.shortTitle} IDCC ${agreement.id}`, + nativeInputProps: { + value: agreement.num, + ...(onAgreementSelect + ? { + onChange: () => { + onAgreementSelect(agreement); + setAgreement(agreement); + emitSelectEnterpriseAgreementEvent(`idcc${agreement.id}`); + }, + } + : {}), + }, + }))} + /> + {agreement && !agreement.contributions && ( + + + Nous n'avons pas de réponse pour cette convention + collective + + + } + description="Vous pouvez tout de même poursuivre pour obtenir les informations générales prévues par le code du travail." + /> + )} + {!agreements.length && ( + + + Aucune convention collective n'a été déclarée pour + l'entreprise + + + } + description="Vous pouvez tout de même poursuivre pour obtenir les informations générales prévues par le code du travail." + /> + )} + + ); +}; diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionLink.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionLink.tsx new file mode 100644 index 0000000000..c286461761 --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/EnterpriseAgreementSelectionLink.tsx @@ -0,0 +1,146 @@ +"use client"; +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { Enterprise, EnterpriseAgreement } from "../types"; +import Card from "@codegouvfr/react-dsfr/Card"; +import { css } from "@styled-system/css"; +import { useSearchParams } from "next/navigation"; +import { EnterpriseAgreementSelectionDetail } from "./EnterpriseAgreementSelectionDetail"; +import { getEnterpriseAgreements } from "./utils"; +import { CardTitleStyle } from "../../convention-collective/style"; +import { useEnterpriseAgreementSearchTracking } from "./tracking"; + +type Props = { + enterprise: Omit; + widgetMode?: boolean; + onAgreementSelect?: (agreement: EnterpriseAgreement) => void; +}; + +export const EnterpriseAgreementSelectionLink = ({ + enterprise, + widgetMode = false, + onAgreementSelect, +}: Props) => { + const searchParams = useSearchParams(); + const { emitSelectEnterpriseAgreementEvent } = + useEnterpriseAgreementSearchTracking(); + const agreementPlurial = enterprise.conventions.length > 1 ? "s" : ""; + return ( + <> + + {enterprise.conventions.length > 0 && ( +

+ {enterprise.conventions.length} convention + {agreementPlurial} collective + {agreementPlurial} trouvée + {agreementPlurial} : +

+ )} + {getEnterpriseAgreements(enterprise.conventions).map( + ({ disabled, description, ...agreement }) => { + return ( + { + emitSelectEnterpriseAgreementEvent( + `idcc${agreement.id}` + ); + if (disabled) ev.preventDefault(); + else if (widgetMode) { + window.parent?.postMessage( + { + name: "agreement", + kind: "select", + extra: { + idcc: agreement.num, + title: agreement.title, + }, + }, + "*" + ); + } + } + : () => { + emitSelectEnterpriseAgreementEvent( + `idcc${agreement.id}` + ); + }, + } + : { + href: "#", + onClick: (ev) => { + if (disabled) ev.preventDefault(); + emitSelectEnterpriseAgreementEvent( + `idcc${agreement.id}` + ); + onAgreementSelect(agreement); + }, + } + : { + href: "#", + onClick: (ev) => { + ev.preventDefault(); + }, + }), + }} + border + enlargeLink + size="large" + desc={description} + title={`${agreement.shortTitle} IDCC ${agreement.id}`} + classes={{ + title: `${fr.cx("fr-h5")} ${CardTitleStyle} ${disabled ? disabledTitle : ""}`, + content: `${fr.cx("fr-px-2w", "fr-pt-1w", "fr-pb-7v")} ${disabled ? disabledContent : ""}`, + desc: fr.cx("fr-mt-1w", "fr-mr-6w"), + end: fr.cx("fr-hidden"), + root: `${disabled ? disabledRoot : ""}`, + }} + /> + ); + } + )} +
+ +
+ + ); +}; + +const disabledRoot = css({ + "&:hover": { + backgroundColor: "unset", + }, + color: `var(--text-disabled-grey) !important`, +}); + +const disabledTitle = css({ + "& a,button": { + color: `var(--text-disabled-grey) !important`, + cursor: "not-allowed !important", + _before: { + cursor: "not-allowed", + }, + }, +}); + +const disabledContent = css({ + cursor: "not-allowed", +}); diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/AgreementSelection.test.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/AgreementSelection.test.tsx index 360f779f4c..211c3d66de 100644 --- a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/AgreementSelection.test.tsx +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/AgreementSelection.test.tsx @@ -1,6 +1,6 @@ import { render, RenderResult } from "@testing-library/react"; import React from "react"; -import { EnterpriseAgreementSelection } from "../EnterpriseAgreementSelection"; +import { EnterpriseAgreementSelectionLink } from "../EnterpriseAgreementSelectionLink"; import { ui } from "./ui"; import { sendEvent } from "../../../utils"; import { UserAction } from "src/common"; @@ -51,7 +51,7 @@ describe("Trouver sa CC - recherche par nom d'entreprise CC", () => { let userAction: UserAction; it("Vérifier l'affichage de la selection", async () => { rendering = render( - + ); userAction = new UserAction(); expect( @@ -91,7 +91,7 @@ describe("Trouver sa CC - recherche par nom d'entreprise CC", () => { it("Vérifier l'affichage de la selection avec une CC sans slug", async () => { rendering = render( - { it("Vérifier l'affichage de la selection avec une CC sans url et contribution", async () => { rendering = render( - { it("Vérifier l'affichage de la selection en widgetMode", async () => { rendering = render( - + ); expect( ui.enterpriseAgreementSelection.agreement.IDCC2216.link.query() diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/EnterpriseAgreementSearch.test.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/EnterpriseAgreementSearch.test.tsx index 93668a33ff..093caf17eb 100644 --- a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/EnterpriseAgreementSearch.test.tsx +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/EnterpriseAgreementSearch.test.tsx @@ -114,7 +114,10 @@ describe("Trouver sa CC - recherche par nom d'entreprise CC", () => { expect(sendEvent).toHaveBeenLastCalledWith({ action: "Trouver sa convention collective", category: "enterprise_select", - name: JSON.stringify(enterprise), + name: JSON.stringify({ + label: enterprise.label, + siren: enterprise.siren, + }), }); userAction.click(ui.enterpriseAgreementSearch.buttonPrevious.get()); expect(sendEvent).toHaveBeenCalledTimes(4); diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/EnterpriseAgreementSearchInput.test.tsx b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/EnterpriseAgreementSearchInput.test.tsx new file mode 100644 index 0000000000..e2114869ae --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/EnterpriseAgreementSearchInput.test.tsx @@ -0,0 +1,143 @@ +import { fireEvent, render, RenderResult } from "@testing-library/react"; +import { EnterpriseAgreementSearchInput } from "../EnterpriseAgreementSearchInput"; +import { searchEnterprises } from "../../queries"; +import { ui } from "./ui"; +import { sendEvent } from "../../../utils"; +import { wait } from "@testing-library/user-event/dist/utils"; + +jest.mock("../../../utils", () => ({ + sendEvent: jest.fn(), +})); + +jest.mock("uuid", () => ({ + v4: jest.fn(() => ""), +})); + +jest.mock("../../queries", () => ({ + searchEnterprises: jest.fn(), +})); + +jest.mock("next/navigation", () => ({ + redirect: jest.fn(), + useSearchParams: jest.fn(), +})); + +describe("EnterpriseAgreementSearchInput", () => { + let rendering: RenderResult; + const enterprise = { + activitePrincipale: + "Location-bail de propriété intellectuelle et de produits similaires, à l’exception des œuvres soumises à copyright", + etablissements: 1294, + highlightLabel: "CARREFOUR PROXIMITE FRANCE (SHOPI-8 A HUIT)", + label: "CARREFOUR PROXIMITE FRANCE (SHOPI-8 A HUIT)", + simpleLabel: "CARREFOUR PROXIMITE FRANCE (SHOPI-8 A HUIT)", + matching: 1294, + siren: "345130488", + address: "ZI ROUTE DE PARIS 14120 MONDEVILLE", + firstMatchingEtablissement: { + siret: "34513048802674", + address: "N°6639 205 RUE SAINT-HONORE 75001 PARIS", + }, + conventions: [], + }; + + describe("Form mode", () => { + beforeEach(async () => { + rendering = render( + {}} /> + ); + }); + it("should navigate correctly with one treated agreement on enterprise", async () => { + (searchEnterprises as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + ...enterprise, + conventions: [ + { + id: "2216", + contributions: true, + num: 2216, + shortTitle: + "Commerce de détail et de gros à prédominance alimentaire", + title: + "Convention collective nationale du commerce de détail et de gros à prédominance alimentaire du 12 juillet 2001. Etendue par arrêté du 26 juillet 2002 JORF 6 août 2002.", + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635085", + slug: "2216-commerce-de-detail-et-de-gros-a-predominance-alimentaire", + }, + { + id: "1747", + contributions: false, + num: 1747, + shortTitle: + "Activités industrielles de boulangerie et pâtisserie", + title: + "Convention collective nationale des activités industrielles de boulangerie et pâtisserie du 13 juillet 1993. Mise à jour par avenant n°10 du 11 octobre 2011.", + url: "https://www.legifrance.gouv.fr/affichIDCC.do?idConvention=KALICONT000005635691", + slug: "1747-activites-industrielles-de-boulangerie-et-patisserie", + }, + ], + }, + ]) + ); + fireEvent.change(ui.enterpriseAgreementSearch.input.get(), { + target: { value: "carrefour" }, + }); + fireEvent.click(ui.enterpriseAgreementSearch.submitButton.get()); + await wait(); + expect(sendEvent).toHaveBeenCalledTimes(1); + expect(sendEvent).toHaveBeenCalledWith({ + action: "Trouver sa convention collective", + category: "enterprise_search", + name: '{"query":"carrefour"}', + value: "", + }); + fireEvent.click( + ui.enterpriseAgreementSearch.resultLines.carrefour.title.get() + ); + fireEvent.click( + rendering.getByText( + "Commerce de détail et de gros à prédominance alimentaire IDCC 2216" + ) + ); + expect( + rendering.queryByText( + "Nous n'avons pas de réponse pour cette convention collective" + ) + ).not.toBeInTheDocument(); + fireEvent.click( + rendering.getByText( + "Activités industrielles de boulangerie et pâtisserie IDCC 1747" + ) + ); + expect( + rendering.queryByText( + "Nous n'avons pas de réponse pour cette convention collective" + ) + ).toBeInTheDocument(); + }); + + it("should display correct message if there is no agreements on enterprise", async () => { + (searchEnterprises as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + ...enterprise, + conventions: [], + }, + ]) + ); + fireEvent.change(ui.enterpriseAgreementSearch.input.get(), { + target: { value: "carrefour" }, + }); + fireEvent.click(ui.enterpriseAgreementSearch.submitButton.get()); + await wait(); + fireEvent.click( + ui.enterpriseAgreementSearch.resultLines.carrefour.title.get() + ); + expect( + rendering.getByText( + "Aucune convention collective n'a été déclarée pour l'entreprise" + ) + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/ui.ts b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/ui.ts index b80b7d783c..7a04cfcaef 100644 --- a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/ui.ts +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/__tests__/ui.ts @@ -25,10 +25,27 @@ export const ui = { name: "CARREFOUR PROXIMITE FRANCE (SHOPI-8 A HUIT)", }), }, + bnp: { + title: byText("BNP PARIBAS (HELLO BANK!)"), + link: byRole("link", { + name: "BNP PARIBAS (HELLO BANK!)", + }), + ccList: { + idcc2120: byLabelText("Banque IDCC 2120"), + idcc9999: byLabelText("___Sans convention collective___ IDCC 9999"), + idcc2931: byLabelText("Activités de marchés financiers IDCC 2931"), + }, + }, }, errorNotFound: { error: byText(/Aucune entreprise n'a été trouvée\./), info: byText(/Vous ne trouvez pas votre entreprise \?/), + notDeclared: byText( + /Aucune convention collective n'a été déclarée pour l'entreprise/ + ), + notTreated: byText( + "Nous n'avons pas de réponse pour cette convention collective" + ), }, }, enterpriseAgreementSelection: { diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/index.ts b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/index.ts index 8fa19fd2a3..e5a24e424a 100644 --- a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/index.ts +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/index.ts @@ -1,3 +1,3 @@ export * from "./EnterpriseAgreementSearch"; export * from "./EnterpriseAgreementSearchInput"; -export * from "./EnterpriseAgreementSelection"; +export * from "./EnterpriseAgreementSelectionLink"; diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/tracking.ts b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/tracking.ts index a15b3f4b60..b89b03bb75 100644 --- a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/tracking.ts +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/tracking.ts @@ -49,19 +49,27 @@ export const useEnterpriseAgreementSearchTracking = () => { }); }; - const emitNoEnterpriseEvent = () => { + const emitNoEnterpriseClickEvent = () => { sendEvent({ category: TrackingAgreementSearchCategory.CC_SEARCH_TYPE_OF_USERS, action: TrackingAgreementSearchAction.CLICK_NO_COMPANY, name: TrackingAgreementSearchAction.AGREEMENT_SEARCH, }); }; + const emitNoEnterpriseSelectEvent = () => { + sendEvent({ + category: TrackingAgreementSearchCategory.CC_SEARCH_TYPE_OF_USERS, + action: TrackingAgreementSearchAction.SELECT_NO_COMPANY, + name: TrackingAgreementSearchAction.AGREEMENT_SEARCH, + }); + }; return { emitEnterpriseAgreementSearchInputEvent, emitSelectEnterpriseEvent, emitSelectEnterpriseAgreementEvent, emitPreviousEvent, - emitNoEnterpriseEvent, + emitNoEnterpriseClickEvent, + emitNoEnterpriseSelectEvent, }; }; diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/utils.ts b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/utils.ts new file mode 100644 index 0000000000..2000b2652f --- /dev/null +++ b/packages/code-du-travail-frontend/src/modules/enterprise/EnterpriseAgreementSearch/utils.ts @@ -0,0 +1,31 @@ +import { EnterpriseAgreement } from "../types"; + +export type DescribedEnterpriseAgreement = EnterpriseAgreement & { + disabled: boolean; + description: string; +}; + +export const getEnterpriseAgreements = (agreements: EnterpriseAgreement[]) => { + return agreements.map((agreement) => { + const { slug, url, contributions } = agreement; + let disabled = false; + let description; + if (slug && !(url || contributions)) { + description = + "Nous n’avons pas d’informations concernant cette convention collective"; + disabled = true; + } else if (!slug) { + description = + "Cette convention collective déclarée par l’entreprise n’est pas reconnue par notre site"; + disabled = true; + } else { + description = + "Retrouvez les questions-réponses les plus fréquentes organisées par thème et élaborées par le Ministère du travail concernant cette convention collective"; + } + return { + ...agreement, + disabled, + description, + }; + }); +}; diff --git a/packages/code-du-travail-frontend/src/modules/enterprise/index.ts b/packages/code-du-travail-frontend/src/modules/enterprise/index.ts index ffe960f02f..5d890e3219 100644 --- a/packages/code-du-travail-frontend/src/modules/enterprise/index.ts +++ b/packages/code-du-travail-frontend/src/modules/enterprise/index.ts @@ -1,2 +1,3 @@ export * from "./EnterpriseAgreementSearch"; export * from "./queries"; +export * from "./types"; diff --git a/packages/code-du-travail-frontend/src/modules/layout/footer/__tests__/__snapshots__/index.test.tsx.snap b/packages/code-du-travail-frontend/src/modules/layout/footer/__tests__/__snapshots__/index.test.tsx.snap index 31bf911c0d..b9dbc2a639 100644 --- a/packages/code-du-travail-frontend/src/modules/layout/footer/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/code-du-travail-frontend/src/modules/layout/footer/__tests__/__snapshots__/index.test.tsx.snap @@ -2,9 +2,7 @@ exports[`