diff --git a/site/package.json b/site/package.json
index a4aad3d3c..87e2cceec 100644
--- a/site/package.json
+++ b/site/package.json
@@ -83,7 +83,6 @@
"modele-social": "workspace:^",
"publicodes": "=1.0.0-beta.40",
"publicodes-react": "=1.0.0-beta.40",
- "ramda": "^0.27.0",
"react": "^17.0.0",
"react-color": "^2.14.0",
"react-dom": "^17.0.0",
@@ -118,7 +117,6 @@
"@storybook/builder-vite": "^0.1.23",
"@storybook/react": "^6.5.0-alpha.49",
"@storybook/testing-library": "^0.0.9",
- "@types/ramda": "^0.26.43",
"@types/react": "^17.0.0",
"@types/react-color": "^3.0.1",
"@types/react-dom": "^17.0.9",
diff --git a/site/scripts/i18n/translate-ui.js b/site/scripts/i18n/translate-ui.js
index b1276f84d..89c88743a 100644
--- a/site/scripts/i18n/translate-ui.js
+++ b/site/scripts/i18n/translate-ui.js
@@ -1,5 +1,4 @@
import { readFileSync, writeFileSync } from 'fs'
-import { assocPath } from 'ramda'
import yaml from 'yaml'
import {
fetchTranslation,
@@ -36,3 +35,15 @@ import {
yaml.stringify(originalKeys, { sortMapEntries: true })
)
})()
+
+function assocPath(path, val, obj) {
+ if (path.length === 0) return val
+
+ const key = path[0]
+
+ if (path.length >= 2) {
+ val = assocPath(path.slice(1), val, obj?.[key] ?? {})
+ }
+
+ return { ...obj, [key]: val }
+}
diff --git a/site/scripts/i18n/utils.js b/site/scripts/i18n/utils.js
index 7a18901cc..24692bab3 100644
--- a/site/scripts/i18n/utils.js
+++ b/site/scripts/i18n/utils.js
@@ -1,8 +1,6 @@
import 'dotenv/config.js'
import { readFileSync } from 'fs'
import 'isomorphic-fetch'
-import { stringify } from 'querystring'
-import { equals, mergeAll, path as _path, pick } from 'ramda'
import yaml from 'yaml'
import rules from '../../../modele-social/dist/index.js'
@@ -27,68 +25,66 @@ export function getRulesMissingTranslations() {
)
let missingTranslations = []
- let resolved = Object.entries(rules)
- .map(([dottedName, rule]) => [
- dottedName,
- !rule || !rule.titre // && utils.ruleWithDedicatedDocumentationPage(rule))
- ? { ...rule, titre: dottedName.split(' . ').slice(-1)[0] }
- : rule,
- ])
- .map(([dottedName, rule]) => ({
- [dottedName]: mergeAll(
- Object.entries(rule)
- .filter(([, v]) => !!v)
- .map(([k, v]) => {
- let attrToTranslate = attributesToTranslate.find(equals(k))
- if (!attrToTranslate) return {}
- let enTrad = attrToTranslate + '.en',
- frTrad = attrToTranslate + '.fr'
+ let resolved = Object.fromEntries(
+ Object.entries(rules)
+ .map(([dottedName, rule]) => [
+ dottedName,
+ !rule || !rule.titre // && utils.ruleWithDedicatedDocumentationPage(rule))
+ ? { ...rule, titre: dottedName.split(' . ').slice(-1)[0] }
+ : rule,
+ ])
+ .map(([dottedName, rule]) => [
+ dottedName,
+ Object.fromEntries(
+ Object.entries(rule)
+ .filter(([, v]) => !!v)
+ .map(([k, v]) => {
+ let attrToTranslate = attributesToTranslate.find(
+ (attr) => attr === k
+ )
+ if (!attrToTranslate) return []
+ let enTrad = attrToTranslate + '.en'
+ let frTrad = attrToTranslate + '.fr'
- let currentTranslation = currentExternalization[dottedName]
+ let currentTranslation = currentExternalization[dottedName]
- if ('suggestions' === attrToTranslate) {
- return Object.keys(v).reduce((acc, suggestion) => {
- const enTrad = `suggestions.${suggestion}.en`
- const frTrad = `suggestions.${suggestion}.fr`
- if (
- currentTranslation &&
- currentTranslation[enTrad] &&
- currentTranslation[frTrad] === suggestion
- ) {
- return {
- ...acc,
- [frTrad]: currentTranslation[frTrad],
- [enTrad]: currentTranslation[enTrad],
+ if ('suggestions' === attrToTranslate) {
+ return Object.keys(v).reduce((acc, suggestion) => {
+ const enTrad = `suggestions.${suggestion}.en`
+ const frTrad = `suggestions.${suggestion}.fr`
+ if (
+ currentTranslation?.[enTrad] &&
+ currentTranslation?.[frTrad] === suggestion
+ ) {
+ return [
+ ...acc,
+ [frTrad, currentTranslation[frTrad]],
+ [enTrad, currentTranslation[enTrad]],
+ ]
}
- }
- missingTranslations.push([dottedName, enTrad, suggestion])
- return {
- ...acc,
- [frTrad]: suggestion,
- }
- }, {})
- }
-
- // Check if a human traduction exists already for this attribute and if
- // it does need to be updated
- if (
- currentTranslation &&
- currentTranslation[enTrad] &&
- currentTranslation[frTrad] === v
- )
- return {
- [enTrad]: currentTranslation[enTrad],
- [frTrad]: v,
+ missingTranslations.push([dottedName, enTrad, suggestion])
+ return [...acc, [frTrad, suggestion]]
+ }, [])
}
- missingTranslations.push([dottedName, enTrad, v])
- return {
- [frTrad]: v,
- }
- })
- ),
- }))
- resolved = mergeAll(resolved)
+ // Check if a human traduction exists already for this attribute and if
+ // it does need to be updated
+ if (
+ currentTranslation &&
+ currentTranslation[enTrad] &&
+ currentTranslation[frTrad] === v
+ )
+ return [
+ [enTrad, currentTranslation[enTrad]],
+ [frTrad, v],
+ ]
+ missingTranslations.push([dottedName, enTrad, v])
+ return [[frTrad, v]]
+ })
+ .flat()
+ ),
+ ])
+ )
return [missingTranslations, resolved]
}
@@ -105,26 +101,32 @@ export const getUiMissingTranslations = () => {
return false
}
const keys = key.split(/(?<=[A-zÀ-ü0-9])\.(?=[A-zÀ-ü0-9])/)
-
- const isNewKey = !_path(keys, translatedKeys)
- const isInvalidatedKey = _path(keys, originalKeys) !== valueInSource
+ const pathReducer = (currentSelection, subPath) =>
+ currentSelection?.[subPath]
+ const isNewKey = !keys.reduce(pathReducer, translatedKeys)
+ const isInvalidatedKey =
+ keys.reduce(pathReducer, originalKeys) !== valueInSource
return isNewKey || isInvalidatedKey
}, staticKeys)
.map(([key]) => key)
- return pick(missingTranslations, staticKeys)
+ return Object.fromEntries(
+ Object.entries(staticKeys).filter(([key]) =>
+ missingTranslations.includes(key)
+ )
+ )
}
export const fetchTranslation = async (text) => {
const response = await fetch(
- `https://api.deepl.com/v2/translate?${stringify({
+ `https://api.deepl.com/v2/translate?${new URLSearchParams({
text,
auth_key: process.env.DEEPL_API_SECRET,
tag_handling: 'xml',
source_lang: 'FR',
target_lang: 'EN',
- })}`
+ }).toString()}`
)
if (response.status !== 200) {
console.error(`❌ Deepl return status ${response.status} for:\n\t${text}\n`)
diff --git a/site/source/components/QuickLinks.tsx b/site/source/components/QuickLinks.tsx
index 57230b843..17e4d11d2 100644
--- a/site/source/components/QuickLinks.tsx
+++ b/site/source/components/QuickLinks.tsx
@@ -2,8 +2,6 @@ import { goToQuestion } from '@/actions/actions'
import { Spacing } from '@/design-system/layout'
import { Link } from '@/design-system/typography/link'
import { SmallBody } from '@/design-system/typography/paragraphs'
-import { DottedName } from 'modele-social'
-import { contains, filter, pipe, reject, toPairs } from 'ramda'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '@/reducers/rootReducer'
@@ -26,11 +24,10 @@ export default function QuickLinks() {
if (!quickLinks) {
return
}
- const links = pipe(
- reject((dottedName: DottedName) => contains(dottedName, quickLinksToHide)),
- filter((dottedName: DottedName) => contains(dottedName, nextSteps)),
- toPairs
- )(quickLinks)
+ const links = Object.entries(quickLinks).filter(
+ ([, dottedName]) =>
+ nextSteps.includes(dottedName) && !quickLinksToHide.includes(dottedName)
+ )
if (links.length < 1) {
return
diff --git a/site/source/components/search/SimulatorHits.tsx b/site/source/components/search/SimulatorHits.tsx
index 5e1e99f12..d83e9dd18 100644
--- a/site/source/components/search/SimulatorHits.tsx
+++ b/site/source/components/search/SimulatorHits.tsx
@@ -6,7 +6,6 @@ import InfoBulle from '@/design-system/InfoBulle'
import { H3 } from '@/design-system/typography/heading'
import { ExtractFromSimuData } from '@/pages/Simulateurs/metadata'
import { MetadataSrc } from '@/pages/Simulateurs/metadata-src'
-import { path } from 'ramda'
import { useContext } from 'react'
import { Trans } from 'react-i18next'
import { Hit } from 'react-instantsearch-core'
@@ -69,12 +68,12 @@ export const SimulatorHits = connectHits<
- }
+ path={hit.pathId
+ .split('.')
+ .reduce>(
+ (acc, curr) => acc[curr as any],
+ sitePaths as any
+ )}
/>
)
diff --git a/site/source/components/utils/useNextQuestion.tsx b/site/source/components/utils/useNextQuestion.tsx
index 110b6a4da..74d3c9278 100644
--- a/site/source/components/utils/useNextQuestion.tsx
+++ b/site/source/components/utils/useNextQuestion.tsx
@@ -1,29 +1,3 @@
-import { DottedName } from 'modele-social'
-import {
- add,
- countBy,
- descend,
- difference,
- equals,
- flatten,
- head,
- identity,
- keys,
- last,
- length,
- map,
- mergeWith,
- pair,
- pipe,
- reduce,
- sortBy,
- sortWith,
- takeWhile,
- toPairs,
- zipWith,
-} from 'ramda'
-import { useContext, useMemo } from 'react'
-import { useSelector } from 'react-redux'
import { Simulation, SimulationConfig } from '@/reducers/rootReducer'
import {
answeredQuestionsSelector,
@@ -32,6 +6,9 @@ import {
objectifsSelector,
situationSelector,
} from '@/selectors/simulationSelectors'
+import { DottedName } from 'modele-social'
+import { useContext, useMemo } from 'react'
+import { useSelector } from 'react-redux'
import { EngineContext } from './EngineContext'
type MissingVariables = Partial>
@@ -39,46 +16,72 @@ type MissingVariables = Partial>
export function getNextSteps(
missingVariables: Array
): Array {
- const byCount = ([, [count]]: [unknown, [number]]) => count
- const byScore = ([, [, score]]: [unknown, [unknown, number]]) => score
- const missingByTotalScore = reduce(
- mergeWith(add),
- {},
- missingVariables
+ const missingByTotalScore = missingVariables.reduce>(
+ (acc, mv) => ({
+ ...acc,
+ ...Object.fromEntries(
+ Object.entries(mv).map(([name, score]) => [
+ name,
+ (acc[name] || 0) + score,
+ ])
+ ),
+ }),
+ {}
)
- const innerKeys = flatten(map(keys, missingVariables))
+ const innerKeys = missingVariables.map((mv) => Object.keys(mv)).flat()
const missingByTargetsAdvanced = Object.fromEntries(
- Object.entries(countBy(identity, innerKeys)).map(
+ Object.entries(
+ innerKeys.reduce>(
+ (counters, key) => ({
+ ...counters,
+ [key]: (counters[key] ?? 0) + 1,
+ }),
+ {}
+ )
+ ).map(
// Give higher score to top level questions
([name, score]) => [name, score + Math.max(0, 4 - name.split('.').length)]
)
)
- const missingByCompound = mergeWith(
- pair,
- missingByTargetsAdvanced,
- missingByTotalScore
- )
- const pairs = toPairs(missingByCompound)
- const sortedPairs = sortWith(
- [descend(byCount), descend(byScore) as any],
- pairs
+ const missingByCompoundEntries = [
+ ...new Set([
+ ...Object.keys(missingByTargetsAdvanced),
+ ...Object.keys(missingByTotalScore),
+ ]),
+ ].map((name): [string, { score: number; count: number }] => [
+ name,
+ {
+ count: missingByTargetsAdvanced[name] ?? 0,
+ score: missingByTotalScore[name] ?? 0,
+ },
+ ])
+
+ const sortedEntries = missingByCompoundEntries.sort(
+ ([, scoresA], [, scoresB]) => {
+ if (scoresA.count === scoresB.count) {
+ return scoresB.score - scoresA.score
+ } else {
+ return scoresB.count - scoresA.count
+ }
+ }
)
- return map(head, sortedPairs) as any
+ return sortedEntries.map(([name]) => name) as Array
}
// Max : 1
// Min -> 0
-const questionDifference = (rule1 = '', rule2 = '') =>
- 1 /
- (1 +
- pipe(
- zipWith(equals),
- takeWhile(Boolean),
- length
- )(rule1.split(' . '), rule2.split(' . ')))
+const questionDifference = (ruleA = '', ruleB = '') => {
+ if (ruleA === ruleB) {
+ return 0
+ }
+ const partsA = ruleA.split(' . ')
+ const partsB = ruleB.split(' . ')
+
+ return 1 / (1 + partsA.findIndex((val, i) => partsB?.[i] !== val))
+}
export function getNextQuestions(
missingVariables: Array,
@@ -93,17 +96,22 @@ export function getNextQuestions(
"à l'affiche": displayed = {},
} = questionConfig
- let nextSteps = difference(
- [...Object.values(displayed), ...getNextSteps(missingVariables)],
- answeredQuestions
- )
- nextSteps = nextSteps.filter(
- (step) =>
- (!whitelist.length || whitelist.some((name) => step.startsWith(name))) &&
- (!blacklist.length || !blacklist.some((name) => step === name))
- )
+ const nextSteps = [
+ ...new Set([
+ ...Object.values(displayed),
+ ...getNextSteps(missingVariables),
+ ]),
+ ]
+ .filter((name) => !answeredQuestions.includes(name))
+ .filter(
+ (step) =>
+ (!whitelist.length ||
+ whitelist.some((name) => step.startsWith(name))) &&
+ (!blacklist.length || !blacklist.some((name) => step === name))
+ )
+
+ const lastStep = answeredQuestions[answeredQuestions.length - 1]
- const lastStep = last(answeredQuestions)
// L'ajout de la réponse permet de traiter les questions dont la réponse est
// "une possibilité", exemple "contrat salarié . cdd"
const lastStepWithAnswer =
@@ -114,7 +122,7 @@ export function getNextQuestions(
.trim() as DottedName)
: lastStep
- return sortBy((question) => {
+ const score = (question: string) => {
const indexList =
whitelist.findIndex((name) => question.startsWith(name)) + 1
const indexNotPriority =
@@ -122,7 +130,9 @@ export function getNextQuestions(
const differenceCoeff = questionDifference(question, lastStepWithAnswer)
return indexList + indexNotPriority + differenceCoeff
- }, nextSteps)
+ }
+
+ return nextSteps.sort((a, b) => score(a) - score(b))
}
export const useNextQuestions = function (): Array {
diff --git a/site/source/pages/Stats/SatisfactionChart.tsx b/site/source/pages/Stats/SatisfactionChart.tsx
index 4202b7876..8f3d50440 100644
--- a/site/source/pages/Stats/SatisfactionChart.tsx
+++ b/site/source/pages/Stats/SatisfactionChart.tsx
@@ -26,9 +26,12 @@ export const SatisfactionStyle: [
function toPercentage(data: Record): Record {
const total = Object.values(data).reduce((a, b: number) => a + b, 0)
- return Object.fromEntries(
- Object.entries(data).map(([key, value]) => [key, (100 * value) / total])
- )
+ return {
+ ...Object.fromEntries(
+ Object.entries(data).map(([key, value]) => [key, (100 * value) / total])
+ ),
+ total,
+ }
}
type SatisfactionChartProps = {
@@ -47,29 +50,23 @@ export default function SatisfactionChart({ data }: SatisfactionChartProps) {
.filter((d) => Object.values(d.nombre).reduce((a, b) => a + b, 0))
return (
- <>
-
-
-
-
- {SatisfactionStyle.map(([level, { emoji, color }]) => (
-
- emoji}
- position="left"
- />
-
- ))}
-
-
- >
+
+
+
+
+ {SatisfactionStyle.map(([level, { emoji, color }]) => (
+
+ emoji} position="left" />
+
+ ))}
+
+
)
}
diff --git a/site/source/pages/Stats/Stats.tsx b/site/source/pages/Stats/Stats.tsx
index b321975ae..fe5d417af 100644
--- a/site/source/pages/Stats/Stats.tsx
+++ b/site/source/pages/Stats/Stats.tsx
@@ -8,13 +8,12 @@ import { Item, Select } from '@/design-system/field/Select'
import { Spacing } from '@/design-system/layout'
import { H2, H3 } from '@/design-system/typography/heading'
import { formatValue } from 'publicodes'
-import { add, groupBy, mapObjIndexed, mergeWith } from 'ramda'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Trans } from 'react-i18next'
import { useHistory, useLocation } from 'react-router-dom'
import { toAtString } from '../../ATInternetTracking'
import statsJson from '@/data/stats.json'
-import { debounce } from '../../utils'
+import { debounce, groupBy } from '../../utils'
import { SimulateurCard } from '../Simulateurs/Home'
import useSimulatorsData, { SimulatorData } from '../Simulateurs/metadata'
import Chart, { Data, isDataStacked } from './Chart'
@@ -48,20 +47,24 @@ const isPAM = (name: string | undefined) =>
const filterByChapter2 = (pages: Pageish[], chapter2: Chapter2 | '') => {
return Object.entries(
groupBy(
- (p) => ('date' in p ? p.date : p.month),
pages.filter(
(p) =>
!chapter2 ||
((!('page' in p) || p.page !== 'accueil_pamc') &&
(p.page_chapter2 === chapter2 ||
(chapter2 === 'PAM' && isPAM(p.page_chapter3))))
- )
+ ),
+ (p) => ('date' in p ? p.date : p.month)
)
).map(([date, values]) => ({
date,
- nombre: mapObjIndexed(
- (v: Array<{ nombre: number }>) => v.map((v) => v.nombre).reduce(add),
- groupBy((x) => ('page' in x ? x.page : x.click), values)
+ nombre: Object.fromEntries(
+ Object.entries(
+ groupBy(values, (x) => ('page' in x ? x.page : x.click))
+ ).map(([key, values]) => [
+ key,
+ values.reduce((sum, value) => sum + value.nombre, 0),
+ ])
),
}))
}
@@ -69,16 +72,19 @@ const filterByChapter2 = (pages: Pageish[], chapter2: Chapter2 | '') => {
function groupByDate(data: Pageish[]) {
return Object.entries(
groupBy(
- (p) => ('date' in p ? p.date : p.month),
- data.filter((d) => 'page' in d && d.page === 'accueil')
+ data.filter((d) => 'page' in d && d.page === 'accueil'),
+ (p) => ('date' in p ? p.date : p.month)
)
).map(([date, values]) => ({
date,
nombre: Object.fromEntries(
Object.entries(
- groupBy((x) => x.page_chapter1 + ' / ' + x.page_chapter2, values)
+ groupBy(values, (x) => x.page_chapter1 + ' / ' + x.page_chapter2)
)
- .map(([k, v]) => [k, v.map((v) => v.nombre).reduce(add, 0)] as const)
+ .map(
+ ([k, v]) =>
+ [k, v.map((v) => v.nombre).reduce((a, b) => a + b, 0)] as const
+ )
.sort((a, b) => b[1] - a[1])
.slice(0, 7)
),
@@ -89,8 +95,19 @@ const computeTotals = (
data: Data | Data>
): number | Record => {
return isDataStacked(data)
- ? data.map((d) => d.nombre).reduce(mergeWith(add), {})
- : data.map((d) => d.nombre).reduce(add, 0)
+ ? data
+ .map((d) => d.nombre)
+ .reduce(
+ (acc, record) =>
+ [...Object.entries(acc), ...Object.entries(record)].reduce(
+ (merge, [key, value]) => {
+ return { ...merge, [key]: (acc[key] ?? 0) + value }
+ },
+ {}
+ ),
+ {}
+ )
+ : data.map((d) => d.nombre).reduce((a, b) => a + b, 0)
}
interface BrushStartEndIndex {
diff --git a/site/source/pages/gerer/_components/Fields.tsx b/site/source/pages/gerer/_components/Fields.tsx
index c47700831..eb220e4d6 100644
--- a/site/source/pages/gerer/_components/Fields.tsx
+++ b/site/source/pages/gerer/_components/Fields.tsx
@@ -16,7 +16,6 @@ import { evaluateQuestion, getMeta } from '@/utils'
import { useSSRSafeId } from '@react-aria/ssr'
import { DottedName } from 'modele-social'
import { RuleNode } from 'publicodes'
-import { isEmpty } from 'ramda'
import { useCallback, useContext } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
@@ -135,7 +134,7 @@ export function SimpleField({
required={meta.requis === 'oui'}
missing={
evaluation.nodeValue === undefined ||
- !isEmpty(evaluation.missingVariables)
+ Object.keys(evaluation.missingVariables).length > 0
}
onChange={dispatchValue}
showSuggestions={showSuggestions}
diff --git a/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx b/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx
index d69a1c7ec..809d1f708 100644
--- a/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx
+++ b/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx
@@ -7,7 +7,6 @@ import { Strong } from '@/design-system/typography'
import { H3 } from '@/design-system/typography/heading'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import { useOrdinal } from '@/hooks/useOrdinal'
-import { isEmpty } from 'ramda'
import { useCallback } from 'react'
import { Trans } from 'react-i18next'
import { useDispatch } from 'react-redux'
@@ -28,7 +27,7 @@ export default function ModeAccompagnement() {
const dispatch = useDispatch()
const imposition = engine.evaluate('entreprise . imposition')
- if (isSelected && !isEmpty(imposition.missingVariables)) {
+ if (isSelected && Object.keys(imposition.missingVariables).length > 0) {
dispatch(
updateSituation(
'entreprise . imposition',
diff --git a/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts b/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts
index 86c7353ba..4fd3d56ac 100644
--- a/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts
+++ b/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts
@@ -1,7 +1,6 @@
import { useEngine } from '@/components/utils/EngineContext'
import { DottedName } from 'modele-social'
import { RuleNode } from 'publicodes'
-import { isEmpty } from 'ramda'
import { useMemo } from 'react'
export function useProgress(objectifs: DottedName[]): number {
@@ -13,8 +12,8 @@ export function useProgress(objectifs: DottedName[]): number {
const objectifsApplicables = evaluatedObjectifs.filter(
(objectif) => objectif.nodeValue !== null
)
- const objectifsRemplis = objectifsApplicables.filter((objectif) =>
- isEmpty(objectif.missingVariables)
+ const objectifsRemplis = objectifsApplicables.filter(
+ (objectif) => Object.keys(objectif.missingVariables).length === 0
)
if (!objectifsApplicables.length) {
diff --git a/site/source/selectors/companyStatusSelectors.ts b/site/source/selectors/companyStatusSelectors.ts
index 8a1839886..d672bfe78 100644
--- a/site/source/selectors/companyStatusSelectors.ts
+++ b/site/source/selectors/companyStatusSelectors.ts
@@ -1,17 +1,4 @@
import { SitePathsContext } from '@/components/utils/SitePathsContext'
-import {
- add,
- any,
- countBy,
- difference,
- flatten,
- isNil,
- keys,
- map,
- mergeAll,
- mergeWith,
- sortBy,
-} from 'ramda'
import { useContext } from 'react'
import { useSelector } from 'react-redux'
import { RootState } from '@/reducers/rootReducer'
@@ -92,39 +79,49 @@ const LEGAL_STATUS_DETAILS = {
export type LegalStatus = keyof typeof LEGAL_STATUS_DETAILS
type Question = keyof LegalStatusRequirements
+type Answers = LegalStatusRequirements
-const QUESTION_LIST: Array = keys(
- mergeAll(flatten(Object.values(LEGAL_STATUS_DETAILS)))
-)
+const QUESTION_LIST: Array = [
+ 'soleProprietorship',
+ 'directorStatus',
+ 'minorityDirector',
+ 'multipleAssociates',
+ 'autoEntrepreneur',
+]
-const isCompatibleStatusWith =
- (answers: any) =>
- (statusRequirements: LegalStatusRequirements): boolean => {
- const stringify = map((x) => (!isNil(x) ? JSON.stringify(x) : x))
- const answerCompatibility = Object.values(
- mergeWith(
- (answer, statusValue) =>
- isNil(answer) || isNil(statusValue) || answer === statusValue,
- stringify(statusRequirements as any),
- stringify(answers)
+function isCompatibleStatusWith(
+ answers: Answers,
+ statusRequirements: LegalStatusRequirements
+): boolean {
+ return Object.entries(statusRequirements).reduce(
+ (isCompatible, [question, statusValue]) => {
+ const answer = answers[question as Question]
+
+ return (
+ isCompatible &&
+ (answer == null ||
+ statusValue == null ||
+ JSON.stringify(answer) === JSON.stringify(statusValue))
)
- )
- const isCompatibleStatus = answerCompatibility.every((x) => x !== false)
+ },
+ true
+ )
+}
- return isCompatibleStatus
- }
-const possibleStatus = (
- answers: Array | LegalStatusRequirements
-): Record =>
- map(
- (statusRequirements) =>
+const possibleStatus = (answers: Answers): Record =>
+ Object.fromEntries(
+ Object.entries(LEGAL_STATUS_DETAILS).map(([key, statusRequirements]) => [
+ key,
Array.isArray(statusRequirements)
- ? any(isCompatibleStatusWith(answers as any), statusRequirements)
- : isCompatibleStatusWith(answers as any)(
+ ? !!statusRequirements.some((requirement) =>
+ isCompatibleStatusWith(answers, requirement)
+ )
+ : isCompatibleStatusWith(
+ answers,
statusRequirements as LegalStatusRequirements
),
- LEGAL_STATUS_DETAILS
- )
+ ])
+ ) as Record
export const possibleStatusSelector = (state: {
choixStatutJuridique: State
@@ -136,9 +133,14 @@ export const nextQuestionSelector = (state: RootState): Question | null => {
const questionAnswered = Object.keys(
legalStatusRequirements
) as Array
- const possibleStatusList = flatten(
- Object.values(LEGAL_STATUS_DETAILS)
- ).filter(isCompatibleStatusWith(legalStatusRequirements) as any)
+ const possibleStatusList = Object.values(LEGAL_STATUS_DETAILS)
+ .flat()
+ .filter((requirement) =>
+ isCompatibleStatusWith(legalStatusRequirements, requirement as any)
+ )
+
+ const difference = (l1: Array, l2: Array): Array =>
+ l1.filter((x) => !l2.includes(x))
const unansweredQuestions = difference(QUESTION_LIST, questionAnswered)
const shannonEntropyByQuestion = unansweredQuestions.map(
@@ -146,24 +148,31 @@ export const nextQuestionSelector = (state: RootState): Question | null => {
const answerPopulation = Object.values(possibleStatusList).map(
(status: any) => status[question]
)
- const frequencyOfAnswers = Object.values(
- countBy(
- (x) => x,
- answerPopulation.filter((x) => x !== undefined)
- )
+
+ const frequencyOfAnswers = Object.values(
+ answerPopulation
+ .filter((x) => x !== undefined)
+ .reduce(
+ (counters: Record, i) => ({
+ ...counters,
+ [i]: (counters?.[i] ?? 0) + 1,
+ }),
+ {}
+ )
).map((numOccurrence) => numOccurrence / answerPopulation.length)
const shannonEntropy = -frequencyOfAnswers
.map((p) => p * Math.log2(p))
- .reduce(add, 0)
+ .reduce((a, b) => a + b, 0)
return [question, shannonEntropy]
}
)
- const sortedPossibleNextQuestions = sortBy(
- ([, entropy]) => -entropy,
- shannonEntropyByQuestion.filter(([, entropy]) => entropy !== 0)
- ).map(([question]) => question)
+ const sortedPossibleNextQuestions = shannonEntropyByQuestion
+ .filter(([, entropy]) => entropy !== 0)
+ .sort(([, entropy1], [, entropy2]) => entropy2 - entropy1)
+ .map(([question]) => question)
+
if (sortedPossibleNextQuestions.length === 0) {
return null
}
diff --git a/site/source/utils.ts b/site/source/utils.ts
index 52511a6e5..cbefceec9 100644
--- a/site/source/utils.ts
+++ b/site/source/utils.ts
@@ -82,6 +82,24 @@ export function omit(obj: T, key: K): Omit {
return returnObject
}
+// TODO: This is will be included in the ES spec soon. Remove our custom
+// implementation and rely on browser native support and polyfill when it is
+// available.
+// https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Array/groupBy
+// https://caniuse.com/?search=groupby
+export function groupBy(
+ arr: Array,
+ callback: (elm: E, index: number, array: Array) => G
+): Record> {
+ return arr.reduce((result, item, currentIndex) => {
+ const key = callback(item, currentIndex, arr)
+ result[key] = result[key] || []
+ result[key].push(item)
+
+ return result
+ }, {} as Record>)
+}
+
export function isIterable(obj: unknown): obj is Iterable {
return Symbol.iterator in Object(obj)
}
diff --git a/yarn.lock b/yarn.lock
index 1ad505be2..d8a9916a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6065,15 +6065,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/ramda@npm:^0.26.43":
- version: 0.26.44
- resolution: "@types/ramda@npm:0.26.44"
- dependencies:
- ts-toolbelt: ^6.3.3
- checksum: 5dbb5dfb0311d4c777ee4d65df4c553a5af4bde06148c149d70fffdfe2498fe3709d892e681f4bdba907662e74ed7d7a09359d13b489f0797edc20719de45a10
- languageName: node
- linkType: hard
-
"@types/react-color@npm:^3.0.1":
version: 3.0.6
resolution: "@types/react-color@npm:3.0.6"
@@ -16803,7 +16794,7 @@ __metadata:
languageName: node
linkType: hard
-"ramda@npm:^0.27.0, ramda@npm:^0.27.1":
+"ramda@npm:^0.27.1":
version: 0.27.2
resolution: "ramda@npm:0.27.2"
checksum: 28d6735dd1eea1a796c56cf6111f3673c6105bbd736e521cdd7826c46a18eeff337c2dba4668f6eed990d539b9961fd6db19aa46ccc1530ba67a396c0a9f580d
@@ -18473,7 +18464,6 @@ __metadata:
"@storybook/builder-vite": ^0.1.23
"@storybook/react": ^6.5.0-alpha.49
"@storybook/testing-library": ^0.0.9
- "@types/ramda": ^0.26.43
"@types/react": ^17.0.0
"@types/react-color": ^3.0.1
"@types/react-dom": ^17.0.9
@@ -18500,7 +18490,6 @@ __metadata:
modele-social: "workspace:^"
publicodes: =1.0.0-beta.40
publicodes-react: =1.0.0-beta.40
- ramda: ^0.27.0
react: ^17.0.0
react-color: ^2.14.0
react-dom: ^17.0.0
@@ -19691,13 +19680,6 @@ __metadata:
languageName: node
linkType: hard
-"ts-toolbelt@npm:^6.3.3":
- version: 6.15.5
- resolution: "ts-toolbelt@npm:6.15.5"
- checksum: 24ad00cfd9ce735c76c873a9b1347eac475b94e39ebbdf100c9019dce88dd5f4babed52884cf82bb456a38c28edd0099ab6f704b84b2e5e034852b618472c1f3
- languageName: node
- linkType: hard
-
"tsconfig-paths@npm:^3.14.1":
version: 3.14.1
resolution: "tsconfig-paths@npm:3.14.1"