Ajoute la selection du guichet unique sur l'assistant au choix du statut

pull/2782/head
Johan Girod 2023-04-24 17:54:20 +02:00
parent 8883ae6b41
commit fb57b99daa
8 changed files with 246 additions and 152 deletions

View File

@ -314,10 +314,13 @@ entreprise . activités . principale:
titre: Activité principale
avec:
code APE:
titre: Code APE
description: |
Le code APE est un code qui permet d'identifier l'activité principale de l'entreprise.
Il est composé de 5 chiffres.
code guichet:
description: |
Le code guichet est utilisé pour identifier l'activité principale lors de la déclaration de
la nouvelle entreprise via le guichet unique
# une possibilité:
# choix obligatoire: oui

View File

@ -2,7 +2,13 @@ import { useEffect, useState } from 'react'
import * as safeLocalStorage from '../../storage/safeLocalStorage'
type Storage = Record<string, unknown> | boolean | number | null
type Storage =
| Record<string, unknown>
| Array<unknown>
| boolean
| number
| null
| string
export const getInitialState = <T extends Storage>(key: string): T | null => {
const value = safeLocalStorage.getItem(key)

View File

@ -47,7 +47,7 @@ export default function Navigation({
onNextStep,
}: {
currentStepIsComplete: boolean
nextStepLabel?: string
nextStepLabel?: false | string
onNextStep?: () => void
}) {
const { t } = useTranslation()
@ -84,7 +84,7 @@ export default function Navigation({
isDisabled={!currentStepIsComplete}
aria-label={t("Suivant, passer à l'étape suivante")}
>
{nextStepLabel || <Trans>Suivant</Trans>}{' '}
{nextStepLabel || <Trans>Enregistrer et passer à la suite</Trans>}{' '}
<span aria-hidden></span>
</Button>
</Grid>

View File

@ -1,62 +1,79 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { Navigate } from 'react-router-dom'
import { Message } from '@/design-system'
import { HelpButtonWithPopover } from '@/design-system/buttons'
import Skeleton from '@/components/ui/Skeleton'
import { useEngine } from '@/components/utils/EngineContext'
import { Message, RadioCardGroup } from '@/design-system'
import { StyledRadioSkeleton } from '@/design-system/field/Radio/RadioCard'
import { Spacing } from '@/design-system/layout'
import { H3, H5 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { useSitePaths } from '@/sitePaths'
import { updateSituation } from '@/store/actions/actions'
import {
GuichetDescription,
GuichetEntry,
getGuichetTitle,
useGuichetInfo,
} from '../recherche-code-ape/GuichetInfo'
import Layout from './_components/Layout'
import Navigation from './_components/Navigation'
export default function Activité() {
const [codeApe, setCodeApe] = useState('')
const { t } = useTranslation()
const codeApe = useEngine().evaluate(
'entreprise . activités . principale . code APE'
).nodeValue as string | undefined
const dispatch = useDispatch()
const [codeGuichet, setCodeGuichet] = useState<string | undefined>(undefined)
const guichetEntries = useGuichetInfo(codeApe)
useEffect(() => {
if (guichetEntries && guichetEntries.length === 1)
setCodeGuichet(guichetEntries[0].code)
}, [guichetEntries])
if (!codeApe) return <CodeAPENonConnu />
return (
<>
<Layout title={t('créer.activité.title', 'Votre activité')}>
<Trans i18nKey={'créer.activité.subtitle'}>
<H3 as="h2">
Mon activité principale est...
<HelpButtonWithPopover
title={t(
'créer.activité.help.title',
'Le choix du statut, un choix adapté à votre situation'
)}
type="info"
>
<Body>
Le choix du statut et les cotisations diffèrent en fonction de
l'activité professionnelle que vous exercez. Renseigner votre
métier vous donnera de la visibilité sur les statuts possibles
et permettra de simuler vos revenus de manière plus précise.
</Body>
<Message type="secondary" border={false}>
<H5 as="h3">Vous cumulez plusieurs activités ?</H5>
<Body>
Votre entreprise doit tout de même déclarer une activité
principale à l'administration. Pour savoir comment la
déterminer, <Link>voir ce guide</Link>.
</Body>
</Message>
</HelpButtonWithPopover>
</H3>
<Trans i18nKey={'créer.activité-détails.subtitle'}>
<H3 as="h2">Précisions sur votre activité</H3>
</Trans>
{!guichetEntries ? (
<GuichetSkeleton />
) : guichetEntries.length === 1 ? (
<>
<Message border={false}>
<H5 as="h3">{getGuichetTitle(guichetEntries[0].label)}</H5>
<GuichetDescription {...guichetEntries[0]} />
</Message>
</>
) : (
<GuichetSelection
entries={guichetEntries}
onGuichetSelected={setCodeGuichet}
/>
)}
<Navigation
currentStepIsComplete={!!codeApe}
nextStepLabel={t(
'créer.activité.next',
'Selectionner cette activité'
)}
currentStepIsComplete={!!codeGuichet}
nextStepLabel={
guichetEntries?.length === 1 &&
t('créer.activité-détails.next1', 'Continuer avec cette activité')
}
onNextStep={() =>
codeGuichet &&
dispatch(
updateSituation(
'entreprise . activités . principale . code APE',
codeApe
'entreprise . activités . principale . code guichet',
`'${codeGuichet}'`
)
)
}
@ -65,3 +82,57 @@ export default function Activité() {
</>
)
}
function CodeAPENonConnu() {
const { absoluteSitePaths } = useSitePaths()
return (
// For now, we don't handle the case where the user doesn't find his code APE
<Navigate
to={absoluteSitePaths.assistants['choix-du-statut']['recherche-activité']}
replace
/>
)
}
function GuichetSelection({
entries,
onGuichetSelected,
codeGuichet,
}: {
entries: GuichetEntry[]
onGuichetSelected: (code: string) => void
codeGuichet?: string
}) {
return (
<>
<Body>Sectionnez la description d'activité qui correspond le mieux.</Body>
<RadioCardGroup value={codeGuichet} onChange={onGuichetSelected}>
{entries.map((guichetEntry) => {
return (
<StyledRadioSkeleton
value={guichetEntry.code}
key={guichetEntry.code}
visibleRadioAs="div"
>
<H5 as="h3">{getGuichetTitle(guichetEntry.label)}</H5>
<GuichetDescription {...guichetEntry} />
</StyledRadioSkeleton>
)
})}
</RadioCardGroup>
</>
)
}
function GuichetSkeleton() {
return (
<Message border={false}>
<Body>
<Skeleton width={300} height={20} />
<Spacing md />
<Skeleton width={600} height={20} />
</Body>
</Message>
)
}

View File

@ -55,15 +55,11 @@ export default function Activité() {
<SearchCodeAPE hideGuichetUnique onCodeAPESelected={setCodeApe} />
<Navigation
currentStepIsComplete={!!codeApe}
nextStepLabel={t(
'créer.activité.next',
'Selectionner cette activité'
)}
onNextStep={() =>
dispatch(
updateSituation(
'entreprise . activités . principale . code APE',
codeApe
`'${codeApe}'`
)
)
}

View File

@ -4,75 +4,92 @@ import { H5 } from '@/design-system/typography/heading'
import { Li, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
import { useAsyncData } from '@/hooks/useAsyncData'
import { capitalise0 } from '@/utils'
const lazyApeToGuichet = () => import('@/public/data/ape-to-guichet.json')
type ApeToGuichet = typeof import('@/public/data/ape-to-guichet.json')
const lazyGuichet = () => import('@/public/data/guichet.json')
type Guichet = typeof import('@/public/data/guichet.json')
export default function GuichetInfo({ apeCode }: { apeCode: string }) {
export type GuichetEntry = Guichet[keyof Guichet]
export function useGuichetInfo(codeApe?: string): GuichetEntry[] | null {
const guichet = useAsyncData(lazyGuichet)?.default
const apeToGuichet = useAsyncData(lazyApeToGuichet)?.default
if (!guichet || !apeToGuichet || !(apeCode in apeToGuichet)) {
if (!codeApe || !guichet || !apeToGuichet || !(codeApe in apeToGuichet)) {
return null
}
const guichetEntries = apeToGuichet[apeCode as keyof ApeToGuichet].map(
return apeToGuichet[codeApe as keyof ApeToGuichet].map(
(code) => guichet[code as keyof Guichet]
)
}
export function getGuichetTitle(label: GuichetEntry['label']) {
return capitalise0(
[
(!label.niv3 || label.niv3 === 'Autre') && label.niv2,
(!label.niv4 || label.niv4 === 'Autre') && label.niv3,
label.niv4,
]
.filter(Boolean)
.join(' - ')
)
}
export default function GuichetInfo({ codeApe }: { codeApe: string }) {
const guichetEntries = useGuichetInfo(codeApe)
if (!guichetEntries) {
return null
}
return (
<Ul noMarker>
{guichetEntries.map(
({
label,
caisseDeRetraiteSpéciale,
typeBénéfice,
artisteAuteurPossible,
catégorieActivité,
code,
}) => {
return (
<Li key={code}>
<Message border={false}>
<H5>
{[
(!label.niv3 || label.niv3 === 'Autre') && label.niv2,
(!label.niv4 || label.niv4 === 'Autre') && label.niv3,
label.niv4,
]
.filter(Boolean)
.join(' - ')}
<Chip type="secondary">{code}</Chip>
</H5>
<Body>
Activité{' '}
<Strong>{catégorieActivité.replace(/_/g, ' ')}</Strong> avec
des revenus déclarés en <Strong>{typeBénéfice}</Strong>
{caisseDeRetraiteSpéciale && (
<>
, affiliée à la{' '}
<Strong>{caisseDeRetraiteSpéciale}</Strong> pour la
retraite
</>
)}
.
</Body>
<Body>
{artisteAuteurPossible && (
<>
Possibilitée d'exercer en tant qu'
<Strong>ARTISTE AUTEUR</Strong>
</>
)}
</Body>
{/* </Ul> */}
</Message>
</Li>
)
}
)}
{guichetEntries.map((guichetEntry) => {
return (
<Li key={guichetEntry.code}>
<Message border={false}>
<H5>
{getGuichetTitle(guichetEntry.label)}{' '}
<Chip type="secondary">{guichetEntry.code}</Chip>
</H5>
<GuichetDescription {...guichetEntry} />
</Message>
</Li>
)
})}
</Ul>
)
}
export function GuichetDescription({
caisseDeRetraiteSpéciale,
typeBénéfice,
artisteAuteurPossible,
catégorieActivité,
}: GuichetEntry) {
return (
<>
<Body>
Activité <Strong>{catégorieActivité.replace(/_/g, ' ')}</Strong> avec
des revenus déclarés en <Strong>{typeBénéfice}</Strong>
{caisseDeRetraiteSpéciale && (
<>
, affiliée à la <Strong>{caisseDeRetraiteSpéciale}</Strong> pour la
retraite
</>
)}
.
</Body>
<Body>
{artisteAuteurPossible && (
<>
Possibilitée d'exercer en tant qu'
<Strong>ARTISTE AUTEUR</Strong>
</>
)}
</Body>
</>
)
}

View File

@ -5,8 +5,6 @@ import styled, { css } from 'styled-components'
import { Appear } from '@/components/ui/animate'
import { Button, HelpButtonWithPopover } from '@/design-system/buttons'
import { ChevronIcon } from '@/design-system/icons'
import InfoBulle from '@/design-system/InfoBulle'
import { Grid } from '@/design-system/layout'
import { Strong } from '@/design-system/typography'
import { H4, H5 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
@ -27,21 +25,14 @@ interface ResultProps {
hideGuichetUnique: boolean
}
export const Result = ({ item, debug, hideGuichetUnique }: ResultProps) => {
export const Result = ({ item, hideGuichetUnique }: ResultProps) => {
const { title, codeApe, contenuCentral, contenuAnnexe, contenuExclu } = item
const [open, setOpen] = useState(false)
const { t } = useTranslation()
return (
<>
<H4 as="h3">
{title}
{debug && (
<InfoBulle>
<pre>{debug}</pre>
</InfoBulle>
)}
</H4>
<H4 as="h3">{title}</H4>
<Body
css={`
display: flex;
@ -49,7 +40,7 @@ export const Result = ({ item, debug, hideGuichetUnique }: ResultProps) => {
align-items: center;
`}
>
<Strong>Code : {codeApe}</Strong>
<Strong>Code APE : {codeApe}</Strong>
<Button
size="XXS"
light
@ -102,27 +93,10 @@ export const Result = ({ item, debug, hideGuichetUnique }: ResultProps) => {
<Trans i18nKey={'codeApe.catégorie-guichet'}>
<H4>
Catégories du Guichet unique
<HelpButtonWithPopover
type="info"
title="Qu'est-ce que le guichet unique ?"
>
<Body>
Le{' '}
<Link href="https://procedures.inpi.fr/">
Guichet électronique des formalités dentreprises
</Link>{' '}
(Guichet unique) est un portail internet sécurisé, auprès
duquel toute entreprise est tenue de déclarer sa création,
depuis le 1er janvier 2023.
</Body>
<Body>
Il utilise une classification des activités différente de
celle utilisée par l'INSEE pour code APE.
</Body>
</HelpButtonWithPopover>
<HelpGuichetUnique />
</H4>
</Trans>
<GuichetInfo apeCode={codeApe} />
<GuichetInfo codeApe={codeApe} />
</>
)}
</Appear>
@ -131,12 +105,6 @@ export const Result = ({ item, debug, hideGuichetUnique }: ResultProps) => {
)
}
const StyledGrid = styled(Grid)`
display: flex;
justify-content: end;
align-items: center;
`
const StyledChevron = styled(ChevronIcon)<{ $isOpen: boolean }>`
vertical-align: middle;
transform: rotate(-90deg);
@ -147,3 +115,26 @@ const StyledChevron = styled(ChevronIcon)<{ $isOpen: boolean }>`
transform: rotate(90deg);
`}
`
export function HelpGuichetUnique() {
return (
<HelpButtonWithPopover
type="info"
title="Qu'est-ce que le guichet unique ?"
>
<Body>
Le{' '}
<Link href="https://procedures.inpi.fr/">
Guichet électronique des formalités dentreprises
</Link>{' '}
(Guichet unique) est un portail internet sécurisé, auprès duquel toute
entreprise est tenue de déclarer sa création, depuis le 1er janvier
2023.
</Body>
<Body>
Il utilise une classification des activités différente de celle utilisée
par l'INSEE pour code APE.
</Body>
</HelpButtonWithPopover>
)
}

View File

@ -6,6 +6,7 @@ import styled from 'styled-components'
import { TrackPage } from '@/components/ATInternetTracking'
import FeedbackForm from '@/components/Feedback/FeedbackForm'
import { FromTop } from '@/components/ui/animate'
import { usePersistingState } from '@/components/utils/persistState'
import {
Message,
PopoverWithTrigger,
@ -91,14 +92,23 @@ export default function SearchCodeAPE({
onCodeAPESelected,
}: SearchCodeApeProps) {
const { t } = useTranslation()
const [job, setJob] = useState('')
const [selected, setSelected] = useState('')
const [list, setList] = useState<ListResult[]>([])
const [searchQuery, setSearchQuery] = usePersistingState<string>(
'codeAPE:search',
''
)
const [selected, setSelected] = usePersistingState<string>(
'codeAPE:selected',
''
)
const [list, setList] = usePersistingState<ListResult[]>(
'codeAPE:results',
[]
)
const lazyData = useAsyncData(() => import('@/public/data/ape-search.json'))
const lastIdxs = useRef<Record<string, UFuzzy.HaystackIdxs>>({})
const prevValue = useRef<string>(job)
const prevValue = useRef<string>(searchQuery)
const buildedResearch = useMemo(() => buildResearch(lazyData), [lazyData])
@ -109,10 +119,10 @@ export default function SearchCodeAPE({
const { apeData } = lazyData
const { fuzzy, genericList, specificList } = buildedResearch
if (!job.length) {
if (!searchQuery.length) {
lastIdxs.current = {}
setList([])
prevValue.current = job
prevValue.current = searchQuery
return
}
@ -129,10 +139,10 @@ export default function SearchCodeAPE({
return { idxs, info, order }
}
const latinizedValue = UFuzzy.latinize([job])[0]
const latinizedValue = UFuzzy.latinize([searchQuery])[0]
const specific = search(specificList.original, job)
const generic = search(genericList.original, job)
const specific = search(specificList.original, searchQuery)
const generic = search(genericList.original, searchQuery)
const specificLatin = search(specificList.latinized, latinizedValue)
const genericLatin = search(genericList.latinized, latinizedValue)
@ -178,8 +188,8 @@ export default function SearchCodeAPE({
.sort(({ score: a }, { score: b }) => a - b)
setList(results)
prevValue.current = job
}, [buildedResearch, job, lazyData])
prevValue.current = searchQuery
}, [buildedResearch, searchQuery, lazyData])
type Alt = { match: string; proposal: string[] }
const [alternative, setAlternative] = useState<Alt | null>(null)
@ -193,11 +203,11 @@ export default function SearchCodeAPE({
setAlternative(null)
alternatives.forEach((alt) => {
if (new RegExp(alt.match, 'i').test(job)) {
if (new RegExp(alt.match, 'i').test(searchQuery)) {
setAlternative(alt)
}
})
}, [job])
}, [searchQuery])
useEffect(() => {
if (onCodeAPESelected) {
@ -208,8 +218,8 @@ export default function SearchCodeAPE({
const ret = (
<>
<SearchField
value={job}
onChange={setJob}
value={searchQuery}
onChange={setSearchQuery}
label={t("Mots-clés définissants l'activité")}
placeholder={t('Par exemple : coiffure, boulangerie ou restauration')}
/>