Améliore les statistiques
- Tous les simulateurs sont désormais présents dans la page stat - Refacto du code, et séparation en plusieurs fichiers - Ajout des stats pour l'assistant au choix du statutpull/2757/head
parent
1952a15784
commit
8d635bdbbb
|
@ -55,119 +55,12 @@ const buildSimulateursQuery = (period, granularity) => ({
|
|||
granularity,
|
||||
top: {
|
||||
'page-num': 1,
|
||||
'max-results': 500,
|
||||
'max-results': 5000,
|
||||
sort: ['-m_visits'],
|
||||
filter: {
|
||||
property: {
|
||||
page_chapter1: {
|
||||
$in: ['gerer', 'simulateurs'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
ignore_null_properties: true,
|
||||
},
|
||||
})
|
||||
|
||||
const buildCreerQuery = (period, granularity) => ({
|
||||
columns: [
|
||||
'page',
|
||||
'page_chapter1',
|
||||
'page_chapter2',
|
||||
'page_chapter3',
|
||||
'm_visits',
|
||||
],
|
||||
space: {
|
||||
s: [617190, 617189],
|
||||
},
|
||||
period: {
|
||||
p1: [period],
|
||||
},
|
||||
evo: {
|
||||
granularity,
|
||||
top: {
|
||||
'page-num': 1,
|
||||
'max-results': 500,
|
||||
sort: ['-m_visits'],
|
||||
filter: {
|
||||
property: {
|
||||
$AND: [
|
||||
{
|
||||
page: {
|
||||
$eq: 'accueil',
|
||||
},
|
||||
},
|
||||
{
|
||||
page_chapter1: {
|
||||
$eq: 'creer',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
ignore_null_properties: true,
|
||||
},
|
||||
})
|
||||
|
||||
const buildCreerSegmentQuery = (period, granularity) => ({
|
||||
columns: [
|
||||
'page',
|
||||
'page_chapter1',
|
||||
'page_chapter2',
|
||||
'page_chapter3',
|
||||
'm_visits',
|
||||
],
|
||||
segment: {
|
||||
section: {
|
||||
scope: 'visit_id',
|
||||
category: 'property',
|
||||
coverage: 'at_least_one_visit',
|
||||
content: {
|
||||
and: [
|
||||
{
|
||||
condition: {
|
||||
filter: {
|
||||
page_chapter1: {
|
||||
$eq: 'creer',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: {
|
||||
filter: {
|
||||
page: {
|
||||
$eq: 'accueil',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
mode: 'include',
|
||||
},
|
||||
},
|
||||
space: {
|
||||
s: [617190],
|
||||
},
|
||||
period: {
|
||||
p1: [period],
|
||||
},
|
||||
evo: {
|
||||
granularity,
|
||||
top: {
|
||||
'page-num': 1,
|
||||
'max-results': 500,
|
||||
sort: ['-m_visits'],
|
||||
filter: {
|
||||
property: {
|
||||
page_chapter1: {
|
||||
$eq: 'creer',
|
||||
$in: ['assistant', 'simulateurs', 'gérer'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -196,14 +89,14 @@ const buildSatisfactionQuery = () => ({
|
|||
granularity: 'M',
|
||||
top: {
|
||||
'page-num': 1,
|
||||
'max-results': 500,
|
||||
'max-results': 5000,
|
||||
sort: ['-m_events'],
|
||||
filter: {
|
||||
property: {
|
||||
$AND: [
|
||||
{
|
||||
page_chapter1: {
|
||||
$in: ['creer', 'gerer', 'simulateurs'],
|
||||
$in: ['assistant', 'gerer', 'simulateurs'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -233,7 +126,7 @@ const buildSiteQuery = (period, granularity) => ({
|
|||
granularity,
|
||||
top: {
|
||||
'page-num': 1,
|
||||
'max-results': 500,
|
||||
'max-results': 5000,
|
||||
sort: ['-m_visits'],
|
||||
},
|
||||
},
|
||||
|
@ -286,40 +179,32 @@ const uniformiseData = (data) =>
|
|||
|
||||
const flattenPage = (list) =>
|
||||
list
|
||||
.filter(
|
||||
(p) => p && (p.page_chapter2 !== 'N/A' || p.page_chapter1 === 'creer')
|
||||
) // Remove simulateur landing page
|
||||
.filter((p) => p && p.page_chapter2 !== 'N/A') // Remove landing pages
|
||||
.map(({ Rows, ...page }) => Rows.map((r) => ({ ...page, ...r })))
|
||||
.flat()
|
||||
|
||||
async function fetchDailyVisits() {
|
||||
const pages = uniformiseData([
|
||||
...flattenPage(await fetchApi(buildSimulateursQuery(last60days, 'D'))),
|
||||
...flattenPage(await fetchApi(buildCreerQuery(last60days, 'D'))),
|
||||
])
|
||||
const pages = uniformiseData(
|
||||
flattenPage(await fetchApi(buildSimulateursQuery(last60days, 'D')))
|
||||
)
|
||||
const site = uniformiseData(
|
||||
(await fetchApi(buildSiteQuery(last60days, 'D')))[0].Rows
|
||||
)
|
||||
|
||||
const creer = uniformiseData(
|
||||
flattenPage(await fetchApi(buildCreerSegmentQuery(last60days, 'D')))
|
||||
)
|
||||
|
||||
const { start, end } = last60days
|
||||
|
||||
return {
|
||||
pages,
|
||||
site,
|
||||
creer,
|
||||
|
||||
api: await apiStats(start, end, 'date'),
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMonthlyVisits() {
|
||||
const pages = uniformiseData([
|
||||
...flattenPage(await fetchApi(buildSimulateursQuery(last12Months, 'M'))),
|
||||
...flattenPage(await fetchApi(buildCreerQuery(last12Months, 'M'))),
|
||||
])
|
||||
const pages = uniformiseData(
|
||||
flattenPage(await fetchApi(buildSimulateursQuery(last12Months, 'M')))
|
||||
)
|
||||
|
||||
const site = [
|
||||
...matomoSiteVisitsHistory.map(({ date, visites }) => ({
|
||||
|
@ -331,16 +216,11 @@ async function fetchMonthlyVisits() {
|
|||
),
|
||||
]
|
||||
|
||||
const creer = uniformiseData(
|
||||
flattenPage(await fetchApi(buildCreerSegmentQuery(last12Months, 'M')))
|
||||
)
|
||||
|
||||
const { start, end } = last12Months
|
||||
|
||||
return {
|
||||
pages,
|
||||
site,
|
||||
creer,
|
||||
api: await apiStats(start, end, 'month'),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1250,32 +1250,23 @@ dirigeant . exonérations . ACRE:
|
|||
titre.en: '[automatic] Default ACRE'
|
||||
titre.fr: ACRE par défaut
|
||||
description.en: >-
|
||||
[automatic] The aid for creating or taking over a business
|
||||
(Acre) consists of a partial exemption from social security charges, known
|
||||
as the start-up exemption, for 12 months.
|
||||
[automatic] Assistance for setting up or taking over a business
|
||||
(Acre) consists of a partial exemption from social security contributions,
|
||||
known as the "start-up exemption", for a period of 12 months.
|
||||
|
||||
|
||||
It is **automatic** for **companies and sole proprietorships** (under certain conditions, such as not having benefited from it in the last three years).
|
||||
It is **automatic** for **companies and sole proprietorships** (subject to certain conditions, such as not having benefited from it in the last three years).
|
||||
|
||||
|
||||
For **self-employed** entrepreneurs, however, it must be requested and is reserved for the following beneficiaries
|
||||
For **self-employed** entrepreneurs, on the other hand, it must be applied for, and is reserved for the following beneficiaries:
|
||||
|
||||
- Jobseekers (whether or not they receive compensation but have been registered with the Pôle Emploi for at least 6 months during the last 18 months).
|
||||
- Jobseekers (with or without benefits, but registered with Pôle Emploi for at least 6 months in the last 18 months).
|
||||
|
||||
- Recipients of social assistance (RSA, ASS, ATA)
|
||||
- Recipients of social assistance (RSA, ASS, ATA).
|
||||
|
||||
- Young people between 18 and 25 years old (up to 29 years old for people recognized as disabled)
|
||||
- Young people aged between 18 and 25 (up to 29 for people recognized as disabled).
|
||||
|
||||
- People creating a micro-enterprise in a priority district of the city (QPPV)
|
||||
|
||||
|
||||
> *Historical*:
|
||||
|
||||
- For auto-businesses created from January 1, 2020, the exemption is again subject to conditions.
|
||||
|
||||
- For businesses created between January 1, 2019 and December 31, 2019, the reduction is generalized to all creators, unless you have already obtained the ACCRE in the previous three years
|
||||
|
||||
- For companies created before January 1, 2019, the exemption of contribution called ACCRE was subject to conditions and was not automatic: you had to apply.
|
||||
- People setting up a micro-business in a priority urban district (QPPV).
|
||||
description.fr: >-
|
||||
L'aide à la création ou à la reprise d'une entreprise (Acre)
|
||||
consiste en une exonération partielle de charges sociales, dite exonération
|
||||
|
@ -1294,15 +1285,6 @@ dirigeant . exonérations . ACRE:
|
|||
- Les jeunes entre 18 et 25 ans (jusqu'à 29 ans pour les personnes reconnues en situation de handicap)
|
||||
|
||||
- Les personnes créant une micro-entreprise dans un quartier prioritaire de la ville (QPPV)
|
||||
|
||||
|
||||
> *Historique*:
|
||||
|
||||
- Pour les auto-entreprise créées à partir du 1er janvier 2020, l'exonération est de nouveau soumise à condition.
|
||||
|
||||
- Pour les entreprises créées entre le 1er janvier 2019 et le 31 décembre 2019, la réduction est généralisée à tous les créateurs, sauf si vous avez déjà obtenu l'ACCRE dans les trois années précédentes
|
||||
|
||||
- Pour les entreprises créées avant le 1er janvier 2019, la l'exonération de cotisation s'appelait ACCRE était soumise à conditions et n'était pas automatique : il fallait en faire la demande.
|
||||
question.en: '[automatic] Did you benefit from the ACRE?'
|
||||
question.fr: Bénéficiez-vous de l'ACRE ?
|
||||
titre.en: '[automatic] ACRE'
|
||||
|
|
|
@ -479,10 +479,10 @@ choix-statut:
|
|||
question3:
|
||||
help:
|
||||
title: Choosing between a sole proprietorship and a company
|
||||
label: Would you prefer to run your business solely as a company?<1><0>Sole
|
||||
label: Do you want to run your business as a company only?<1><0>Sole
|
||||
proprietorship</0><1><0>You are <2>responsible for all your business
|
||||
assets</2> (equipment, premises, tools, etc.). </0><1>Your <2>personal
|
||||
assets may be seized</2> if you fail to meet your tax and social
|
||||
assets can be seized</2> if you fail to meet your tax and social
|
||||
security obligations.</1><2>You can <2>waive the separation of your
|
||||
assets</2>, for example to secure a bank loan.</2><3>The formalities
|
||||
involved in setting up and running a company are <2>simpler and less
|
||||
|
|
|
@ -504,14 +504,14 @@ choix-statut:
|
|||
question3:
|
||||
help:
|
||||
title: Choisir entre une entreprise individuelle et une société
|
||||
label: Préférez-vous exercer votre activité sous la forme d'une société
|
||||
uniquement ?<1><0>Entreprise individuelle</0><1><0>Vous êtes
|
||||
<2>responsable sur l'ensemble de vos biens utiles à votre activité</2>
|
||||
(matériel, locaux, outils, etc.). </0><1>Votre <2>patrimoine personnel
|
||||
peut être saisi</2> en cas de manquements à vos obligations fiscales et
|
||||
sociales.</1><2>Vous pouvez <2>renoncer à la séparation de ses
|
||||
patrimoines</2>, par exemple pour garantir un crédit bancaire.</2><3>Les
|
||||
formalités de création et de gestion sont <2>plus simples et moins
|
||||
label: Voulez-vous exercer votre activité sous la forme d'une société uniquement
|
||||
?<1><0>Entreprise individuelle</0><1><0>Vous êtes <2>responsable sur
|
||||
l'ensemble de vos biens utiles à votre activité</2> (matériel, locaux,
|
||||
outils, etc.). </0><1>Votre <2>patrimoine personnel peut être saisi</2>
|
||||
en cas de manquements à vos obligations fiscales et sociales.</1><2>Vous
|
||||
pouvez <2>renoncer à la séparation de ses patrimoines</2>, par exemple
|
||||
pour garantir un crédit bancaire.</2><3>Les formalités de création et de
|
||||
gestion sont <2>plus simples et moins
|
||||
coûteuses.</2></3></1><2>Société</2><3><0>Vous êtes uniquement
|
||||
<2>responsable sur le montant de votre apport au capital social</2> et
|
||||
des biens détenus par la société.</0><1>Les formalités de création et de
|
||||
|
|
|
@ -121,8 +121,8 @@ export default function Associés() {
|
|||
<Message type="secondary" border={false}>
|
||||
<H4 as="h3" id="question3">
|
||||
<Trans i18nKey="choix-statut.associés.question3.label">
|
||||
Préférez-vous exercer votre activité sous la forme d'une
|
||||
société uniquement ?
|
||||
Voulez-vous exercer votre activité sous la forme d'une société
|
||||
uniquement ?
|
||||
<HelpButtonWithPopover
|
||||
title={t(
|
||||
'choix-statut.associés.question3.help.title',
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import { Item } from '@/design-system'
|
||||
import { Emoji } from '@/design-system/emoji'
|
||||
import { Select } from '@/design-system/field/Select'
|
||||
import useSimulatorsData from '@/hooks/useSimulatorsData'
|
||||
|
||||
import { SimulateurCard } from '../../components/SimulateurCard'
|
||||
import { getFilter } from './StatsDetail'
|
||||
import { Filter } from './useStatistiques'
|
||||
|
||||
export function SelectedSimulator(props: { filter: Filter | '' }) {
|
||||
const simulateur = Object.values(useSimulatorsData()).find(
|
||||
(s) => JSON.stringify(getFilter(s)) === JSON.stringify(props.filter)
|
||||
)
|
||||
if (!simulateur) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <SimulateurCard small {...simulateur} />
|
||||
}
|
||||
export function SimulateursChoice(props: {
|
||||
onChange: (ch: Filter | '') => void
|
||||
value: Filter | ''
|
||||
}) {
|
||||
const simulateurs = useSimulatorsData()
|
||||
const choices = Object.values(simulateurs)
|
||||
.filter((s) => getFilter(s))
|
||||
.sort((a, b) => (a.shortName < b.shortName ? -1 : 1))
|
||||
|
||||
return (
|
||||
<Select
|
||||
onSelectionChange={(val) => {
|
||||
if (val === '' || val === 'api-rest') {
|
||||
return props.onChange(val)
|
||||
}
|
||||
if (!(val in simulateurs)) {
|
||||
return
|
||||
}
|
||||
props.onChange(getFilter(simulateurs[val as keyof typeof simulateurs]))
|
||||
}}
|
||||
defaultSelectedKey={
|
||||
typeof props.value === 'string'
|
||||
? props.value
|
||||
: JSON.stringify(props.value)
|
||||
}
|
||||
label={'Voir les statistiques pour :'}
|
||||
id="simulator-choice-input"
|
||||
>
|
||||
{[
|
||||
<Item key="" textValue="Tout le site">
|
||||
<Emoji emoji="🌍" />
|
||||
Tout le site
|
||||
</Item>,
|
||||
<Item key="api-rest" textValue="API REST">
|
||||
<Emoji emoji="👩💻" />
|
||||
API REST
|
||||
</Item>,
|
||||
...choices.map((s) => (
|
||||
<Item key={s.id} textValue={s.shortName}>
|
||||
{s.icône && (
|
||||
<>
|
||||
<Emoji emoji={s.icône} />
|
||||
|
||||
</>
|
||||
)}
|
||||
{s.shortName}
|
||||
</Item>
|
||||
)),
|
||||
]}
|
||||
</Select>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { formatValue } from 'publicodes'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { BrushProps } from 'recharts'
|
||||
|
@ -8,28 +8,25 @@ import styled from 'styled-components'
|
|||
import { toAtString } from '@/components/ATInternetTracking'
|
||||
import PagesChart from '@/components/charts/PagesCharts'
|
||||
import { useScrollToHash } from '@/components/utils/markdown'
|
||||
import { Item, Message, Radio, ToggleGroup } from '@/design-system'
|
||||
import { Emoji } from '@/design-system/emoji'
|
||||
import { Select } from '@/design-system/field/Select'
|
||||
import { Message, Radio, ToggleGroup } from '@/design-system'
|
||||
import InfoBulle from '@/design-system/InfoBulle'
|
||||
import { Grid, Spacing } from '@/design-system/layout'
|
||||
import { H2, H3 } from '@/design-system/typography/heading'
|
||||
import { Body } from '@/design-system/typography/paragraphs'
|
||||
import useSimulatorsData, { SimulatorData } from '@/hooks/useSimulatorsData'
|
||||
import { debounce, groupBy } from '@/utils'
|
||||
import { debounce } from '@/utils'
|
||||
|
||||
import { SimulateurCard } from '../../components/SimulateurCard'
|
||||
import { AccessibleTable } from './AccessibleTable'
|
||||
import Chart, { Data, formatLegend, isDataStacked } from './Chart'
|
||||
import Chart, { formatLegend } from './Chart'
|
||||
import SatisfactionChart from './SatisfactionChart'
|
||||
import { SelectedSimulator, SimulateursChoice } from './SimulateursChoice'
|
||||
import { BigIndicator } from './StatsGlobal'
|
||||
import { Page, PageChapter2, PageSatisfaction, StatsStruct } from './types'
|
||||
import { PageChapter2, StatsStruct } from './types'
|
||||
import { Filter, useStatistiques } from './useStatistiques'
|
||||
import { useTotals } from './useTotals'
|
||||
import { formatDay, formatMonth } from './utils'
|
||||
|
||||
type Period = 'mois' | 'jours'
|
||||
type Chapter2 = PageChapter2 | 'PAM' | 'api-rest'
|
||||
|
||||
type Pageish = Page | PageSatisfaction
|
||||
|
||||
interface StatsDetailProps {
|
||||
stats: StatsStruct
|
||||
|
@ -37,66 +34,15 @@ interface StatsDetailProps {
|
|||
}
|
||||
|
||||
export const StatsDetail = ({ stats, accessibleMode }: StatsDetailProps) => {
|
||||
const defaultPeriod = 'mois'
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
useScrollToHash()
|
||||
|
||||
const [period, setPeriod] = useState<Period>(
|
||||
(searchParams.get('periode') as Period) ?? defaultPeriod
|
||||
)
|
||||
const [chapter2, setChapter2] = useState<Chapter2 | ''>(
|
||||
(searchParams.get('module') as Chapter2) ?? ''
|
||||
)
|
||||
const [{ period, filter }, { setPeriod, setFilter }] = useStatState()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const paramsEntries = [
|
||||
['periode', period !== defaultPeriod ? period : ''],
|
||||
['module', chapter2],
|
||||
].filter(([, val]) => val !== '') as [string, string][]
|
||||
|
||||
setSearchParams(paramsEntries, { replace: true })
|
||||
}, [period, chapter2, setSearchParams])
|
||||
|
||||
const visites = useMemo(() => {
|
||||
const rawData = period === 'jours' ? stats.visitesJours : stats.visitesMois
|
||||
if (!chapter2) {
|
||||
return rawData.site
|
||||
}
|
||||
if (chapter2 === 'api-rest') {
|
||||
return (rawData.api ?? []).map(({ date, ...nombre }) => ({
|
||||
date,
|
||||
nombre,
|
||||
info:
|
||||
(period === 'jours' ? '2023-06-16' : '2023-06-01') === date ? (
|
||||
<ChangeJune2023 />
|
||||
) : null,
|
||||
}))
|
||||
}
|
||||
if (chapter2 === 'guide') {
|
||||
const pages = rawData.pages as Pageish[]
|
||||
const creer = rawData.creer as Pageish[]
|
||||
|
||||
return statsCreer(pages, creer)
|
||||
}
|
||||
|
||||
return filterByChapter2(rawData.pages as Pageish[], chapter2)
|
||||
}, [period, chapter2])
|
||||
|
||||
const repartition = useMemo(() => {
|
||||
const rawData = stats.visitesMois
|
||||
|
||||
return groupByDate(rawData.pages as Pageish[])
|
||||
}, [])
|
||||
|
||||
const satisfaction = useMemo(() => {
|
||||
return filterByChapter2(stats.satisfaction as Pageish[], chapter2)
|
||||
}, [chapter2]) as Array<{
|
||||
date: string
|
||||
nombre: Record<string, number>
|
||||
percent: Record<string, number>
|
||||
}>
|
||||
const { visites, repartition, satisfaction } = useStatistiques({
|
||||
period,
|
||||
stats,
|
||||
filter,
|
||||
})
|
||||
|
||||
const [[startDateIndex, endDateIndex], setDateIndex] = useState<
|
||||
[startIndex: number, endIndex: number]
|
||||
|
@ -111,6 +57,8 @@ export const StatsDetail = ({ stats, accessibleMode }: StatsDetailProps) => {
|
|||
setSlicedVisits(visites)
|
||||
}, [visites])
|
||||
|
||||
const totals = useTotals(slicedVisits)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleDateChange = useCallback(
|
||||
debounce(1000, ({ startIndex, endIndex }) => {
|
||||
|
@ -122,22 +70,12 @@ export const StatsDetail = ({ stats, accessibleMode }: StatsDetailProps) => {
|
|||
[visites]
|
||||
)
|
||||
|
||||
const totals: number | Record<string, number> = useMemo(
|
||||
() => computeTotals(slicedVisits),
|
||||
[slicedVisits]
|
||||
)
|
||||
|
||||
const chapters2: Chapter2[] = [
|
||||
...new Set(stats.visitesMois?.pages.map((p) => p.page_chapter2)),
|
||||
'PAM',
|
||||
]
|
||||
|
||||
type ApiData = {
|
||||
date: string
|
||||
nombre: { evaluate: number; rules: number; rule: number }
|
||||
}
|
||||
const apiCumul =
|
||||
chapter2 === 'api-rest' &&
|
||||
filter === 'api-rest' &&
|
||||
slicedVisits.length > 0 &&
|
||||
typeof slicedVisits[0]?.nombre === 'object' &&
|
||||
(slicedVisits as ApiData[]).reduce(
|
||||
|
@ -159,14 +97,10 @@ export const StatsDetail = ({ stats, accessibleMode }: StatsDetailProps) => {
|
|||
flex-basis: 50%;
|
||||
`}
|
||||
>
|
||||
<SimulateursChoice
|
||||
onChange={setChapter2}
|
||||
value={chapter2}
|
||||
possibleValues={chapters2}
|
||||
/>
|
||||
<SimulateursChoice onChange={setFilter} value={filter} />
|
||||
<Spacing sm />
|
||||
<Grid container columns={4}>
|
||||
{chapter2 && <SelectedSimulator chapter2={chapter2} />}
|
||||
{filter && <SelectedSimulator filter={filter} />}
|
||||
</Grid>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -294,14 +228,14 @@ export const StatsDetail = ({ stats, accessibleMode }: StatsDetailProps) => {
|
|||
<>
|
||||
<H3>Satisfaction</H3>
|
||||
<SatisfactionChart
|
||||
key={chapter2}
|
||||
key={JSON.stringify(filter)}
|
||||
data={satisfaction}
|
||||
accessibleMode={accessibleMode}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{chapter2 === '' && period === 'mois' && (
|
||||
{filter === '' && period === 'mois' && (
|
||||
<>
|
||||
<H2>Simulateurs principaux</H2>
|
||||
<PagesChart data={repartition} accessibleMode={accessibleMode} />
|
||||
|
@ -311,247 +245,29 @@ export const StatsDetail = ({ stats, accessibleMode }: StatsDetailProps) => {
|
|||
)
|
||||
}
|
||||
|
||||
const ChangeJune2023 = () => (
|
||||
<Body style={{ maxWidth: '350px' }}>
|
||||
<Trans i18nKey="stats.change_june_2023">
|
||||
Ajout d'un cache sur l'API pour améliorer les performances et réduire le
|
||||
nombre de requêtes.
|
||||
</Trans>
|
||||
</Body>
|
||||
)
|
||||
|
||||
const isPAM = (name: string | undefined) =>
|
||||
name &&
|
||||
[
|
||||
'medecin',
|
||||
'chirurgien_dentiste',
|
||||
'auxiliaire_medical',
|
||||
'sage_femme',
|
||||
].includes(name)
|
||||
|
||||
const filterByChapter2 = (pages: Pageish[], chapter2: Chapter2 | '') => {
|
||||
return Object.entries(
|
||||
groupBy(
|
||||
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: 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),
|
||||
])
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
const statsCreer = (pages: Pageish[], creer: Pageish[]) => {
|
||||
const accueil = groupBy(
|
||||
pages.filter(
|
||||
(p) =>
|
||||
'page' in p &&
|
||||
p.page === 'accueil' &&
|
||||
p.page_chapter1 === 'creer' &&
|
||||
true
|
||||
),
|
||||
(p) => ('date' in p ? p.date : p.month)
|
||||
)
|
||||
|
||||
const commencee = groupBy(
|
||||
creer.filter(
|
||||
(p) =>
|
||||
'page' in p &&
|
||||
p.page === 'accueil' &&
|
||||
p.page_chapter1 === 'creer' &&
|
||||
true
|
||||
),
|
||||
(p) => ('date' in p ? p.date : p.month)
|
||||
)
|
||||
const terminee = groupBy(
|
||||
creer.filter(
|
||||
(p) =>
|
||||
'page' in p &&
|
||||
p.page !== 'liste' &&
|
||||
p.page_chapter1 === 'creer' &&
|
||||
(p.page_chapter2 as string) === 'statut' &&
|
||||
true
|
||||
),
|
||||
(p) => ('date' in p ? p.date : p.month)
|
||||
)
|
||||
|
||||
return Object.entries(commencee).map(([date, values]) => ({
|
||||
date,
|
||||
nombre: {
|
||||
accueil: accueil[date]?.reduce((acc, p) => acc + p.nombre, 0),
|
||||
simulation_commencee: values.reduce((acc, p) => acc + p.nombre, 0),
|
||||
simulation_terminee: terminee[date]?.reduce(
|
||||
(acc, p) => acc + p.nombre,
|
||||
0
|
||||
),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function groupByDate(data: Pageish[]) {
|
||||
const topTenPageByMonth = Object.entries(
|
||||
groupBy(
|
||||
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(values, (x) => x.page_chapter1 + ' / ' + x.page_chapter2)
|
||||
).map(
|
||||
([k, v]) =>
|
||||
[k, v.map((v) => v.nombre).reduce((a, b) => a + b, 0)] as const
|
||||
)
|
||||
),
|
||||
}))
|
||||
|
||||
const topPagesOfAllTime = Object.entries(
|
||||
topTenPageByMonth.reduce((acc, { nombre }) => {
|
||||
Object.entries(nombre).forEach(([page, visits]) => {
|
||||
acc[page] ??= 0
|
||||
acc[page] += visits
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8)
|
||||
.map(([page]) => page)
|
||||
|
||||
return topTenPageByMonth.map(({ date, nombre }) => ({
|
||||
date,
|
||||
nombre: Object.fromEntries(
|
||||
Object.entries(nombre).filter(([page]) =>
|
||||
topPagesOfAllTime.includes(page)
|
||||
)
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
const computeTotals = (
|
||||
data: Data<number> | Data<Record<string, number>>
|
||||
): number | Record<string, number> => {
|
||||
return isDataStacked(data)
|
||||
? 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)
|
||||
}
|
||||
|
||||
function getChapter2(s: SimulatorData[keyof SimulatorData]): Chapter2 | '' {
|
||||
export function getFilter(s: SimulatorData[keyof SimulatorData]): Filter | '' {
|
||||
if ('iframePath' in s && s.iframePath === 'pamc') {
|
||||
return 'PAM'
|
||||
}
|
||||
if (!s.tracking) {
|
||||
return ''
|
||||
}
|
||||
const tracking = s.tracking as { chapter2?: Chapter2 }
|
||||
const chapter2 =
|
||||
typeof tracking === 'string' ? tracking : tracking.chapter2 ?? ''
|
||||
const tracking = s.tracking as
|
||||
| string
|
||||
| { chapter2?: PageChapter2; chapter3?: string }
|
||||
|
||||
return toAtString(chapter2) as typeof chapter2
|
||||
}
|
||||
|
||||
function SelectedSimulator(props: { chapter2: Chapter2 | '' }) {
|
||||
const simulateur = Object.values(useSimulatorsData()).find(
|
||||
(s) =>
|
||||
getChapter2(s) === props.chapter2 &&
|
||||
!(
|
||||
typeof s.tracking === 'object' &&
|
||||
'chapter3' in s.tracking &&
|
||||
s.tracking.chapter3
|
||||
)
|
||||
)
|
||||
if (!simulateur) {
|
||||
return null
|
||||
const filter =
|
||||
typeof tracking === 'string' ? { chapter2: tracking } : tracking ?? ''
|
||||
if (!filter.chapter2) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return <SimulateurCard small {...simulateur} />
|
||||
}
|
||||
|
||||
function SimulateursChoice(props: {
|
||||
onChange: (ch: Chapter2 | '') => void
|
||||
value: Chapter2 | ''
|
||||
possibleValues: Array<Chapter2>
|
||||
}) {
|
||||
const simulateurs = Object.values(useSimulatorsData())
|
||||
.filter((s) => {
|
||||
const chapter2 = getChapter2(s)
|
||||
|
||||
return (
|
||||
chapter2 &&
|
||||
props.possibleValues.includes(chapter2) &&
|
||||
!(
|
||||
typeof s.tracking === 'object' &&
|
||||
'chapter3' in s.tracking &&
|
||||
s.tracking.chapter3
|
||||
)
|
||||
)
|
||||
})
|
||||
.sort((a, b) => (a.shortName < b.shortName ? -1 : 1))
|
||||
|
||||
return (
|
||||
<Select
|
||||
onSelectionChange={(val) => {
|
||||
props.onChange(
|
||||
typeof val === 'string' && val.length && !isNaN(parseInt(val))
|
||||
? getChapter2(simulateurs[parseInt(val)])
|
||||
: val === 'api-rest'
|
||||
? val
|
||||
: ''
|
||||
)
|
||||
}}
|
||||
defaultSelectedKey={props.value || 'general-stats'}
|
||||
label={'Voir les statistiques pour :'}
|
||||
id="simulator-choice-input"
|
||||
>
|
||||
{[
|
||||
<Item key="general-stats" textValue="Tout le site">
|
||||
<Emoji emoji="🌍" />
|
||||
Tout le site
|
||||
</Item>,
|
||||
<Item key="api-rest" textValue="API REST">
|
||||
<Emoji emoji="👩💻" />
|
||||
API REST
|
||||
</Item>,
|
||||
...simulateurs.map((s, i) => (
|
||||
<Item key={i} textValue={s.shortName}>
|
||||
{s.icône && (
|
||||
<>
|
||||
<Emoji emoji={s.icône} />
|
||||
|
||||
</>
|
||||
)}
|
||||
{s.shortName}
|
||||
</Item>
|
||||
)),
|
||||
]}
|
||||
</Select>
|
||||
)
|
||||
return {
|
||||
chapter2: toAtString(filter.chapter2),
|
||||
...('chapter3' in filter && filter.chapter3
|
||||
? { chapter3: toAtString(filter.chapter3) }
|
||||
: {}),
|
||||
} as Filter
|
||||
}
|
||||
|
||||
const Indicators = styled.div`
|
||||
|
@ -569,3 +285,47 @@ const Indicators = styled.div`
|
|||
const StyledBody = styled(Body)`
|
||||
margin-bottom: 0.25rem;
|
||||
`
|
||||
|
||||
const DEFAULT_PERIOD = 'mois'
|
||||
function useStatState() {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
||||
const [period, setPeriod] = useState<Period>(
|
||||
(searchParams.get('periode') as Period) ?? DEFAULT_PERIOD
|
||||
)
|
||||
const simulators = useSimulatorsData()
|
||||
const URLFilter: string = searchParams.get('module') ?? ''
|
||||
|
||||
const [filter, setFilter] = useState<Filter | ''>(
|
||||
URLFilter in simulators
|
||||
? getFilter(simulators[URLFilter as keyof typeof simulators])
|
||||
: ['PAMC', 'api-rest'].includes(URLFilter)
|
||||
? (URLFilter as Filter)
|
||||
: ''
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const module =
|
||||
Object.values(simulators).find(
|
||||
(s) =>
|
||||
!!filter && JSON.stringify(getFilter(s)) === JSON.stringify(filter)
|
||||
)?.id ?? filter
|
||||
const paramsEntries = [
|
||||
['periode', period !== DEFAULT_PERIOD ? period : ''],
|
||||
['module', module],
|
||||
].filter(([, val]) => val !== '') as [string, string][]
|
||||
|
||||
setSearchParams(paramsEntries, { replace: true })
|
||||
}, [period, filter, simulators, setSearchParams])
|
||||
|
||||
return [
|
||||
{
|
||||
period,
|
||||
filter,
|
||||
},
|
||||
{
|
||||
setPeriod,
|
||||
setFilter,
|
||||
},
|
||||
] as const
|
||||
}
|
||||
|
|
|
@ -71,4 +71,5 @@ export enum PageChapter2 {
|
|||
Independant = 'independant',
|
||||
ProfessionLiberale = 'profession_liberale',
|
||||
Salarie = 'salarie',
|
||||
ChoixDuStatut = 'choix_du_statut',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
import { useMemo } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
import { Body } from '@/design-system/typography/paragraphs'
|
||||
import { groupBy } from '@/utils'
|
||||
|
||||
import { Page, PageChapter2, PageSatisfaction, StatsStruct } from './types'
|
||||
|
||||
type Pageish = Page | PageSatisfaction
|
||||
|
||||
export type Filter =
|
||||
| { chapter2: PageChapter2; chapter3?: string }
|
||||
| 'PAM'
|
||||
| 'api-rest'
|
||||
|
||||
export function useStatistiques({
|
||||
period,
|
||||
stats,
|
||||
filter,
|
||||
}: {
|
||||
period: 'mois' | 'jours'
|
||||
stats: StatsStruct
|
||||
filter: Filter | ''
|
||||
}) {
|
||||
const visites = useMemo(() => {
|
||||
const rawData = period === 'jours' ? stats.visitesJours : stats.visitesMois
|
||||
if (!filter) {
|
||||
return rawData.site
|
||||
}
|
||||
if (filter === 'api-rest') {
|
||||
return (rawData.api ?? []).map(({ date, ...nombre }) => ({
|
||||
date,
|
||||
nombre,
|
||||
info:
|
||||
(period === 'jours' ? '2023-06-16' : '2023-06-01') === date ? (
|
||||
<ChangeJune2023 />
|
||||
) : null,
|
||||
}))
|
||||
}
|
||||
if (typeof filter !== 'string' && filter.chapter2 === 'choix_du_statut') {
|
||||
const pages = rawData.pages as Pageish[]
|
||||
|
||||
return statsChoixStatut(pages)
|
||||
}
|
||||
|
||||
return filterPage(rawData.pages as Pageish[], filter)
|
||||
}, [period, filter])
|
||||
|
||||
const repartition = useMemo(() => {
|
||||
const rawData = stats.visitesMois
|
||||
|
||||
return groupByDate(rawData.pages as Pageish[])
|
||||
}, [])
|
||||
|
||||
const satisfaction = useMemo(() => {
|
||||
if (filter === 'api-rest') {
|
||||
return []
|
||||
}
|
||||
|
||||
return filterPage(stats.satisfaction as Pageish[], filter)
|
||||
}, [filter]) as Array<{
|
||||
date: string
|
||||
nombre: Record<string, number>
|
||||
percent: Record<string, number>
|
||||
}>
|
||||
|
||||
return {
|
||||
visites,
|
||||
repartition,
|
||||
satisfaction,
|
||||
}
|
||||
}
|
||||
|
||||
const ChangeJune2023 = () => (
|
||||
<Body style={{ maxWidth: '350px' }}>
|
||||
<Trans i18nKey="stats.change_june_2023">
|
||||
Ajout d'un cache sur l'API pour améliorer les performances et réduire le
|
||||
nombre de requêtes.
|
||||
</Trans>
|
||||
</Body>
|
||||
)
|
||||
|
||||
const isPAM = (name: string | undefined) =>
|
||||
name &&
|
||||
[
|
||||
'medecin',
|
||||
'chirurgien_dentiste',
|
||||
'auxiliaire_medical',
|
||||
'sage_femme',
|
||||
].includes(name)
|
||||
|
||||
function filterPage(
|
||||
pages: Pageish[],
|
||||
filter: Exclude<Filter, 'api-rest'> | ''
|
||||
) {
|
||||
return Object.entries(
|
||||
groupBy(
|
||||
pages.filter(
|
||||
(p) =>
|
||||
(!('page' in p) || p.page !== 'accueil_pamc') &&
|
||||
(!filter
|
||||
? true
|
||||
: filter === 'PAM'
|
||||
? isPAM(p.page_chapter3)
|
||||
: filter.chapter2 === p.page_chapter2 &&
|
||||
(!filter.chapter3 || filter.chapter3 === p.page_chapter3))
|
||||
),
|
||||
(p) => ('date' in p ? p.date : p.month)
|
||||
)
|
||||
).map(([date, values]) => ({
|
||||
date,
|
||||
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),
|
||||
])
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
const statsChoixStatut = (pages: Pageish[]) => {
|
||||
const choixStatutPage = pages.filter(
|
||||
(p) => p.page_chapter2 === 'choix_du_statut'
|
||||
)
|
||||
const accueil = groupBy(
|
||||
choixStatutPage.filter((p) => 'page' in p && p.page === 'accueil'),
|
||||
(p) => ('date' in p ? p.date : p.month)
|
||||
)
|
||||
const commencee = groupBy(
|
||||
pages.filter(
|
||||
(p) =>
|
||||
p.page_chapter3 === 'pas_a_pas' &&
|
||||
'page' in p &&
|
||||
p.page === 'recherche_activite'
|
||||
),
|
||||
(p) => ('date' in p ? p.date : p.month)
|
||||
)
|
||||
const terminee = groupBy(
|
||||
pages.filter((p) => p.page_chapter3 === 'resultat'),
|
||||
(p) => ('date' in p ? p.date : p.month)
|
||||
)
|
||||
|
||||
return Object.entries(commencee).map(([date, values]) => ({
|
||||
date,
|
||||
nombre: {
|
||||
accueil: accueil[date]?.reduce((acc, p) => acc + p.nombre, 0),
|
||||
simulation_commencee: values.reduce((acc, p) => acc + p.nombre, 0),
|
||||
simulation_terminee: terminee[date]?.reduce(
|
||||
(acc, p) => acc + p.nombre,
|
||||
0
|
||||
),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export type Visites = ReturnType<typeof useStatistiques>['visites']
|
||||
|
||||
function groupByDate(data: Pageish[]) {
|
||||
const topTenPageByMonth = Object.entries(
|
||||
groupBy(
|
||||
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(values, (x) => x.page_chapter1 + ' / ' + x.page_chapter2)
|
||||
).map(
|
||||
([k, v]) =>
|
||||
[k, v.map((v) => v.nombre).reduce((a, b) => a + b, 0)] as const
|
||||
)
|
||||
),
|
||||
}))
|
||||
|
||||
const topPagesOfAllTime = Object.entries(
|
||||
topTenPageByMonth.reduce((acc, { nombre }) => {
|
||||
Object.entries(nombre).forEach(([page, visits]) => {
|
||||
acc[page] ??= 0
|
||||
acc[page] += visits
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8)
|
||||
.map(([page]) => page)
|
||||
|
||||
return topTenPageByMonth.map(({ date, nombre }) => ({
|
||||
date,
|
||||
nombre: Object.fromEntries(
|
||||
Object.entries(nombre).filter(([page]) =>
|
||||
topPagesOfAllTime.includes(page)
|
||||
)
|
||||
),
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { useMemo } from 'react'
|
||||
|
||||
import { Data, isDataStacked } from './Chart'
|
||||
import { Visites } from './useStatistiques'
|
||||
|
||||
export function useTotals(visits: Visites): number | Record<string, number> {
|
||||
return useMemo(() => computeTotals(visits), [visits])
|
||||
}
|
||||
|
||||
const computeTotals = (
|
||||
data: Data<number> | Data<Record<string, number>>
|
||||
): number | Record<string, number> => {
|
||||
return isDataStacked(data)
|
||||
? 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)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue