Rework du comparateur de statut (#2442)

* Ajoute les accidents du travail et les congés maternité paternité au comparateur de statuts

* Ajoute des références sur le calcul du la retraite

* Corrige les calculs des droits maladie maternité paternité adoption

* Ajoute les pensions d'invalidité

* Ajoute le capital décès

* Ajoute les rentes ATMP et la pension de reversion

* Met à jour le Changelog, les traductions, et tutti quanti

* feat: Redécoupage fichiers

* feat: Ajoute le mode tab à ToggleGroup

* feat: Ajoute mode fullWidth à Simulation

* feat: Ajoute une description + lien vers doc

* feat: Modifie bouton modifier

* feat: Ajoute focusstyle

* feat: Ajoute l'icon edit

* feat: Ajoute composants à Comparateur

* feat: Add StatusCard and icons

* feat: Continue implémentation StatusCard

* feat: Ajoute icônes, checklist

* feat: Ajoute checklist dans les cards de statut

* feat: retire id

* feat: Ajoute Détails et retraite santé

* feat: Ajoute Accident du t

* feat: Ajoute les parties manquantes

* feat: Améliore liste, retire erreur ts

* fix: corrige mise en page

* feat: Ajoute le contenu du modal aller plus loin

* feat: Retire underline en mode light

* feat: Modifie couleur questions, applique modif partie bleue sur tous les simu

* feat: Améliore responsiveness

* feat: Ajoute impots

* feat: Peu de progrès..

* fix small

* feat: Implémentation nouveaux droits

* feat: Aller plus loin fonctionne

* feat: Prise en compte des paramètres allerplus loin

* feat: Ajoute DetailsRowCard

* feat: Début remplaçage

* feat: Passe les engine aux when + cleaning

* feat: Commente tester éligibilité

* feat: Ajoute Tooltip + warning icon

* feat: Ajoute seuils micro + tooltip

* feat: Creation WarningTooltip

* feat: Corrections + ajouter warning sasu

* feat: Ajoute déplier/plier

* rebase

* fix: Type errors

* fix: Open/Close all accordion

* fix: E2E tests

* feat: cache certains label si la règle n'est pas applicable

* feat: Dynamic best option

* refacto

* Ajoute trad à la mano

* feat: Ajoute chevron déplier + modifie structure aller plus loin

* feat: Corrige les copy/paste + ajoute tooltip

* fix: Best option tooltips

* improve tooltip id logic

* fix: StatusCard tooltip id

* feat: Réimplémentation du tableau aller plus loin

* feat: Ajoute toggle ACRE

* feat: Ajoute système sauvegarde d'état d'ouverture

* feat: Ajoute un timeout sur le scrollto

* wtf

* feat: Bestoption Revenuapresimpot

* feat Ajoute Drawer

* fix: Gestion état champ acre

* fix: Scroll issue on confirm

* fix: Repare clickoutside + améliore doc

* feat: Ajoute label

* feat: Affine ACRE + fix liens doc

* fix: Save accordion state + disable it

* feat: Ajoute CasParticuliers context

* fix

* feat: Affiche la documentation dans une modale

* chore: Rebase + fix button color

* feat: Ajoute valeur default force theme

* fix: Mobile style

* feat: Améliorations style mobile

* feat: Amélioration darkmode comparateur

* feat: Améliore accessibilité + fix doc passée ei ae

* chore: clean

* fix: unique id a11y error

* fix: Fix divers

* fix: Test e2e

* fix: Dernier fix

* fix: Espacement tag en vue mobile

Co-authored-by: Johan Girod <johan.girod@beta.gouv.fr>
pull/2477/head
Benjamin Arias 2023-01-26 14:48:00 +01:00 committed by GitHub
parent dfcfabf214
commit 0d0491bd17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 4829 additions and 567 deletions

View File

@ -1,5 +1,39 @@
# Journal des modifications
## 1.5.0
Ajoute les droits ouverts à la protection sociale pour les régimes suivants :
- indépendants AC/PLNR
- auto-entrepreneur hors CIPAV
- assimilé salarié
Les droits suivants ont été implémentés :
- Indemnités journalières et délai dattente en cad darrêt maladie
- Indemnités journalières pour les accidents du travail et maladie professionnelle
- Indemnités journalières et forfaitaire pour les congés maternité paternité adoption
- Rentes, capital décès, pension de reversion et dinvalidité
### Détails
Ajout des règles suivantes :
- dirigeant . indépendant . cotisations et contributions . invalidité et décès
- protection sociale . maladie . raam
- protection sociale . maladie . maternité paternité adoption . *
- protection sociale . maladie . arrêt maladie . *
- protection sociale . invalidité et décès . *
Renomme les règles suivantes :
- protection sociale . retraite . base . cotisée . revenu salarié -> protection sociale . retraite . base . cotisée . salarié
- protection sociale . retraite . base . cotisée . revenu indépendant -> protection sociale . retraite . base . cotisée . indépendant
- protection sociale . accidents du travail et maladies professionnelles -> protection sociale . maladie . accidents du travail et maladies professionnelles
Supression des règles suivantes :
- protection sociale . maladie . ATMP
*Note : lespace de nom `protection social` étant taggué comme « experimental », ces changements cassants ne provoquent pas de montée de version majeure.
## 1.4.2
- Augmentation du plafond de taux réduit pour l'impôt sur les sociétés (merci @fmata)

View File

@ -1304,6 +1304,7 @@ dirigeant . indépendant . cotisations et contributions . retraite complémentai
dirigeant . indépendant . cotisations et contributions . invalidité et décès:
produit:
assiette:
nom: assiette
valeur: assiette des cotisations
plancher: assiette minimale . retraite
plafond: plafond sécurité sociale

View File

@ -59,6 +59,11 @@ entreprise . chiffre d'affaires:
identifiant court: CA
résumé: Montant total des recettes brutes (hors taxe)
unité: €/an
description: |
### Chiffre d'affaires estimé
Le chiffre d'affaires est la <strong>somme des montants des ventes réalisées
pendant votre exercice comptable</strong> (un an) : CA = prix de vente x quantités
vendues.
variations:
- si: dirigeant . auto-entrepreneur
alors: dirigeant . auto-entrepreneur . chiffre d'affaires

View File

@ -288,10 +288,21 @@ entreprise . imposition . régime . micro-entreprise . revenu abattu:
entreprise . imposition . régime . micro-entreprise . alerte seuil dépassés:
type: notification
sévérité: avertissement
formule: chiffre d'affaires . seuil micro dépassé
formule: chiffre d'affaires . seuil micro . dépassé
description: Le seuil annuel de chiffre d'affaires pour le régime micro-fiscal est dépassé. [En savoir plus](/documentation/entreprise/chiffre-d'affaires/seuil-micro-dépassé)
entreprise . chiffre d'affaires . seuil micro dépassé:
entreprise . chiffre d'affaires . seuil micro:
experimental: oui
entreprise . chiffre d'affaires . seuil micro . libérale:
unité: €/an
valeur: 72600 €/an
entreprise . chiffre d'affaires . seuil micro . total:
unité: €/an
valeur: 176200 €/an
entreprise . chiffre d'affaires . seuil micro . dépassé:
experimental: oui
applicable si: imposition . IR
description: |
@ -330,8 +341,8 @@ entreprise . chiffre d'affaires . seuil micro dépassé:
# économie collaborative). Il faudrait référencer la même valeur partout où
# elle est utilisée.
une de ces conditions:
- entreprise . chiffre d'affaires > 176200 €/an
- entreprise . chiffre d'affaires . service > 72600 €/an
- entreprise . chiffre d'affaires > total
- entreprise . chiffre d'affaires . service > service
entreprise . imposition . régime . déclaration contrôlée:
applicable si: IR . type de bénéfices . BNC

View File

@ -29,7 +29,7 @@ protection sociale . retraite:
références:
Panorama des régimes de retraites: https://travail-emploi.gouv.fr/retraite/le-systeme-de-retraite-actuel/
'Retraites de base et complémentaire dans le privé : quelles différences ?': https://www.service-public.fr/particuliers/vosdroits/F12389
'Régime de retraite des travailleurs indépendants': 'https://entreprendre.service-public.fr/vosdroits/F33841'
protection sociale . retraite . trimestres:
titre: trimestres validés
@ -58,26 +58,32 @@ protection sociale . retraite . base:
Le montant de votre pension pour la retraite de base est calculé à partir la moyenne de vos revenus des 25 meilleures années.
Cet estimation de votre pension de retraite est calculée en se basant sur les principes suivants :
- La rémunération calculée correspond à celle de vos 25 meilleures années
- La rémunération calculée dans le simulateur correspond à celle de vos 25 meilleures années
- Vous avez cotisé suffisement de trimestres et vous partez à l'âge requis pour bénéficier du taux plein
arrondi: oui
unité: €/mois
produit:
assiette: base . cotisée
taux: 50%
références:
'Retraites de base et complémentaire dans le privé : quelles différences ?': https://www.service-public.fr/particuliers/vosdroits/F12389
'Assurance Retraite de la Sécurité sociale': https://www.lassuranceretraite.fr/
'Calcul de la retraite du salarié du secteur privé': 'https://www.service-public.fr/particuliers/vosdroits/F21552'
'Régime de retraite des travailleurs indépendants': 'https://entreprendre.service-public.fr/vosdroits/F33841'
protection sociale . retraite . base . cotisée:
titre: revenu cotisés pour la retraite de base
unité: €/mois
arrondi: oui
variations:
- si: dirigeant . indépendant
alors: revenu indépendant
alors: indépendant
- si: dirigeant . auto-entrepreneur
alors: revenu auto-entrepreneur
- sinon: revenu salarié
- sinon: salarié
plafond: plafond sécurité sociale
avec:
revenu salarié:
salarié:
titre: revenu salarié
valeur: salarié . cotisations . vieillesse . salarié / (salarié . cotisations . vieillesse . salarié . plafonnée . taux + salarié . cotisations . vieillesse . salarié . déplafonnée . taux)
références:
Article R351-9 du Code de la sécurité sociale: https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000028751530/2014-03-21
@ -88,7 +94,8 @@ protection sociale . retraite . base . cotisée:
avec:
entreprise . imposition . régime . micro-entreprise . revenu abattu . plancher abattement: non
revenu indépendant:
indépendant:
titre: revenu indépendant
valeur: dirigeant . indépendant . cotisations et contributions . retraite de base / dirigeant . indépendant . cotisations et contributions . retraite de base . taux
références:
Article R351-9 du code de la sécurité sociale: https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000028751530/2014-03-21
@ -366,6 +373,22 @@ protection sociale . maladie:
Ce qui est remboursé pour tout le monde: https://www.ameli.fr/assure/remboursements/rembourse
Rapport d'activité de l'assurance maladie 2017 (PDF): https://assurance-maladie.ameli.fr/sites/default/files/ra-2017_agir-ensemble-proteger-chacun.pdf
Rapport OCDE sur l'esperance de vie dans les différents pays: https://read.oecd-ilibrary.org/social-issues-migration-health/health-at-a-glance-europe-2018_health_glance_eur-2018-en#page89
avec:
'[privé] plancher indemnités salarié': 1015 heure * SMIC . horaire
'[privé] abattement forfaitaire salarié': 21%
'raam':
titre: Revenu dactivité annuel moyen
valeur:
variations:
- si: dirigeant . indépendant
alors: dirigeant . indépendant . cotisations et contributions . indemnités journalières maladie . assiette
- si: dirigeant . auto-entrepreneur
alors: dirigeant . auto-entrepreneur . impôt . revenu imposable
plafond:
variations:
- si: entreprise . activité . nature . libérale . réglementée
alors: 3 * plafond sécurité sociale
- sinon: plafond sécurité sociale
protection sociale . maladie . arrêt maladie:
titre:
@ -390,45 +413,41 @@ protection sociale . maladie . arrêt maladie:
non applicable si: arrêt maladie = 0
protection sociale . maladie . arrêt maladie . salarié:
références:
'Arrêt de travail pour maladie : les indemnités journalières du salarié': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/arret-maladie-salarie
'Arrêt maladie : indemnités journalières versées au salarié': https://www.service-public.fr/particuliers/vosdroits/F3053
non applicable si:
une de ces conditions:
- dirigeant . indépendant
- dirigeant . auto-entrepreneur
avec:
conditions:
avec:
revenu:
valeur: salarié . cotisations . assiette * 6 mois > plancher
revenu: salarié . cotisations . assiette * 6 mois > plancher indemnités salarié
délai d'attente:
description: |
Pour pouvoir prétendre à une indemnisation pour maladie au titre de votre activité professionnelle, vous devez justifier dun délai daffiliation continus dans cette activité. Ce dernier dépend de votre rémunération des mois précédents.
remplace: arrêt maladie . délai d'attente
applicable si: conditions . revenu
valeur: (plancher / salarié . cotisations . assiette) + 0.5
valeur: (plancher indemnités salarié / salarié . cotisations . assiette) + 0.5
arrondi: oui
'[privé] plancher': 1015 heure * SMIC . horaire
références:
Quels sont les critères pour être indemnisé en cas de maladie ?: https://www.ameli.fr/tarn/assure/remboursements/indemnites-journalieres/arret-maladie-salarie#text_2632
Quels sont les critères pour être indemnisé en cas de maladie ?: https://www.ameli.fr/assure/remboursements/indemnites-journalieres/arret-maladie-salarie#text_2632
indemnités:
titre: indemnités journalières
applicable si: conditions . revenu
unité: €/jour
description: |
L'indemnité journalière que vous recevrez pendant votre arrêt de travail est égale à 50 % de votre salaire journalier de base. Celui-ci est calculé sur la moyenne des salaires bruts des 3 derniers mois précédant votre arrêt de travail (12 mois en cas d'activité saisonnière).
produit:
assiette:
valeur: salarié . cotisations . assiette / 91.25 jour/trimestre
valeur: salarié . cotisations . assiette
unité: €/jour
plafond: 1.8 * SMIC
taux: 50%
notes: |
- Vu que le simulateur ne permet pas encore la conversion de période vers le jour, on multiplie le salaire moyen par 3 pour avoir le salaire trimestriel, puis on le divise par 91.25, conformément à la fiche service-public.fr
- Pour les salarié, votre entreprise est peut-être soumise à une convention collective de branche professionnelle qui assure le maintien de votre salaire intégral ou partiel pendant votre arrêt de travail pour maladie. Elle peut aussi avoir conclu un accord interne à lentreprise qui prévoit ce maintien, appelé subrogation. Renseignez-vous auprès du service qui gère la paye dans votre entreprise.
références:
'Arrêt de travail pour maladie : les indemnités journalières du salarié': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/arret-maladie-salarie
'Arrêt maladie : indemnités journalières versées au salarié': https://www.service-public.fr/particuliers/vosdroits/F3053
protection sociale . maladie . arrêt maladie . indépendant:
applicable si:
@ -442,7 +461,7 @@ protection sociale . maladie . arrêt maladie . indépendant:
Depuis le 1er janvier 2022, il est donc possible de percevoir des indemnités journalières pour maladie et/ou pour maternité au titre de son ancienne activité (quel que soit le régime auquel on était affilié).
référence:
Comment bénéficier d'indemnités liées à son ancien régime: https://www.ameli.fr/tarn/assure/actualites/indemnites-maladie-et-maternite-du-nouveau-pour-certains-travailleurs-independants
Comment bénéficier d'indemnités liées à son ancien régime: https://www.ameli.fr/assure/actualites/indemnites-maladie-et-maternite-du-nouveau-pour-certains-travailleurs-independants
avec:
revenu: raam > 10% * plafond sécurité sociale
délai d'attente:
@ -456,6 +475,7 @@ protection sociale . maladie . arrêt maladie . indépendant:
'Artisan/commerçant : quels sont les critères pour être indemnisé en cas de maladie ?': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/arret-maladie-artisans-commercants#text_124972#text_124921
'Profession libérale : quels sont les critères pour être indemnisé en cas de maladie ?': 'https://www.ameli.fr/assure/remboursements/indemnites-journalieres/arret-maladie-profession-liberale#text_170646'
indemnités:
titre: indemnités journalières
applicable si: conditions . revenu
description: |
L'indemnité journalière que vous recevrez pendant votre arrêt de travail est égale à 1/730e de votre revenu dactivité annuel moyen (Raam) (1). Celui-ci est calculé sur la moyenne de vos revenus cotisés des 3 années civiles précédant la date de votre arrêt de travail.
@ -465,26 +485,100 @@ protection sociale . maladie . arrêt maladie . indépendant:
assiette: raam
facteur: 1 an / 730 jour
'[privé] raam':
titre: Revenu dactivité annuel moyen
valeur:
variations:
- si: dirigeant . indépendant
alors: dirigeant . indépendant . cotisations et contributions . indemnités journalières maladie . assiette
- si: dirigeant . auto-entrepreneur
alors: dirigeant . auto-entrepreneur . impôt . revenu imposable
plafond:
variations:
- si: entreprise . activité . nature . libérale . réglementée
alors: 3 * plafond sécurité sociale
- sinon: plafond sécurité sociale
références:
Quelles indemnités journalières pour les artisans/commerçants: https://www.ameli.fr/assure/remboursements/indemnites-journalieres/arret-maladie-artisans-commercants#text_124972
Quelles indemnités journalières pour les professions libérales: https://www.ameli.fr/assure/remboursements/indemnites-journalieres/arret-maladie-profession-liberale#text_170670
protection sociale . maladie . ATMP:
titre: Accident du travail et maladie professionnelle
protection sociale . maladie . maternité paternité adoption:
titre: indemnités congé maternité paternité adoption
références:
'Paternité et accueil de lenfant : vos indemnités journalières': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/conge-paternite-accueil-enfant
'Les prestations maternité des travailleuses indépendantes et des conjointes collaboratrices': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/prestations-maternite-independantes-conjointes-collaboratric
'Congé dadoption : les indemnités journalières': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/conge-adoption
'Simulateur maternité paternité adoption': https://www.ameli.fr/assure/simulateur-maternite-paternite
somme:
- salarié . indemnités
- indépendant . indemnités
avec:
délai d'attente:
description: |
## Maternité
Vous devez justifiez de 10 mois daffiliation à la date prévue de votre accouchement,
et cesser toute activité professionnelle pendant la période de perception et au moins pendant 8 semaines dont 6 après laccouchement
## Paternité / Adoption
Pour en bénéficier, vous devez justifier de 10 mois daffiliation à la naissance / à ladoption.
valeur: 10 mois
allocation forfaitaire de repos maternel:
non applicable si: oui
allocation forfaitaire de repos adoption:
non applicable si: oui
salarié:
applicable si: salarié
avec:
indemnités:
titre: indemnités journalières
applicable si: arrêt maladie . salarié . conditions . revenu
unité: €/jour
description: |
L'indemnité journalière que vous recevrez pendant votre arrêt de travail est égale à 50 % de votre salaire journalier de base. Celui-ci est calculé sur la moyenne des salaires bruts des 3 derniers mois précédant votre arrêt de travail (12 mois en cas d'activité saisonnière).
produit:
assiette:
valeur: salarié . cotisations . assiette
unité: €/jour
plafond:
unité: €/jour
arrondi: 2 décimales
le minimum de:
- plafond sécurité sociale
- salarié . cotisations . assiette - abattement forfaitaire salarié
plancher: invalidité et décès . pension invalidité . minimum salarié
références:
'Congé maternité : les indemnités journalières pour les salariées ': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/conge-maternite-salariee
indépendant:
applicable si:
une de ces conditions:
- dirigeant . indépendant
- dirigeant . auto-entrepreneur
avec:
indemnités:
titre: indemnités journalières forfaitaires
unité: €/jour
produit:
assiette: plafond sécurité sociale
facteur: 50%
abattement:
applicable si: raam < 10% * plafond sécurité sociale
valeur: 90%
références:
'Les indemnités journalières forfaitaires maternité des travailleuses indépendantes': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/prestations-maternite-independantes-conjointes-collaboratric#text_125695
'Paternité et accueil de lenfant : les indemnités journalières pour les travailleurs indépendants': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/conge-paternite-accueil-enfant#text_114763
allocation forfaitaire de repos maternel:
remplace: allocation forfaitaire de repos maternel
description: |
Elle est versée pour moitié au début du congé et pour moitié à la fin de la période obligatoire de cessation dactivité de 8 semaines. La totalité du montant de lallocation est versée après laccouchement lorsque celui-ci a lieu avant la fin du 7e mois de la grossesse.
produit:
assiette:
variations:
- si: raam < 10% * plafond sécurité sociale
alors: 10% * plafond sécurité sociale
- sinon: plafond sécurité sociale
facteur: 1 mois
référence:
'Lallocation forfaitaire de repos maternel': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/prestations-maternite-independantes-conjointes-collaboratric#text_125689
'Article L623-1 du code de la sécurité sociale': https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000042685502/
allocation forfaitaire de repos adoption:
remplace: allocation forfaitaire de repos adoption
valeur: 50% * allocation forfaitaire de repos maternel
références:
'Article L623-1 du code de la sécurité sociale': https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000042685502/
protection sociale . invalidité et décès:
icônes: 🦽
@ -498,6 +592,158 @@ protection sociale . invalidité et décès:
capital décès (amelie.fr): https://www.ameli.fr/assure/remboursements/pensions-allocations-rentes/deces-proche-capital-deces
capital décès (salarié privé): https://www.service-public.fr/particuliers/vosdroits/F3005
pension invalidité: https://www.service-public.fr/particuliers/vosdroits/F672
avec:
pension invalidité:
références:
Articles R341-4 à R341-6-1 du Code de la sécurité sociale: https://www.legifrance.gouv.fr/codes/id/LEGISCTA000006173390
avec:
invalidité partielle:
unité: €/mois
description: Si vous êtes capable d'exercer une activité professionnelle rémunérée, vous êtes classé en 1re catégorie.
plancher: minimum
produit:
assiette: revenu annuel moyen des 10 meilleures années
taux: 30%
références:
'Le montant de votre pension dinvalidité': https://www.ameli.fr/assure/remboursements/pensions-allocations-rentes/invalidite
invalidité totale:
unité: €/mois
plancher: minimum
description: Si vous ne pouvez plus exercer d'activité professionnelle, vous êtes classé en 2e catégorie.
produit:
assiette: revenu annuel moyen des 10 meilleures années
taux: 50%
références:
'Le montant de votre pension dinvalidité': https://www.ameli.fr/assure/remboursements/pensions-allocations-rentes/invalidite
revenu annuel moyen des 10 meilleures années:
description: |
Depuis le 1er juillet 2016, vous ne pouvez percevoir quune seule pension dinvalidité : soit au titre de votre activité salariée, soit au titre de votre activité dartisan/commerçant. Toutefois, le montant de la pension tient compte de tous vos revenus perçus quils proviennent de votre activité salariée ou de votre activité comme indépendant.
somme:
- salarié . cotisations . assiette
- applicable si: maladie . arrêt maladie . indépendant . conditions . revenu
variations:
- si: dirigeant . indépendant
alors: dirigeant . indépendant . cotisations et contributions . invalidité et décès . assiette
- si: dirigeant . auto-entrepreneur
alors: dirigeant . auto-entrepreneur . impôt . revenu imposable
plafond: plafond sécurité sociale
'[privé] minimum': non
minimum salarié:
remplace: minimum
applicable si: salarié
valeur: 309.09 €/mois
références:
"Montant minimal de la pension d'invalidité de travailleur salarié": https://www.legislation.cnav.fr/Lists/ArticlesBareme/DispForm.aspx?ID=4266&ContentTypeId=0x01007CF8FA8574A1B64CA3888B8B205B3F58
'Le montant de votre pension dinvalidité ': https://www.ameli.fr/assure/remboursements/pensions-allocations-rentes/invalidite
'[privé] minimum indépendant':
applicable si: maladie . arrêt maladie . indépendant . conditions . revenu
une de ces conditions:
- dirigeant . auto-entrepreneur
- dirigeant . indépendant
remplace:
- règle: minimum
dans: invalidité partielle
par: 486.98 €/mois
- règle: minimum
dans: invalidité totale
par: 686.09 €/mois
références:
"Montant et versement de la pension d'invalidité": https://www.ameli.fr/assure/remboursements/pensions-allocations-rentes/invalidite
accidents du travail et maladies professionnelles:
applicable si: salarié
références:
'Incapacité permanente suite à un accident du travail : indemnités et rente': https://www.ameli.fr/tarn/assure/remboursements/pensions-allocations-rentes/incapacite-permanente-suite-accident-travail
'Incapacité permanente suite à une maladie professionnelle : indemnités et rentes': https://www.ameli.fr/tarn/assure/remboursements/pensions-allocations-rentes/incapacite-permanente-suite-maladie-professionnelle
avec:
rente incapacité:
titre: Rente incapacité AT/MP
description: |
Si votre taux d'incapacité permanente suite à un accident du travail ou une maladie professionnelle est supérieur à 10 %, vous percevrez une rente d'incapacité permanente.
produit:
assiette:
barème:
assiette: salarié . cotisations . assiette
multiplicateur: SMIC
tranches:
- taux: 100%
plafond: 2
- taux: 1 / 3
plafond: 8
taux:
barème:
assiette:
nom: taux incapacité
question: Quel taux d'incapacité voulez-vous simuler pour la rente accidents du travail et maladie professionnelle ?
plancher: 10%
plafond: 100%
par défaut: 50%
tranches:
- taux: 50%
plafond: 50%
- taux: 150%
références:
"Accident du travail : indemnisation en cas d'incapacité permanente": https://www.service-public.fr/particuliers/vosdroits/F14840
rente décès:
titre: Rente décès AT/MP
description: |
Si l'accident du travail entraîne le décès de l'assuré, les proches (conjoint, concubin, partenaire lié par un pacte civil de solidarité (Pacs) - non divorcé ni séparé - enfants, etc.) peuvent bénéficier d'une rente.
produit:
assiette: salarié . cotisations . assiette
taux: 40%
références:
"Décès d'un salarié suite à un accident de travail ou de trajet : indemnisation des ayants droit": https://www.service-public.fr/particuliers/vosdroits/F14868
capital décès:
unité:
variations:
- si: salarié
alors: 3681 €
- si:
une de ces conditions:
- dirigeant . indépendant
- dirigeant . auto-entrepreneur
alors: 20% * plafond sécurité sociale * 1 an
capital décès . orphelin:
applicable si:
une de ces conditions:
- dirigeant . indépendant
- dirigeant . auto-entrepreneur
description: |
Un capital « orphelin » est versé aux enfants des travailleurs indépendants décédés. Il concerne :
- les enfants âgés de moins de 16 ans au jour du décès de lassuré et à sa charge ;
- les enfants à la charge du défunt de plus de 16 ans, et de moins de 20 ans, poursuivant leurs études ou leur apprentissage ;
- les enfants, quel que soit leur âge, bénéficiaires des allocations instituées en faveur des handicapés.
références:
'Le capital orphelin pour les enfants des travailleurs indépendants': https://www.ameli.fr/tarn/assure/remboursements/pensions-allocations-rentes/deces-proche-capital-deces#text_76987
valeur: 5% * plafond sécurité sociale * 1 an / 1 enfant
pension de reversion:
titre: pension de reversion maximum
unité: €/mois
description: |
Au décès de votre époux(se) ou ex-époux(se), vous pouvez percevoir une pension de réversion.
Le versement de la pension est possible, sous certaines conditions, lorsque le défunt exerçait une activité salariée ou non salariée (travailleur indépendant, professionnel libéral, agriculteur).
La pension est égale à 54 % de la retraite que votre époux(se) ou ex-époux(se) percevait ou aurait pu percevoir (majorations non comprises).
références:
Pension de réversion: https://www.lassuranceretraite.fr/portail-info/home/actif/travailleur-independant/veuvage/pension-reversion-veuvage.html
Montants minimums de la pension de reversion: https://www.legislation.cnav.fr/Pages/bareme.aspx?Nom=retraite_reversion_montant_minimum_bar
Pension de réversion - Défunt ayant travaillé dans le privé: https://www.service-public.fr/particuliers/vosdroits/F35774/0?idFicheParent=N378#0
plancher:
variations:
- si: date >= 07/2022
alors: 3672.01 €/an
- sinon: 3530.78 €/an
valeur: 54 % * retraite . base
protection sociale . assurance chômage:
icônes: 💸
@ -540,38 +786,47 @@ protection sociale . famille:
Allocations destinées aux familles: https://www.service-public.fr/particuliers/vosdroits/N156
Tout savoir sur les Allocations familiales: https://www.caf.fr/nous-connaitre/qui-sommes-nous
protection sociale . accidents du travail et maladies professionnelles:
protection sociale . maladie . accidents du travail et maladies professionnelles:
icônes: ☣️
résumé: Offre une couverture complète des maladies ou accidents du travail.
description: |
Lassurance AT/MP (accident du travail et maladie professionnelle) est la plus ancienne branche de la Sécurité sociale : elle relève de principes qui remontent à lannée 1898 et qui ont été repris dans la loi du 31 décembre 1946.
[🎞️ Voir la vidéo](https://www.youtube.com/watch?v=NaGI_deZJD8 )
La cotisation AT/MP couvre les risques accidents du travail, accidents de trajet et maladies professionnelles pour les salariés relevant du régime général.
Pour connaître les risques professionnels et mettre en place des actions de prévention, le [compte AT/MP](https://www.ameli.fr/entreprise/votre-entreprise/compte-atmp/ouvrir-compte-atmp) est un service ouvert à toutes les entreprises du régime général de la Sécurité sociale.
En cas dAT/MP, les soins médicaux et chirurgicaux sont remboursés intégralement dans la limite des tarifs de la Sécurité sociale.
Vous avez subi un accident du travail ou êtes atteint dune maladie professionnelle ?
Vos frais médicaux sont pris en charge à 100 %.
Pour compenser votre perte de salaire, vous pouvez percevoir des indemnités journalières.
Si vous êtes déclaré inapte suite à votre accident / maladie, vous pouvez recevoir une indemnité temporaire d'inaptitude.
unité: €/jour
applicable si: salarié
produit:
assiette:
valeur: 5
plafond: 83.4% * plafond sécurité sociale
taux:
nom: Pourcentage du salaire journalier de référence
valeur: 60%
note: |
Le taux est de 80% à partir du 29e jour d'arrêt.
avec:
indemmnités:
produit:
assiette:
nom: salaire journalier de référence
privé: oui
valeur: salarié . cotisations . assiette
unité: €/jour
plafond:
arrondi: 2 décimales
unité: €/jour
le minimum de:
- valeur: 0.834% * (plafond sécurité sociale * 1 an) / 1 jour
- valeur: salarié . cotisations . assiette - abattement forfaitaire salarié
taux: 60%
avec:
à partir du 29ème jour:
produit:
assiette: salaire journalier de référence
taux: 80%
références:
ameli.fr: https://www.ameli.fr/entreprise/votre-entreprise/cotisation-atmp
service-public.fr (AT): https://www.service-public.fr/particuliers/vosdroits/F31881
service-public.fr (MP): https://www.service-public.fr/particuliers/vosdroits/F31880
Calcul de l'indemnité: https://www.service-public.fr/particuliers/vosdroits/F32148
Code de la Sécurité Sociale: https://www.legifrance.gouv.fr/codes/id/LEGISCTA000006156659/2020-12-10/
"Comprendre l'assurance AT/MP": https://www.ameli.fr/entreprise/votre-entreprise/cotisation-atmp
'Maladie professionnelle : prise en charge et indemnités journalières': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/maladie-professionnelle
'Accident du travail : prise en charge et indemnités journalières': https://www.ameli.fr/assure/remboursements/indemnites-journalieres/accident-travail
"Qu'est-ce qu'un accident de trajet ?": https://www.service-public.fr/particuliers/vosdroits/F31881
"Qu'est-ce qu'une maladie professionnelle ?": https://www.service-public.fr/particuliers/vosdroits/F31880
"Accident du travail : indemnités journalières pendant l'arrêt de travail": https://www.service-public.fr/particuliers/vosdroits/F175
"Maladie professionnelle : indemnités journalières pendant l'arrêt de travail": https://www.service-public.fr/particuliers/vosdroits/F32148
Articles R433-1 à R433-17 du Code de la Sécurité Sociale: https://www.legifrance.gouv.fr/codes/id/LEGISCTA000006156659/2020-12-10/
protection sociale . formation:
icônes: 👩‍🎓

View File

@ -10,8 +10,8 @@ const salaireNetApresImpot = 'input[id="salariérémunérationnetpayéaprèsimp
describe('Test prerender', function () {
const testSimuSalaire = (cy: cyType) => {
cy.contains('Mensuel')
cy.contains('Annuel')
cy.contains('Montant mensuel')
cy.contains('Montant annuel')
cy.contains('Coût total')
cy.get(coutTotalSelector).should('exist')
@ -57,8 +57,8 @@ describe('Test prerender', function () {
cy.contains('Impôt sur le revenu')
cy.contains('Impôt sur les sociétés')
cy.contains('Mensuel')
cy.contains('Annuel')
cy.contains('Montant mensuel')
cy.contains('Montant annuel')
cy.contains("Chiffre d'affaires")
cy.get('input[id="entreprisechiffred\'affaires"]').should('exist')
@ -93,7 +93,7 @@ describe('Test prerender', function () {
cy.contains('a', 'Employee')
cy.contains('a', 'Auto-entrepreneur')
cy.contains('a', 'Liberal profession')
cy.contains('a', 'Status Comparison')
cy.contains('a', 'Discover all the simulators and assistants')
},
},

View File

@ -20,7 +20,7 @@ describe('Simulateur auto-entrepreneur', { testIsolation: 'off' }, function () {
})
it('should not have negative value', function () {
cy.contains('Mensuel').click()
cy.contains('Montant mensuel').click()
cy.get(inputSelector).first().type('{selectall}5000')
cy.get(inputSelector).each(($input) => {
cy.wrap($input).should(($i) => {

View File

@ -17,7 +17,7 @@ export const runSimulateurTest = (simulateur) => {
})
it('should display a result when entering a value in any of the currency input', function () {
cy.contains(fr ? 'Annuel' : 'Yearly').click()
cy.contains(fr ? 'Montant annuel' : 'Yearly amount').click()
if (['indépendant', 'profession-liberale'].includes(simulateur)) {
cy.get(chargeInputSelector).type(1000)
}
@ -40,13 +40,13 @@ export const runSimulateurTest = (simulateur) => {
})
it('should allow to change period', function () {
cy.contains(fr ? 'Annuel' : 'Yearly').click()
cy.contains(fr ? 'Montant annuel' : 'Yearly amount').click()
cy.get(inputSelector).first().type('{selectall}12000')
if (['indépendant', 'profession-liberale'].includes(simulateur)) {
cy.get(chargeInputSelector).type('{selectall}6000')
}
cy.get(inputSelector).eq(1).invoke('val').should('not.be.empty')
cy.contains(fr ? 'Mensuel' : 'Monthly').click()
cy.contains(fr ? 'Montant mensuel' : 'Monthly amount').click()
cy.get(inputSelector)
.first()
.invoke('val')
@ -54,7 +54,7 @@ export const runSimulateurTest = (simulateur) => {
if (['indépendant', 'profession-liberale'].includes(simulateur)) {
cy.get(chargeInputSelector).first().invoke('val').should('match', /500/)
}
cy.contains(fr ? 'Annuel' : 'Yearly').click()
cy.contains(fr ? 'Montant annuel' : 'Yearly amount').click()
})
it('should allow to navigate to a documentation page', function () {

View File

@ -97,6 +97,7 @@
"react-router-dom": "^6.4.4",
"react-signature-pad-wrapper": "^3.3.1",
"react-spring": "^9.5.5",
"react-tooltip": "^5.4.0",
"react-use-measure": "^2.1.1",
"recharts": "2.3.2",
"reduce-reducers": "^1.0.4",

View File

@ -109,10 +109,16 @@ const StyledValue = styled.span<{ $flashOnChange: boolean }>`
type ConditionProps = {
expression: PublicodesExpression | ASTNode
children: React.ReactNode
engine?: Engine<DottedName>
}
export function Condition({ expression, children }: ConditionProps) {
const engine = useEngine()
export function Condition({
expression,
children,
engine: engineFromProps,
}: ConditionProps) {
const defaultEngine = useEngine()
const engine = engineFromProps ?? defaultEngine
const nodeValue = engine.evaluate({ '!=': [expression, 'non'] }).nodeValue
if (!nodeValue) {
@ -122,15 +128,39 @@ export function Condition({ expression, children }: ConditionProps) {
return <>{children}</>
}
export function WhenValueEquals({
expression,
value,
children,
engine: engineFromProps,
}: ConditionProps & { value: string | number }) {
const defaultEngine = useEngine()
const engine = engineFromProps ?? defaultEngine
const nodeValue = engine.evaluate(expression).nodeValue
if (nodeValue !== value) {
return null
}
return <>{children}</>
}
export function WhenApplicable({
dottedName,
children,
engine,
}: {
dottedName: DottedName
children: React.ReactNode
engine?: Engine<DottedName>
}) {
const engine = useEngine()
if (engine.evaluate({ 'est applicable': dottedName }).nodeValue !== true) {
const defaultEngine = useEngine()
const engineValue = engine ?? defaultEngine
if (
engineValue.evaluate({ 'est applicable': dottedName }).nodeValue !== true
) {
return null
}
@ -140,13 +170,19 @@ export function WhenApplicable({
export function WhenNotApplicable({
dottedName,
children,
engine,
}: {
dottedName: DottedName
children: React.ReactNode
engine?: Engine<DottedName>
}) {
const engine = useEngine()
const defaultEngine = useEngine()
const engineValue = engine ?? defaultEngine
if (
engine.evaluate({ 'est non applicable': dottedName }).nodeValue !== true
engineValue.evaluate({ 'est non applicable': dottedName }).nodeValue !==
true
) {
return null
}
@ -157,12 +193,17 @@ export function WhenNotApplicable({
export function WhenAlreadyDefined({
dottedName,
children,
engine,
}: {
dottedName: DottedName
children: React.ReactNode
engine?: Engine<DottedName>
}) {
const engine = useEngine()
if (engine.evaluate({ 'est non défini': dottedName }).nodeValue) {
const defaultEngine = useEngine()
const engineValue = engine ?? defaultEngine
if (engineValue.evaluate({ 'est non défini': dottedName }).nodeValue) {
return null
}

View File

@ -12,11 +12,11 @@ export default function PeriodSwitch() {
const { t } = useTranslation()
const periods = [
{
label: t('Mensuel'),
label: t('Montant mensuel'),
unit: '€/mois',
},
{
label: t('Annuel'),
label: t('Montant annuel'),
unit: '€/an',
},
]
@ -26,6 +26,8 @@ export default function PeriodSwitch() {
<ToggleGroup
value={currentUnit}
onChange={(unit: string) => dispatch(updateUnit(unit))}
mode="tab"
hideRadio
>
{periods.map(({ label, unit }) => (
<span

View File

@ -1,6 +1,7 @@
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import { RuleLink as EngineRuleLink } from 'publicodes-react'
import React, { useContext } from 'react'
import React, { ReactNode, useContext } from 'react'
import { Link } from '@/design-system/typography/link'
import { useSitePaths } from '@/sitePaths'
@ -14,13 +15,17 @@ export default function RuleLink(
displayIcon?: boolean
children?: React.ReactNode
documentationPath?: string
linkComponent?: ReactNode
engine?: Engine<DottedName>
} & Omit<React.ComponentProps<typeof Link>, 'to' | 'children'>
) {
const { absoluteSitePaths } = useSitePaths()
const engine = useContext(EngineContext)
const defaultEngine = useContext(EngineContext)
const engineUsed = props?.engine ?? defaultEngine
try {
engine.getRule(props.dottedName)
engineUsed.getRule(props.dottedName)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
@ -32,8 +37,8 @@ export default function RuleLink(
<EngineRuleLink
{...props}
// @ts-ignore
linkComponent={Link}
engine={engine}
linkComponent={props?.linkComponent || Link}
engine={engineUsed}
documentationPath={
props.documentationPath ?? absoluteSitePaths.documentation.index
}

View File

@ -15,14 +15,11 @@ const QuestionsContainer = styled.div`
padding: ${({ theme }) => ` ${theme.spacings.xs} ${theme.spacings.lg}`};
border-radius: ${({ theme }) =>
`0 0 ${theme.box.borderRadius} ${theme.box.borderRadius}`};
background: ${({ theme }) => {
const palettePrimary = theme.colors.bases.primary
const paletteGrey = theme.colors.extended.grey
return theme.darkMode
? `linear-gradient(60deg, ${paletteGrey[800]} 0%, ${paletteGrey[700]} 100%);`
: `linear-gradient(60deg, ${palettePrimary[200]} 0%, ${palettePrimary[100]} 100%);`
}};
background-color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[700]
: theme.colors.extended.grey[100]};
box-shadow: ${({ theme }) => theme.elevations[2]};
`
const Notice = styled(Body)`

View File

@ -5,11 +5,14 @@ import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import { updateSituation } from '@/actions/actions'
import { ForceThemeProvider } from '@/contexts/DarkModeContext'
import { Grid } from '@/design-system/layout'
import { Strong } from '@/design-system/typography'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import { targetUnitSelector } from '@/selectors/simulationSelectors'
import RuleLink from '../RuleLink'
import { ExplicableRule } from '../conversation/Explicable'
import RuleInput, { InputProps } from '../conversation/RuleInput'
import AnimatedTargetValue from '../ui/AnimatedTargetValue'
import { Appear } from '../ui/animate'
@ -23,6 +26,7 @@ type SimulationGoalProps = {
appear?: boolean
editable?: boolean
isTypeBoolean?: boolean
isInfoMode?: boolean
onUpdateSituation?: (
name: DottedName,
@ -38,6 +42,7 @@ export function SimulationGoal({
appear = true,
editable = true,
isTypeBoolean = false, // TODO : remove when type inference works in publicodes
isInfoMode = false,
}: SimulationGoalProps) {
const dispatch = useDispatch()
const engine = useEngine()
@ -76,12 +81,34 @@ export function SimulationGoal({
>
<Grid item md="auto" sm={small ? 9 : 8} xs={8}>
<StyledGoalHeader>
<RuleLink
id={`${dottedName.replace(/\s|\./g, '')}-label`}
dottedName={dottedName}
>
{label}
</RuleLink>
{isInfoMode ? (
<Grid
container
css={`
align-items: center;
`}
>
<Grid item>
<StyledBody
id={`${dottedName.replace(/\s|\./g, '')}-label`}
>
<Strong>{label || rule.title}</Strong>
</StyledBody>
</Grid>
<Grid item>
<ForceThemeProvider forceTheme="default">
<ExplicableRule dottedName={dottedName} light />
</ForceThemeProvider>
</Grid>
</Grid>
) : (
<RuleLink
id={`${dottedName.replace(/\s|\./g, '')}-label`}
dottedName={dottedName}
>
{label}
</RuleLink>
)}
{rule.rawNode.résumé && (
<StyledSmallBody
@ -172,3 +199,7 @@ const StyledGoal = styled.div`
const StyledSmallBody = styled(SmallBody)`
margin-bottom: 0;
`
const StyledBody = styled(Body)`
color: ${({ theme }) => theme.colors.extended.grey[100]};
margin: 0;
`

View File

@ -1,11 +1,12 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled, { css } from 'styled-components'
import { ForceThemeProvider } from '@/contexts/DarkModeContext'
import { Grid } from '@/design-system/layout'
import { Link } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { firstStepCompletedSelector } from '@/selectors/simulationSelectors'
import { Logo } from '../Logo'
@ -13,7 +14,6 @@ import { WatchInitialRender } from '../utils/useInitialRender'
import { useIsEmbedded } from '../utils/useIsEmbedded'
type SimulationGoalsProps = {
className?: string
legend: string
publique?:
| 'employeur'
@ -36,29 +36,37 @@ export function SimulationGoals({
return (
<WatchInitialRender>
<TopSection toggles={toggles} />
<div role="group" aria-labelledby="simulator-legend-label">
<TopSection toggles={toggles} />
<StyledSimulationGoals
isEmbeded={isEmbeded}
isFirstStepCompleted={isFirstStepCompleted}
publique={publique}
role="group"
id="simulator-legend"
aria-labelledby="simulator-legend-label"
aria-live="polite"
>
<ForceThemeProvider forceTheme="dark">
<div className="sr-only" aria-hidden id="simulator-legend-label">
{legend}
</div>
{children}
</ForceThemeProvider>
</StyledSimulationGoals>
<SimulationGoalsContainer
isEmbeded={isEmbeded}
isFirstStepCompleted={isFirstStepCompleted}
publique={publique}
id="simulator-legend"
aria-live="polite"
>
<ForceThemeProvider forceTheme="dark">
<div className="sr-only" aria-hidden id="simulator-legend-label">
{legend}
</div>
<Body className="visually-hidden">
<em>
<Trans>
Les données de simulations se mettront automatiquement à jour
après la modification d'un champ.
</Trans>
</em>
</Body>
{children}
</ForceThemeProvider>
</SimulationGoalsContainer>
</div>
</WatchInitialRender>
)
}
const StyledSimulationGoals = styled.div<
export const SimulationGoalsContainer = styled.div<
Pick<SimulationGoalsProps, 'publique'> & {
isFirstStepCompleted: boolean
isEmbeded: boolean
@ -67,7 +75,10 @@ const StyledSimulationGoals = styled.div<
z-index: 1;
position: relative;
padding: ${({ theme }) => `${theme.spacings.sm} ${theme.spacings.lg}`};
border-radius: ${({ theme }) => theme.box.borderRadius};
border-start-end-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
border-start-start-radius: ${({ theme }) => theme.box.borderRadius};
${({ isFirstStepCompleted }) =>
isFirstStepCompleted &&
css`
@ -81,13 +92,19 @@ const StyledSimulationGoals = styled.div<
? theme.colors.publics[publique]
: theme.colors.bases.primary
return css`linear-gradient(60deg, ${colorPalette[800]} 0%, ${colorPalette[600]} 100%);`
return css`
${colorPalette[600]};
`
}};
@media print {
background: initial;
padding: 0;
}
@media (max-width: ${({ theme }) => theme.breakpointsWidth.sm}) {
border-start-start-radius: ${({ theme }) => theme.box.borderRadius};
border-start-end-radius: ${({ theme }) => theme.box.borderRadius};
}
`
function TopSection({ toggles }: { toggles?: React.ReactNode }) {
@ -129,9 +146,9 @@ const Section = styled(Grid).attrs({ container: true })`
gap: ${({ theme }) => theme.spacings.xs};
`
const ToggleSection = styled.div`
export const ToggleSection = styled.div`
padding: ${({ theme }) => theme.spacings.sm} 0;
padding-bottom: 0;
display: flex;
justify-content: right;
text-align: right;

View File

@ -10,7 +10,6 @@ import { ConversationProps } from '@/components/conversation/Conversation'
import { PopoverWithTrigger } from '@/design-system'
import { Grid, Spacing } from '@/design-system/layout'
import { Link } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import {
companySituationSelector,
firstStepCompletedSelector,
@ -21,7 +20,7 @@ import Banner from '../Banner'
import AnswerList from '../conversation/AnswerList'
import PrintExportRecover from '../simulationExplanation/PrintExportRecover'
import PreviousSimulationBanner from './../PreviousSimulationBanner'
import { FadeIn, FromTop } from './../ui/animate'
import { FromTop } from './../ui/animate'
import { Questions } from './Questions'
export { Questions } from './Questions'
@ -37,9 +36,12 @@ type SimulationProps = {
hideDetails?: boolean
showQuestionsFromBeginning?: boolean
customEndMessages?: ConversationProps['customEndMessages']
fullWidth?: boolean
id?: string
}
const StyledGrid = styled(Grid)`
width: 100%;
@media print {
max-width: initial;
flex-basis: initial;
@ -57,6 +59,8 @@ export default function Simulation({
showQuestionsFromBeginning,
engines,
hideDetails = false,
fullWidth,
id,
}: SimulationProps) {
const firstStepCompleted = useSelector(firstStepCompletedSelector)
const existingCompany = !!useSelector(companySituationSelector)[
@ -74,15 +78,21 @@ export default function Simulation({
css={`
justify-content: center;
`}
id={id}
>
<StyledGrid item xl={9} lg={10} md={11} sm={12}>
<StyledGrid
item
css={`
${fullWidth
? `width: 100%; max-width: none; flex-basis: auto;`
: ''}
`}
xl={9}
lg={10}
md={11}
sm={12}
>
<PrintExportRecover />
<Body className="visually-hidden">
<Trans>
Les données de simulations se mettront automatiquement à jour
après la modification d'un champ.
</Trans>
</Body>
{children}
<FromTop>
{(firstStepCompleted || showQuestionsFromBeginning) && (

View File

@ -28,6 +28,7 @@ import {
import { Emoji } from '@/design-system/emoji'
import { Item, Select } from '@/design-system/field/Select'
import { Spacing } from '@/design-system/layout'
import { Switch } from '@/design-system/switch'
import { H3, H4 } from '@/design-system/typography/heading'
import { ExplicableRule } from './Explicable'
@ -166,75 +167,84 @@ function RadioChoice<Names extends string = DottedName>({
return (
<>
{choice.children.map((node) => (
<Fragment key={node.dottedName}>
{' '}
{hiddenOptions.includes(
node.dottedName as DottedName
) ? null : 'children' in node ? (
<div
role="group"
aria-labelledby={
node.dottedName.replace(/\s|\./g, '') + '-legend'
}
css={`
margin-top: -1rem;
`}
>
<H4 as={H3} id={node.dottedName + '-legend'}>
{node.title}
</H4>
<Spacing lg />
<StyledSubRadioGroup>
<RadioChoice
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
defaultValue={defaultValue}
choice={node}
rootDottedName={rootDottedName}
type={type}
/>
</StyledSubRadioGroup>
</div>
) : (
<span>
<Radio
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={
// Doit autoFocus si correspond à la valeur par défaut
(defaultValue &&
defaultValue ===
`'${relativeDottedName(
rootDottedName,
node.dottedName
)}'` &&
autoFocus) ||
// Sinon doit autoFocus automatiquement
autoFocus
{choice.children.map((node) => {
return (
<Fragment key={node.dottedName}>
{' '}
{hiddenOptions.includes(
node.dottedName as DottedName
) ? null : 'children' in node ? (
<div
role="group"
aria-labelledby={
node.dottedName.replace(/\s|\./g, '') + '-legend'
}
value={`'${relativeDottedName(
rootDottedName,
node.dottedName
)}'`}
id={`radio-input-${node.dottedName.replace(
/\s|\./g,
''
)}-${rootDottedName.replace(/\s|\./g, '')}`}
css={`
margin-top: -1rem;
`}
>
{node.title}{' '}
{node.rawNode.icônes && <Emoji emoji={node.rawNode.icônes} />}
</Radio>{' '}
{type !== 'toggle' && (
<ExplicableRule
light
dottedName={node.dottedName as DottedName}
aria-label={`En savoir plus sur ${node.title}`}
/>
)}
</span>
)}
</Fragment>
))}
<H4 as={H3} id={node.dottedName + '-legend'}>
{node.title}
</H4>
<Spacing lg />
<StyledSubRadioGroup>
<RadioChoice
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
defaultValue={defaultValue}
choice={node}
rootDottedName={rootDottedName}
type={type}
/>
</StyledSubRadioGroup>
</div>
) : (
<span>
<Radio
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={
// Doit autoFocus si correspond à la valeur par défaut
(defaultValue &&
defaultValue ===
`'${relativeDottedName(
rootDottedName,
node.dottedName
)}'` &&
autoFocus) ||
// Sinon doit autoFocus automatiquement
autoFocus
}
value={`'${relativeDottedName(
rootDottedName,
node.dottedName
)}'`}
id={`radio-input-${relativeDottedName(
rootDottedName,
node.dottedName
).replace(/\s|\./g, '')}-${rootDottedName.replace(
/\s|\./g,
''
)}`}
>
{node.title}{' '}
{node.rawNode.icônes && <Emoji emoji={node.rawNode.icônes} />}
</Radio>{' '}
{type !== 'toggle' && (
<ExplicableRule
light
dottedName={node.dottedName as DottedName}
aria-label={`En savoir plus sur ${node.title}`}
/>
)}
</span>
)}
</Fragment>
)
})}
{choice.canGiveUp && (
<>
<Radio value={'non'}>{t('Aucun')}</Radio>
@ -334,3 +344,25 @@ export function useSelection<Names extends string = DottedName>({
return { currentSelection, handleChange, defaultValue }
}
export const SwitchInput = (props: {
onChange?: (isSelected: boolean) => void
defaultSelected?: boolean
label?: string
id?: string
key?: string
}) => {
const { onChange, id, label, defaultSelected, key } = props
return (
<Switch
defaultSelected={defaultSelected}
onChange={(isSelected: boolean) => onChange && onChange(isSelected)}
light
id={id}
key={key}
>
{label}
</Switch>
)
}

View File

@ -132,7 +132,7 @@ export default function Conversation({
<Grid container spacing={2}>
{previousAnswers.length > 0 && (
<Grid item xs={6} sm="auto">
<Button light onPress={goToPrevious} size="XS">
<Button color="secondary" onPress={goToPrevious} size="XS">
<span aria-hidden></span> <Trans>Précédent</Trans>
</Button>
</Grid>
@ -141,7 +141,8 @@ export default function Conversation({
<Button
size="XS"
onPress={goToNextQuestion}
light={!currentQuestionIsAnswered}
light={!currentQuestionIsAnswered ? true : undefined}
color={!currentQuestionIsAnswered ? 'secondary' : undefined}
aria-label={
currentQuestionIsAnswered
? t('Suivant, passer à la question suivante')

View File

@ -3,20 +3,24 @@ import { useContext } from 'react'
import { EngineContext } from '@/components/utils/EngineContext'
import { Markdown } from '@/components/utils/markdown'
import HelpButton from '@/design-system/buttons/HelpButton'
import HelpButtonWithPopover from '@/design-system/buttons/HelpButtonWithPopover'
import { Spacing } from '@/design-system/layout'
import { H3 } from '@/design-system/typography/heading'
import { References } from '../References'
import RuleLink from '../RuleLink'
export function ExplicableRule<Names extends string = DottedName>({
dottedName,
light,
bigPopover,
title,
...props
}: {
dottedName: Names
light?: boolean
bigPopover?: boolean
title?: string
}) {
const engine = useContext(EngineContext)
@ -33,10 +37,10 @@ export function ExplicableRule<Names extends string = DottedName>({
// TODO montrer les variables de type 'une possibilité'
return (
<HelpButton
<HelpButtonWithPopover
key={rule.dottedName}
type="info"
title={rule.title}
title={title ?? rule.title}
light={light}
bigPopover={bigPopover}
className="print-hidden"
@ -44,12 +48,18 @@ export function ExplicableRule<Names extends string = DottedName>({
{...props}
>
<Markdown>{rule.rawNode.description}</Markdown>
<RuleLink dottedName={dottedName as DottedName}>
Lire la documentation
</RuleLink>
{rule.rawNode.références && (
<>
<H3>Liens utiles</H3>
<References references={rule.rawNode.références} />
</>
)}
</HelpButton>
<Spacing xxl />
</HelpButtonWithPopover>
)
}

View File

@ -22,6 +22,8 @@ import TextInput from './TextInput'
import SelectPaysDétachement from './select/SelectPaysDétachement'
import SelectAtmp from './select/SelectTauxRisque'
type InputType = 'radio' | 'card' | 'toggle' | 'select'
type Props<Names extends string = DottedName> = Omit<
React.HTMLAttributes<HTMLInputElement>,
'onChange' | 'defaultValue' | 'onSubmit'
@ -42,10 +44,11 @@ type Props<Names extends string = DottedName> = Omit<
// cf .https://github.com/betagouv/mon-entreprise/issues/1489#issuecomment-823058710
showDefaultDateValue?: boolean
onSubmit?: (source?: string) => void
inputType?: 'radio' | 'card' | 'toggle' | 'select'
inputType?: InputType
formatOptions?: Intl.NumberFormatOptions
displayedUnit?: string
modifiers?: Record<string, string>
engine?: Engine<DottedName>
}
export type InputProps<Name extends string = string> = Omit<
@ -77,13 +80,16 @@ export default function RuleInput<Names extends string = DottedName>({
missing,
inputType,
modifiers = {},
engine,
...props
}: Props<Names>) {
const engine = useContext(EngineContext)
const rule = engine.getRule(dottedName)
const evaluation = engine.evaluate({ valeur: dottedName, ...modifiers })
const value = evaluation.nodeValue
const defaultEngine = useContext(EngineContext)
const engineValue = engine ?? defaultEngine
const rule = engineValue.getRule(dottedName)
const evaluation = engineValue.evaluate({ valeur: dottedName, ...modifiers })
const value = evaluation.nodeValue
const shouldFocusField = useShouldFocusField()
const commonProps: InputProps<Names> = {
@ -106,7 +112,7 @@ export default function RuleInput<Names extends string = DottedName>({
}
const meta = getMeta<{ affichage?: string }>(rule.rawNode, {})
if (getVariant(engine.getRule(dottedName))) {
if (getVariant(engineValue.getRule(dottedName))) {
const type =
inputType ??
(meta.affichage &&
@ -117,7 +123,7 @@ export default function RuleInput<Names extends string = DottedName>({
return (
<MultipleAnswerInput
{...commonProps}
choice={buildVariantTree(engine, dottedName)}
choice={buildVariantTree(engineValue, dottedName)}
type={type}
/>
)

View File

@ -1,8 +1,11 @@
import React from 'react'
import { Trans } from 'react-i18next'
import styled from 'styled-components'
import { PopoverWithTrigger } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { FocusStyle } from '@/design-system/global-style'
import { EditIcon } from '@/design-system/icons'
import Answers from './AnswerList'
@ -17,14 +20,13 @@ export default function SeeAnswersButton({
<>
<PopoverWithTrigger
trigger={(buttonProps) => (
<Button
{...buttonProps}
size="XS"
color="secondary"
aria-haspopup="dialog"
>
{label ?? <Trans>Modifier mes réponses</Trans>}
</Button>
<StyledButton {...buttonProps} aria-haspopup="dialog">
{label ?? (
<>
<EditIcon /> <Trans>Modifier mes réponses</Trans>
</>
)}
</StyledButton>
)}
>
{(close) => <Answers onClose={close}>{children}</Answers>}
@ -32,3 +34,32 @@ export default function SeeAnswersButton({
</>
)
}
const StyledButton = styled(Button)`
background-color: transparent;
padding: 0;
border: none;
color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[100]
: theme.colors.bases.primary[700]};
border-radius: 0;
display: flex;
align-items: center;
svg {
margin-right: ${({ theme }) => theme.spacings.xxs};
fill: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[100]
: theme.colors.bases.primary[700]};
}
&:hover {
border: none;
background-color: transparent;
text-decoration: underline;
}
&:focus {
${FocusStyle}
}
`

View File

@ -2,6 +2,7 @@ import { ReactNode, createContext, useState } from 'react'
import { ThemeProvider } from 'styled-components'
import { useIsEmbedded } from '@/components/utils/useIsEmbedded'
import { useDarkMode } from '@/hooks/useDarkMode'
import { getItem, setItem } from '@/storage/safeLocalStorage'
type DarkModeContextType = [boolean, (darkMode: boolean) => void]
@ -47,7 +48,7 @@ export const DarkModeProvider = ({ children }: { children: ReactNode }) => {
)
}
export type ThemeType = 'light' | 'dark'
export type ThemeType = 'default' | 'light' | 'dark'
export const ForceThemeProvider = ({
children,
@ -56,12 +57,16 @@ export const ForceThemeProvider = ({
children: ReactNode
forceTheme?: ThemeType
}) => {
const [darkMode] = useDarkMode()
return (
<ThemeProvider
theme={(theme) => ({
...theme,
darkMode:
forceTheme === undefined ? theme.darkMode : forceTheme === 'dark',
forceTheme === undefined || forceTheme === 'default'
? darkMode
: forceTheme === 'dark',
})}
>
{children}

View File

@ -2,29 +2,140 @@ import { useAccordion, useAccordionItem } from '@react-aria/accordion'
import { TreeState, useTreeState } from '@react-stately/tree'
import { AriaAccordionProps } from '@react-types/accordion'
import { Node } from '@react-types/shared'
import { useRef } from 'react'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { Trans } from 'react-i18next'
import { animated, useSpring } from 'react-spring'
import useMeasure from 'react-use-measure'
import styled, { css } from 'styled-components'
import { omit } from '@/utils'
import { Button } from '../buttons'
import { FocusStyle } from '../global-style'
import { ChevronIcon } from '../icons'
import { Grid } from '../layout'
import chevronImg from './chevron.svg'
export const Accordion = <T extends object>(props: AriaAccordionProps<T>) => {
const SAVE_STATE_LOCALSTORAGE_KEY = 'accordion-state'
export const Accordion = <T extends object>(
props: AriaAccordionProps<T> & {
variant?: 'light'
shouldToggleAll?: boolean
title?: ReactNode
isFoldable?: boolean
shouldSaveState?: boolean
}
) => {
const { title, isFoldable, shouldSaveState } = props
const state = useTreeState<T>(props)
const ref = useRef<HTMLDivElement>(null)
const { accordionProps } = useAccordion(props, state, ref)
const [shouldOpenAll, setShouldOpenAll] = useState(false)
const [shouldCloseAll, setShouldCloseAll] = useState(false)
const openAll = () => {
setShouldOpenAll(true)
setTimeout(() => {
setShouldOpenAll(false)
}, 100)
}
const closeAll = () => {
setShouldCloseAll(true)
setTimeout(() => {
setShouldCloseAll(false)
})
}
// State and useEffect ne fonctionnent pas ensemble
if (shouldOpenAll) {
const keys = state.collection.getKeys()
for (const key of keys) {
if (!state.expandedKeys.has(key)) {
state.expandedKeys.add(key)
}
}
}
if (shouldCloseAll) {
const keys = state.collection.getKeys()
for (const key of keys) {
if (state.expandedKeys.has(key)) {
state.expandedKeys.delete(key)
}
}
}
const allItemsOpen = Array.from(state.collection.getKeys()).every((key) =>
state.expandedKeys.has(key)
)
// Save opening state of Accordion between pages
if (shouldSaveState && localStorage?.getItem(SAVE_STATE_LOCALSTORAGE_KEY)) {
const arrayExpandedKeys = JSON.parse(
localStorage.getItem(SAVE_STATE_LOCALSTORAGE_KEY) || ''
) as string[]
const keys = state.collection.getKeys()
for (const key of keys) {
if (arrayExpandedKeys.includes(key as string)) {
state.expandedKeys.add(key)
}
}
localStorage.removeItem(SAVE_STATE_LOCALSTORAGE_KEY)
}
useEffect(() => {
if (!shouldSaveState) return
return () => {
localStorage.setItem(
SAVE_STATE_LOCALSTORAGE_KEY,
JSON.stringify(Array.from(state.expandedKeys))
)
}
}, [state])
return (
<StyledAccordionGroup {...props} {...accordionProps} ref={ref}>
{[...state.collection].map((item) => (
<AccordionItem<T> key={item.key} item={item} state={state} />
))}
</StyledAccordionGroup>
<>
{title && (
<StyledGrid container>
<Grid item>{title}</Grid>
{isFoldable && (
<Grid item>
<StyledFoldButton
underline
onClick={() => (allItemsOpen ? closeAll() : openAll())}
>
<StyledChevronIcon $isOpen={allItemsOpen} />
<Trans>{allItemsOpen ? 'Tout plier' : 'Tout déplier'}</Trans>
</StyledFoldButton>
</Grid>
)}
</StyledGrid>
)}
<StyledAccordionGroup
{...omit(props, 'title')}
{...accordionProps}
ref={ref}
>
{[...state.collection].map((item) => {
return (
<AccordionItem<T>
key={item.key}
item={item}
state={state}
$variant={props?.variant}
/>
)
})}
</StyledAccordionGroup>
</>
)
}
const StyledAccordionGroup = styled.div`
const StyledAccordionGroup = styled.div<{ variant?: 'light' }>`
max-width: 100%;
${({ theme }) =>
css`
@ -32,20 +143,26 @@ const StyledAccordionGroup = styled.div`
border: 1px solid ${theme.colors.bases.primary[400]};
margin-bottom: ${theme.spacings.lg};
`}
${({ variant }) =>
variant === 'light' &&
css`
border-radius: 0;
border: none;
`}
`
interface AccordionItemProps<T> {
item: Node<T>
state: TreeState<T>
$variant?: 'light'
}
function AccordionItem<T>(props: AccordionItemProps<T>) {
const ref = useRef<HTMLButtonElement>(null)
const { state, item } = props
const { state, item, $variant } = props
const { buttonProps, regionProps } = useAccordionItem<T>(props, state, ref)
const isOpen = state.expandedKeys.has(item.key)
// const isDisabled = state.disabledKeys.has(item.key)
const [regionRef, { height }] = useMeasure()
const animatedStyle = useSpring({
@ -58,13 +175,19 @@ function AccordionItem<T>(props: AccordionItemProps<T>) {
return (
<StyledAccordionItem onMouseDown={(x) => x.stopPropagation()}>
<StyledTitle>
<StyledButton {...buttonProps} ref={ref}>
<StyledButton {...buttonProps} ref={ref} $variant={$variant}>
<span>{item.props.title}</span>
<ChevronRightMedium aria-hidden $isOpen={isOpen} alt="" />
</StyledButton>
</StyledTitle>
{/* @ts-ignore: https://github.com/pmndrs/react-spring/issues/1515 */}
<StyledContent {...regionProps} style={animatedStyle} hidden={!isOpen}>
<StyledContent
// @ts-ignore
{...regionProps}
style={animatedStyle}
hidden={!isOpen}
$variant={$variant}
>
<div ref={regionRef}>{item.props.children}</div>
</StyledContent>
</StyledAccordionItem>
@ -81,7 +204,7 @@ const StyledAccordionItem = styled.div`
}
`
const StyledButton = styled.button`
const StyledButton = styled.button<{ $variant?: 'light' }>`
display: flex;
width: 100%;
background: none;
@ -98,11 +221,25 @@ const StyledButton = styled.button`
}
`}
:hover {
text-decoration: underline;
text-decoration: ${({ $variant }) =>
$variant === 'light' ? 'none' : 'underline'};
}
:focus {
${FocusStyle}
}
${({ theme, $variant }) =>
$variant === 'light' &&
css`
background-color: transparent;
padding: 1.5rem;
padding-left: 0;
align-items: center;
border-bottom: 1px solid ${theme.colors.bases.primary[400]};
> span {
border-radius: 0;
}
`}
`
interface Chevron {
@ -118,11 +255,41 @@ const ChevronRightMedium = styled.img.attrs({ src: chevronImg })<Chevron>`
`}
`
const StyledContent = styled(animated.div)<{ $isOpen: boolean }>`
const StyledContent = styled(animated.div)<{
$isOpen: boolean
$variant?: 'light'
}>`
overflow: hidden;
> div {
margin: ${({ theme }) => theme.spacings.lg};
margin: ${({ theme, $variant }) =>
$variant !== 'light' && theme.spacings.lg};
}
`
const StyledGrid = styled(Grid)`
justify-content: space-between;
align-items: center;
`
const StyledFoldButton = styled(Button)`
text-decoration: none;
background-color: transparent;
color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[100]
: theme.colors.bases.primary[700]};
&:hover {
text-decoration: none;
}
`
const StyledChevronIcon = styled(ChevronIcon)<{ $isOpen?: boolean }>`
transition: transform 0.15s ease-in-out;
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(-90deg)' : 'rotate(90deg)')};
fill: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[100]
: theme.colors.bases.primary[700]}!important;
`
Accordion.StyledTitle = StyledTitle

View File

@ -20,6 +20,7 @@ type ButtonProps = GenericButtonOrNavLinkProps & {
role?: string
['aria-disabled']?: boolean
lang?: string
underline?: boolean
}
export const Button = forwardRef(function Button(
@ -27,6 +28,7 @@ export const Button = forwardRef(function Button(
size = 'MD',
light = false,
color = 'primary' as const,
underline,
isDisabled,
role,
lang,
@ -41,11 +43,13 @@ export const Button = forwardRef(function Button(
return (
<StyledButton
// eslint-disable-next-line react/jsx-props-no-spreading
{...buttonOrLinkProps}
$size={size}
$light={light}
$color={color}
$isDisabled={isDisabled}
$underline={underline}
role={role}
aria-disabled={ariaButtonProps?.['aria-disabled']}
lang={lang}
@ -58,6 +62,7 @@ type StyledButtonProps = {
$size: Size
$light: boolean
$isDisabled?: boolean
$underline?: boolean
}
export const StyledButton = styled.button<StyledButtonProps>`
@ -143,7 +148,7 @@ export const StyledButton = styled.button<StyledButtonProps>`
${$color === 'secondary' &&
css`
border-color: ${theme.colors.bases[$color][500]};
`}
`};
`}
@media not print {
@ -207,4 +212,30 @@ export const StyledButton = styled.button<StyledButtonProps>`
`}
}
}
${({ $underline }) =>
$underline &&
css`
background-color: transparent;
padding: 0;
border: none;
color: ${({ theme }) => theme.colors.bases.primary[700]};
border-radius: 0;
display: flex;
align-items: center;
text-decoration: underline;
svg {
margin-right: ${({ theme }) => theme.spacings.xxs};
fill: ${({ theme }) => theme.colors.bases.primary[700]};
}
&:hover {
border: none;
background-color: transparent;
text-decoration: underline;
}
&:focus {
${FocusStyle}
}
`}
`

View File

@ -15,7 +15,7 @@ type HelpButtonProps = {
className?: string
}
export default function HelpButton({
export default function HelpButtonWithPopover({
children,
title,
type,

View File

@ -29,4 +29,4 @@ Secondary.args = {
children: 'Secondary XS button',
}
export { CloseButton, HelpButton } from '@/design-system/buttons'
export { CloseButton, HelpButtonWithPopover } from '@/design-system/buttons'

View File

@ -1,3 +1,3 @@
export * from './Button'
export { default as CloseButton } from './CloseButton'
export { default as HelpButton } from './HelpButton'
export { default as HelpButtonWithPopover } from './HelpButtonWithPopover'

View File

@ -125,12 +125,15 @@ const IconContainer = styled.div`
margin-top: ${({ theme }) => theme.spacings.md};
`
export const CardContainer = styled.div<{ $compact?: boolean }>`
export const CardContainer = styled.div<{
$compact?: boolean
$inert?: boolean
}>`
display: flex;
width: 100%;
height: 100%;
text-decoration: none;
cursor: pointer;
cursor: ${({ $inert }) => ($inert ? 'auto' : 'pointer')};
flex-direction: column;
align-items: center;
background-color: ${({ theme }) =>
@ -141,12 +144,14 @@ export const CardContainer = styled.div<{ $compact?: boolean }>`
box-shadow: ${({ theme }) =>
theme.darkMode ? theme.elevationsDarkMode[2] : theme.elevations[2]};
&:hover {
box-shadow: ${({ theme }) =>
theme.darkMode ? theme.elevationsDarkMode[3] : theme.elevations[3]};
background-color: ${({ theme }) =>
theme.darkMode
box-shadow: ${({ theme, $inert }) =>
!$inert &&
(theme.darkMode ? theme.elevationsDarkMode[3] : theme.elevations[3])};
background-color: ${({ theme, $inert }) =>
!$inert &&
(theme.darkMode
? theme.colors.extended.dark[500]
: theme.colors.bases.primary[100]};
: theme.colors.bases.primary[100])};
}
padding: ${({ theme: { spacings }, $compact = false }) =>
$compact

View File

@ -0,0 +1,24 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'
import { CheckList } from '@/design-system'
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
component: CheckList,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
items: [{ isChecked: 'boolean', label: 'string' }],
},
} as ComponentMeta<typeof CheckList>
export const CheckListWithData: ComponentStory<typeof CheckList> = () => (
<CheckList
items={[
{ isChecked: false, label: "Tient compte de l'ACRE" },
{
isChecked: true,
label: "Versement libératoire de l'impôt sur le revenu",
},
]}
/>
)

View File

@ -0,0 +1,54 @@
import { ReactNode } from 'react'
import styled, { css } from 'styled-components'
import { CheckmarkIcon, CrossIcon } from '../icons'
export const CheckList = ({
items,
}: {
items: { label: ReactNode; isChecked: boolean }[]
}) => {
return (
<StyledUl>
{items.map((item, index) => {
const { isChecked, label } = item
return (
<StyledLi
$isChecked={isChecked}
key={`checklist-item-${item.toString()}-${index}`}
>
{isChecked ? <CheckmarkIcon /> : <CrossIcon />}
{label}
</StyledLi>
)
})}
</StyledUl>
)
}
const StyledUl = styled.ul`
margin: 0;
padding: 0;
`
const StyledLi = styled.li<{ $isChecked?: boolean }>`
list-style: none;
svg {
margin-right: 0.5rem;
flex-shrink: 0;
${({ theme, $isChecked }) =>
!$isChecked &&
css`
fill: ${theme.darkMode
? theme.colors.extended.grey[100]
: theme.colors.extended.grey[600]}!important;
`}
}
display: flex;
align-items: center;
font-family: ${({ theme }) => theme.fonts.main};
&:not(:last-child) {
margin-bottom: 1.5rem;
}
`

View File

@ -0,0 +1,249 @@
import FocusTrap from 'focus-trap-react'
import React, { ReactNode, useCallback, useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
import { Trans } from 'react-i18next'
import styled, { css } from 'styled-components'
import { Button } from '../buttons'
import { Grid } from '../layout'
import { CloseButton, CloseButtonContainer } from '../popover/Popover'
export type DrawerButtonProps = {
onClick: () => void
['aria-expanded']: boolean
['aria-haspopup']:
| boolean
| 'dialog'
| 'menu'
| 'grid'
| 'listbox'
| 'tree'
| 'true'
| 'false'
| undefined
}
export const Drawer = ({
trigger,
children,
onConfirm,
onCancel,
confirmLabel,
cancelLabel,
isDismissable = true,
}: {
trigger: ({ onClick }: DrawerButtonProps) => ReactNode
children: ReactNode
confirmLabel?: string
cancelLabel?: string
onConfirm: () => void
onCancel?: () => void
isDismissable?: boolean
}) => {
const [isOpen, setIsOpen] = useState(false)
const [isMounted, setIsMounted] = useState(false)
const openDrawer = () => {
setIsMounted(true)
}
const disablePageScrolling = (shouldDisableScroll: boolean) => {
if (shouldDisableScroll) {
document.body.style.top = `-${window.scrollY}px`
document.body.style.position = 'fixed'
} else {
const scrollY = document.body.style.top
document.body.style.position = ''
document.body.style.top = ''
// Avoid scroll jump
window.scrollTo(0, parseInt(scrollY || '0') * -1)
}
}
useEffect(() => {
if (isMounted) {
setIsOpen(true)
disablePageScrolling(true)
}
}, [isMounted])
const closeDrawer = () => {
setIsOpen(false)
setTimeout(() => {
setIsMounted(false)
if (onCancel) {
onCancel()
}
}, 500)
}
const handleDeactivate = useCallback(() => {
disablePageScrolling(false)
}, [])
return (
<>
{trigger({
'aria-expanded': isOpen,
'aria-haspopup': 'dialog',
onClick: openDrawer,
})}
{isMounted &&
ReactDOM.createPortal(
<DrawerContainer>
<DrawerBackground $isOpen={isOpen} />
<FocusTrap
focusTrapOptions={{
clickOutsideDeactivates: true,
onDeactivate: () => {
closeDrawer()
handleDeactivate()
},
}}
>
<DrawerPanel $isOpen={isOpen} role="dialog">
{isDismissable && (
<StyledCloseButtonContainer>
{/* TODO : replace with Link when in design system */}
<CloseButton onClick={() => closeDrawer()}>
Fermer
<svg
role="img"
aria-hidden
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.69323 17.2996C6.30271 16.9091 6.30271 16.276 6.69323 15.8854L15.8856 6.69304C16.2761 6.30252 16.9093 6.30252 17.2998 6.69304C17.6904 7.08356 17.6904 7.71673 17.2998 8.10725L8.10744 17.2996C7.71692 17.6902 7.08375 17.6902 6.69323 17.2996Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.6635 6.69306C7.05402 6.30254 7.68719 6.30254 8.07771 6.69306L17.2701 15.8854C17.6606 16.276 17.6606 16.9091 17.2701 17.2997C16.8796 17.6902 16.2464 17.6902 15.8559 17.2997L6.6635 8.10727C6.27297 7.71675 6.27297 7.08359 6.6635 6.69306Z"
/>
</svg>
</CloseButton>
</StyledCloseButtonContainer>
)}
<DrawerContent>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { closeDrawer } as {
closeDrawer: () => void
})
}
})}
</DrawerContent>
{onConfirm && (
<DrawerFooter>
<StyledGrid container>
<Grid item>
<Button light onPress={() => closeDrawer()}>
{cancelLabel ?? <Trans>Annuler</Trans>}
</Button>
</Grid>
<Grid item>
<Button
onPress={() => {
onConfirm()
setTimeout(() => closeDrawer(), 200)
}}
>
{confirmLabel ?? <Trans>Confirmer</Trans>}
</Button>
</Grid>
</StyledGrid>
</DrawerFooter>
)}
</DrawerPanel>
</FocusTrap>
</DrawerContainer>,
document.querySelector('body') as Element
)}
</>
)
}
const DrawerContainer = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
width: 100vw;
height: 100vh;
`
const DrawerBackground = styled.div<{ $isOpen?: boolean }>`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
transition: background-color 0.4s ease-in-out;
background-color: ${({ $isOpen }) =>
$isOpen ? 'rgba(0, 0, 0, 0.25)' : 'transparent'};
overflow: hidden;
cursor: pointer;
width: 100vw;
height: 100vh;
`
const DrawerPanel = styled.div<{
$isOpen: boolean
}>`
width: 500px;
max-width: 100vw;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
background-color: ${({ theme }) => theme.colors.extended.grey[100]};
transition: transform 0.5s ease-in-out;
position: fixed;
right: 0;
top: 0;
transform: translateX(100%);
z-index: 10;
${({ $isOpen }) =>
$isOpen &&
css`
transform: translateX(0);
`}
`
const DrawerContent = styled.div`
padding: 0 ${({ theme }) => theme.spacings.xxl};
padding-bottom: 2rem;
min-height: 100%;
`
const DrawerFooter = styled.div`
position: sticky;
bottom: 0;
left: 0;
right: 0;
background: ${({ theme }) => theme.colors.extended.grey[100]};
padding: ${({ theme }) => theme.spacings.xl};
box-shadow: 0px 1px 15px rgba(0, 0, 0, 0.15);
z-index: 10;
`
const StyledGrid = styled(Grid)`
display: flex;
justify-content: center;
gap: ${({ theme }) => theme.spacings.md};
`
const StyledCloseButtonContainer = styled(CloseButtonContainer)`
position: sticky;
top: 0;
left: 0;
right: 0;
background: ${({ theme }) => theme.colors.extended.grey[100]};
z-index: 10;
`

View File

@ -0,0 +1,51 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'
import { Drawer } from '@/design-system/drawer'
import { Button } from '../buttons'
import { DrawerButtonProps } from './Drawer'
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
component: Drawer,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
children: {
type: 'string',
},
trigger: {
type: 'function',
},
isDismissable: {
type: 'boolean',
},
onConfirm: {
type: 'function',
},
confirmLabel: {
type: 'string',
},
cancelLabel: {
type: 'string',
},
},
} as ComponentMeta<typeof Drawer>
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof Drawer> = (args) => <Drawer {...args} />
export const Basic = Template.bind({})
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Basic.args = {
children: <div>Coucou, je suis ouvert !</div>,
trigger: (buttonProps: DrawerButtonProps) => (
<Button color="primary" light {...buttonProps}>
Ouvrir le tiroir
</Button>
),
isDismissable: true,
// eslint-disable-next-line no-console
onConfirm: () => console.log('onConfirm appelé !'),
confirmLabel: 'Sauvegarder',
cancelLabel: 'Annuler',
}

View File

@ -0,0 +1 @@
export { Drawer } from './Drawer'

View File

@ -142,6 +142,7 @@ export const LabelBody = styled(Body)<{
}>`
margin: ${({ theme }) => theme.spacings.xs} 0px;
margin-left: ${({ theme }) => theme.spacings.xxs};
background-color: transparent;
${({ $hideRadio }) =>
$hideRadio &&
css`

View File

@ -11,11 +11,15 @@ import {
VisibleRadio,
} from './Radio'
export type Toggle = 'toggle'
export type Tab = 'tab'
type ToggleGroupProps = AriaRadioGroupProps & {
label?: string
hideRadio?: boolean
children: React.ReactNode
className?: string
mode?: Toggle | Tab
}
export function ToggleGroup(props: ToggleGroupProps) {
@ -33,17 +37,52 @@ export function ToggleGroup(props: ToggleGroupProps) {
aria-label={props['aria-label'] ?? undefined}
>
{label && <span {...labelProps}>{label}</span>}
<ToggleGroupContainer hideRadio={props.hideRadio ?? false}>
<ToggleGroupContainer
hideRadio={props.hideRadio ?? false}
mode={props?.mode}
>
<RadioContext.Provider value={state}>{children}</RadioContext.Provider>
</ToggleGroupContainer>
</div>
)
}
export const ToggleGroupContainer = styled.div<{ hideRadio: boolean }>`
const TabModeStyle = css`
border: none !important;
border-radius: ${({ theme }) =>
`${theme.spacings.md} ${theme.spacings.md} 0 0`}!important;
background-color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.dark[600]
: theme.colors.bases.primary[200]};
padding: 0.875rem 2rem;
@media (max-width: ${({ theme }) => theme.breakpointsWidth.sm}) {
padding: 0.875rem 0.875rem;
}
`
const TabModeCheckedStyle = css`
z-index: 2;
border: none !important;
background-color: ${({ theme }) => theme.colors.bases.primary[600]};
${LabelBody} {
color: ${({ theme }) => theme.colors.extended.grey[100]}!important;
}
`
export const ToggleGroupContainer = styled.div<{
hideRadio: boolean
mode?: Toggle | Tab
}>`
--radius: 0.25rem;
display: inline-flex;
flex-wrap: wrap;
${({ mode }) =>
mode === 'tab' &&
css`
flex-wrap: nowrap;
`}
${VisibleRadio} {
position: relative;
@ -63,6 +102,8 @@ export const ToggleGroupContainer = styled.div<{ hideRadio: boolean }>`
theme.darkMode
? theme.colors.extended.dark[600]
: theme.colors.extended.grey[100]};
${({ mode }) => mode === 'tab' && TabModeStyle}
}
${LabelBody} {
@ -92,6 +133,7 @@ export const ToggleGroupContainer = styled.div<{ hideRadio: boolean }>`
theme.darkMode
? theme.colors.bases.primary[500]
: theme.colors.bases.primary[200]};
${({ mode }) => mode === 'tab' && TabModeCheckedStyle}
}
${VisibleRadio}:hover {

View File

@ -15,4 +15,8 @@ export {
ReturnIcon,
SearchIcon,
SuccessIcon,
EditIcon,
HexagonIcon,
TriangleIcon,
CircleIcon,
} from '@/design-system/icons'

View File

@ -151,3 +151,257 @@ export const ReturnIcon = (props: HTMLAttributes<SVGElement>) => (
/>
</SvgIcon>
)
export const EditIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="#1D458C"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.6713 4.34045C17.1223 3.79142 16.1035 3.9201 15.3957 4.62786L14.0118 6.01178L17.9882 9.98822L19.3721 8.6043C20.0799 7.89653 20.2086 6.8777 19.6595 6.32867L17.6713 4.34045ZM16.4882 11.4882L12.5118 7.51178L5.26693 14.7566C4.94924 15.0743 4.73441 15.4722 4.66412 15.8732L4.00634 19.625C3.96438 19.8643 4.13568 20.0356 4.37496 19.9937L8.12685 19.3359C8.52777 19.2656 8.92567 19.0508 9.24336 18.7331L16.4882 11.4882Z"
/>
</svg>
)
export const HexagonIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="16"
height="16"
viewBox="0 0 16 16"
fill="#1D458C"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path d="M8 3L12.3301 5.5V10.5L8 13L3.66987 10.5V5.5L8 3Z" />
</svg>
)
export const TriangleIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="16"
height="16"
viewBox="0 0 16 16"
fill="#1D458C"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path d="M8 3L13.1962 12H2.80385L8 3Z" />
</svg>
)
export const CircleIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="16"
height="16"
viewBox="0 0 16 16"
fill="#1D458C"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path d="M8 3L8.52264 3.02739L9.03956 3.10926L9.54508 3.24472L10.0337 3.43227L10.5 3.66987L10.9389 3.95492L11.3457 4.28428L11.7157 4.65435L12.0451 5.06107L12.3301 5.5L12.5677 5.96632L12.7553 6.45492L12.8907 6.96044L12.9726 7.47736L13 8L12.9726 8.52264L12.8907 9.03956L12.7553 9.54508L12.5677 10.0337L12.3301 10.5L12.0451 10.9389L11.7157 11.3457L11.3457 11.7157L10.9389 12.0451L10.5 12.3301L10.0337 12.5677L9.54508 12.7553L9.03956 12.8907L8.52264 12.9726L8 13L7.47736 12.9726L6.96044 12.8907L6.45492 12.7553L5.96632 12.5677L5.5 12.3301L5.06107 12.0451L4.65435 11.7157L4.28428 11.3457L3.95492 10.9389L3.66987 10.5L3.43227 10.0337L3.24472 9.54508L3.10926 9.03956L3.02739 8.52264L3 8L3.02739 7.47736L3.10926 6.96044L3.24472 6.45492L3.43227 5.96632L3.66987 5.5L3.95492 5.06107L4.28428 4.65435L4.65435 4.28428L5.06107 3.95492L5.5 3.66987L5.96632 3.43227L6.45492 3.24472L6.96044 3.10926L7.47736 3.02739L8 3Z" />
</svg>
)
export const HelpIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="#1D458C"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM12.9725 16.5886C12.9725 17.1472 12.5197 17.6 11.9612 17.6C11.4026 17.6 10.9498 17.1472 10.9498 16.5886C10.9498 16.0301 11.4026 15.5772 11.9612 15.5772C12.5197 15.5772 12.9725 16.0301 12.9725 16.5886ZM10.6146 9.25871C10.6784 9.05633 10.7844 8.86166 10.9534 8.71892C11.1062 8.58978 11.3903 8.43428 11.949 8.43296L11.9495 8.43297C11.9552 8.43316 11.9681 8.43369 11.9873 8.43506C12.0261 8.43782 12.088 8.44382 12.165 8.4566C12.3237 8.48295 12.5199 8.53377 12.7037 8.62531C12.8836 8.71493 13.0322 8.83327 13.138 8.9913C13.2394 9.14281 13.3416 9.38993 13.3416 9.80899C13.3416 10.1472 13.1563 10.4615 12.8509 10.747C12.7055 10.8829 12.5563 10.989 12.4428 11.0609C12.387 11.0962 12.3424 11.1216 12.3147 11.1368L12.3005 11.1444C11.704 11.4042 11.3207 11.8034 11.1131 12.2523C10.9193 12.6712 10.9196 13.0527 10.9198 13.1824L10.9198 13.1912V13.5544C10.9198 14.113 11.3726 14.5658 11.9312 14.5658C12.4897 14.5658 12.9426 14.113 12.9426 13.5544V13.1912C12.9426 13.115 12.9473 13.1049 12.9489 13.1017L12.949 13.1014L12.9492 13.101L12.9502 13.0995C12.9512 13.0981 12.9547 13.0933 12.9627 13.0858C12.9775 13.0718 13.0214 13.0352 13.1209 12.9935C13.1372 12.9867 13.1534 12.9794 13.1693 12.9717L12.7301 12.0607C13.1693 12.9717 13.1696 12.9716 13.17 12.9714L13.1707 12.9711L13.1722 12.9703L13.1758 12.9686L13.1847 12.9642L13.2095 12.9517C13.229 12.9417 13.2545 12.9283 13.285 12.9116C13.3461 12.8782 13.4283 12.831 13.5246 12.7701C13.7154 12.6493 13.9719 12.468 14.2322 12.2247C14.7383 11.7516 15.3644 10.9401 15.3644 9.80899C15.3644 9.03433 15.167 8.38616 14.819 7.8662C14.4754 7.35277 14.0247 7.02346 13.6054 6.81465C13.19 6.60776 12.7869 6.50937 12.4963 6.46111C12.3485 6.43659 12.2232 6.42394 12.1309 6.41737C12.0846 6.41407 12.046 6.41227 12.0166 6.41129C12.0019 6.4108 11.9894 6.41052 11.9793 6.41036L11.9659 6.41019L11.9605 6.41016L11.9582 6.41016L11.9571 6.41015C11.9566 6.41015 11.9561 6.41015 11.9561 7.42155V6.41015C10.9764 6.41015 10.2108 6.69811 9.64782 7.17384C9.10024 7.63652 8.82473 8.20846 8.68535 8.65082C8.51749 9.18358 8.8133 9.75155 9.34606 9.91941C9.87882 10.0873 10.4468 9.79147 10.6146 9.25871ZM12.2868 11.1516L12.2872 11.1513C12.2851 11.1524 12.2843 11.1528 12.2849 11.1525L12.2868 11.1516Z"
/>
</svg>
)
export const CheckmarkIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="24"
height="12"
viewBox="0 0 16 12"
fill="#3CB053"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.7109 0.697112C16.0993 1.08974 16.0959 1.7229 15.7033 2.11131L6.39385 11.3207C6.00272 11.7077 5.37249 11.706 4.98346 11.3169L0.292893 6.62637C-0.0976311 6.23584 -0.0976311 5.60268 0.292893 5.21215C0.683417 4.82163 1.31658 4.82163 1.70711 5.21215L5.69437 9.19942L14.2967 0.689476C14.6894 0.301066 15.3225 0.304485 15.7109 0.697112Z"
/>
</svg>
)
export const CrossIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="#6C757D"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.69372 17.2999C6.30319 16.9094 6.30319 16.2762 6.69372 15.8857L15.8861 6.69328C16.2766 6.30276 16.9098 6.30276 17.3003 6.69328C17.6908 7.08381 17.6908 7.71697 17.3003 8.1075L8.10793 17.2999C7.71741 17.6904 7.08424 17.6904 6.69372 17.2999Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.66399 6.69331C7.05451 6.30278 7.68768 6.30278 8.0782 6.69331L17.2706 15.8857C17.6611 16.2762 17.6611 16.9094 17.2706 17.2999C16.8801 17.6904 16.2469 17.6904 15.8564 17.2999L6.66399 8.10752C6.27346 7.71699 6.27346 7.08383 6.66399 6.69331Z"
/>
</svg>
)
export const ExternalLinkIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="14"
height="14"
viewBox="0 0 14 14"
fill="#6C757D"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.69372 17.2999C6.30319 16.9094 6.30319 16.2762 6.69372 15.8857L15.8861 6.69328C16.2766 6.30276 16.9098 6.30276 17.3003 6.69328C17.6908 7.08381 17.6908 7.71697 17.3003 8.1075L8.10793 17.2999C7.71741 17.6904 7.08424 17.6904 6.69372 17.2999Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.292969 2.83301C0.292969 1.45229 1.41226 0.333008 2.79297 0.333008H4.6388C5.09904 0.333008 5.47214 0.706104 5.47214 1.16634C5.47214 1.62658 5.09904 1.99967 4.6388 1.99967H2.79297C2.33273 1.99967 1.95964 2.37277 1.95964 2.83301V11.208C1.95964 11.6682 2.33273 12.0413 2.79297 12.0413L11.168 12.0413C11.6282 12.0413 12.0013 11.6682 12.0013 11.208V9.36217C12.0013 8.90194 12.3744 8.52884 12.8346 8.52884C13.2949 8.52884 13.668 8.90194 13.668 9.36217V11.208C13.668 12.5887 12.5487 13.708 11.168 13.708L2.79297 13.708C1.41226 13.708 0.292969 12.5887 0.292969 11.208V2.83301ZM7.83464 1.99967C7.3744 1.99967 7.0013 1.62658 7.0013 1.16634C7.0013 0.706104 7.3744 0.333008 7.83464 0.333008H12.8346C13.2949 0.333008 13.668 0.706104 13.668 1.16634V6.16634C13.668 6.62658 13.2949 6.99967 12.8346 6.99967C12.3744 6.99967 12.0013 6.62658 12.0013 6.16634V3.17819L8.42389 6.7556C8.09845 7.08103 7.57082 7.08103 7.24538 6.7556C6.91994 6.43016 6.91994 5.90252 7.24538 5.57709L10.8228 1.99967H7.83464Z"
fill="#212529"
/>
</svg>
)
export const CircledArrowIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="40"
height="40"
viewBox="0 0 40 40"
fill="#1D458C"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20 40C31.0457 40 40 31.0457 40 20C40 8.95431 31.0457 0 20 0C8.95431 0 0 8.95431 0 20C0 31.0457 8.95431 40 20 40Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M23.1195 13.6676C23.5243 13.2918 24.157 13.3152 24.5328 13.7199L29.7328 19.3199C30.0891 19.7036 30.0891 20.2972 29.7328 20.6808L24.5328 26.2808C24.157 26.6856 23.5243 26.709 23.1195 26.3332C22.7148 25.9574 22.6914 25.3247 23.0672 24.9199L27.6354 20.0004L23.0672 15.0808C22.6914 14.6761 22.7148 14.0434 23.1195 13.6676Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 20.0004C10 19.4481 10.4477 19.0004 11 19.0004L29 19.0004C29.5523 19.0004 30 19.4481 30 20.0004C30 20.5527 29.5523 21.0004 29 21.0004L11 21.0004C10.4477 21.0004 10 20.5527 10 20.0004Z"
fill="white"
/>
</svg>
)
export const PlusCircleIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="#2E5FB6"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM22 12C22 17.5229 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5229 2 22 6.47715 22 12Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.2 12C17.2 12.5523 16.7523 13 16.2 13L7.8 13C7.24771 13 6.8 12.5523 6.8 12C6.8 11.4477 7.24771 11 7.8 11L16.2 11C16.7523 11 17.2 11.4477 17.2 12Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 17.2C11.4477 17.2 11 16.7523 11 16.2L11 7.8C11 7.24772 11.4477 6.8 12 6.8C12.5523 6.8 13 7.24771 13 7.8L13 16.2C13 16.7523 12.5523 17.2 12 17.2Z"
/>
</svg>
)
export const ArrowRightIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="14"
height="14"
viewBox="0 0 14 14"
fill="#212529"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.91877 0.667113C7.32348 0.29131 7.95621 0.314745 8.33201 0.719455L13.532 6.31945C13.8883 6.70314 13.8883 7.29668 13.532 7.68036L8.33201 13.2804C7.95621 13.6851 7.32348 13.7085 6.91877 13.3327C6.51406 12.9569 6.49062 12.3242 6.86643 11.9195L11.4346 6.99991L6.86643 2.08036C6.49062 1.67565 6.51406 1.04292 6.91877 0.667113Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.199219 6.99991C0.199219 6.44762 0.646934 5.99991 1.19922 5.99991L12.7992 5.99991C13.3515 5.99991 13.7992 6.44762 13.7992 6.9999C13.7992 7.55219 13.3515 7.9999 12.7992 7.9999L1.19922 7.99991C0.646934 7.99991 0.199219 7.55219 0.199219 6.99991Z"
/>
</svg>
)
export const WarningIcon = (props: HTMLAttributes<SVGElement>) => (
<svg
{...props}
width="18"
height="18"
viewBox="0 0 18 18"
fill="#FFBB0C"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.72825 0.928245C9.58121 0.663907 9.30248 0.5 9 0.5C8.69752 0.5 8.41879 0.663907 8.27175 0.928245L0.855084 14.2616C0.711514 14.5197 0.715285 14.8345 0.864996 15.0891C1.01471 15.3437 1.28799 15.5 1.58333 15.5H16.4167C16.712 15.5 16.9853 15.3437 17.135 15.0891C17.2847 14.8345 17.2885 14.5197 17.1449 14.2616L9.72825 0.928245ZM8.99992 4.91667C8.53968 4.91667 8.16658 5.28976 8.16658 5.75V9.91667C8.16658 10.3769 8.53968 10.75 8.99992 10.75C9.46016 10.75 9.83325 10.3769 9.83325 9.91667V5.75C9.83325 5.28976 9.46016 4.91667 8.99992 4.91667ZM8.99992 13.4583C9.46016 13.4583 9.83325 13.0852 9.83325 12.625C9.83325 12.1648 9.46016 11.7917 8.99992 11.7917C8.53968 11.7917 8.16658 12.1648 8.16658 12.625C8.16658 13.0852 8.53968 13.4583 8.99992 13.4583Z"
/>
</svg>
)

View File

@ -9,3 +9,4 @@ export { default as Popover } from './popover/Popover'
export { default as PopoverWithTrigger } from './popover/PopoverWithTrigger'
export { Step, Stepper } from './stepper'
export * as typography from './typography'
export * from './checklist'

View File

@ -51,17 +51,22 @@ type ContainerProps = {
children: ReactNode
forceTheme?: ThemeType
backgroundColor?: (theme: DefaultTheme) => string
className?: string
}
export default function Container({
backgroundColor,
forceTheme,
children,
className,
}: ContainerProps) {
return (
<ForceThemeProvider forceTheme={forceTheme}>
<OuterOuterContainer>
<OuterContainer $backgroundColor={backgroundColor}>
<OuterContainer
$backgroundColor={backgroundColor}
className={className}
>
<InnerContainer>{children}</InnerContainer>
</OuterContainer>
</OuterOuterContainer>

View File

@ -127,13 +127,9 @@ export default function Popover(
</CloseButton>
</CloseButtonContainer>
)}
{/* tabIndex -1 is for text selection in popover, see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 */}
<PopoverContent ref={contentRef}>
{title && (
<H2 as="h1" {...titleProps}>
{title}
</H2>
)}
{title && <H2 {...titleProps}>{title}</H2>}
{children}
</PopoverContent>
</PopoverContainer>
@ -162,7 +158,7 @@ const Underlay = styled.div<UnderlayProps>`
right: 0;
bottom: 0;
left: 0;
overflow: auto;
overflow: visible;
z-index: 200; // to be in front of the menu of the Publicodes doc
background: rgba(0, 0, 0, 0.5);
animation: ${appear} 0.2s;

View File

@ -35,9 +35,10 @@ export default function PopoverConfirm({
return (
<PopoverWithTrigger trigger={trigger} small={small}>
{(closePopover) => (
<StyledContainer>
<H3>{title}</H3>
<Body>{children}</Body>
<div>
{title && <H3>{title}</H3>}
<div>{children}</div>
<StyledGrid container>
<Grid item>
@ -56,7 +57,7 @@ export default function PopoverConfirm({
</Button>
</Grid>
</StyledGrid>
</StyledContainer>
</div>
)}
</PopoverWithTrigger>
)
@ -68,7 +69,3 @@ const StyledGrid = styled(Grid)`
gap: ${({ theme }) => theme.spacings.md};
margin-top: ${({ theme }) => theme.spacings.xl};
`
const StyledContainer = styled.div`
padding: ${({ theme }) => theme.spacings.xxl};
`

View File

@ -1,12 +1,47 @@
import styled from 'styled-components'
export const Tag = styled.div`
import { baseTheme, getColorGroup } from '../theme'
export type TagType = keyof typeof baseTheme.colors.bases &
keyof typeof baseTheme.colors.extended &
keyof typeof baseTheme.colors.publics
type SizeType = 'sm' | 'md' | 'lg'
const lightColors = ['grey']
export const Tag = styled.div<{ $color?: TagType; $size?: SizeType }>`
font-family: ${({ theme }) => theme.fonts.main};
background-color: ${({ theme }) => theme.colors.bases.primary[100]};
display: flex;
align-items: center;
width: fit-content;
padding: 0.25rem 1rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
color: ${({ theme }) => theme.colors.extended.grey[800]};
font-weight: 500;
background-color: ${({ theme, $color }) =>
$color
? theme.colors[getColorGroup($color)][$color][
lightColors.includes($color) ? 300 : 100
]
: theme.colors.bases.primary[100]};
color: ${({ theme, $color }) =>
$color
? theme.colors[getColorGroup($color)][$color][600]
: theme.colors.extended.grey[800]};
font-size: ${({ $size }) => {
switch ($size) {
case 'sm':
return '0.75rem'
case 'md':
default:
return '1rem'
}
}};
svg {
fill: ${({ theme, $color }) =>
$color
? theme.colors[getColorGroup($color)][$color][600]
: theme.colors.extended.grey[800]};
}
`

View File

@ -1,6 +1,8 @@
import { Theme } from '@/types/styled'
const baseTheme = {
import { TagType } from './tag'
export const baseTheme = {
colors: {
bases: {
primary: {
@ -183,6 +185,19 @@ const baseTheme = {
},
}
export type ColorGroups = Array<keyof typeof baseTheme.colors>
export const getColorGroup = (color: TagType) => {
const colorGroups: ColorGroups = Object.keys(baseTheme.colors).map(
(colorGroup) => colorGroup as keyof typeof baseTheme.colors
)
return colorGroups.find(
(colorGroup: keyof typeof baseTheme.colors) =>
!!baseTheme.colors[colorGroup]?.[color]
) as keyof typeof baseTheme.colors
}
// We use the Grid from material-ui, we need to uniformise
// breakpoints and spacing with the Urssaf design system
export type SpacingKey = keyof typeof baseTheme.breakpointsWidth

View File

@ -0,0 +1,54 @@
import React, { ReactNode } from 'react'
import { Tooltip as RTooltip } from 'react-tooltip'
import 'react-tooltip/dist/react-tooltip.css'
import styled from 'styled-components'
export const Tooltip = ({
children,
tooltip,
className,
id,
}: {
children: ReactNode
tooltip: ReactNode
className?: string
// A11y : préciser un aria-describedby sur l'élément visé par le tooltip
id: string
}) => {
return (
<StyledSpan>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { id } as {
id: string
})
}
})}
<StyledRTooltip
anchorId={id}
className={className}
id={`${id}-description`}
>
{tooltip}
</StyledRTooltip>
</StyledSpan>
)
}
const StyledRTooltip = styled(RTooltip)`
max-width: 20rem;
font-size: 0.75rem;
`
const StyledSpan = styled.span`
display: flex;
align-items: center;
justify-content: center;
.react-tooltip {
opacity: 1 !important;
background: ${({ theme }) => theme.colors.extended.grey[800]};
color: ${({ theme }) => theme.colors.extended.grey[100]};
z-index: 100;
}
`

View File

@ -0,0 +1,31 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'
import { Tooltip } from '@/design-system/tooltip'
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
component: Tooltip,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
children: {
type: 'string',
},
tooltip: {
type: 'string',
},
id: {
type: 'string',
},
},
} as ComponentMeta<typeof Tooltip>
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof Tooltip> = (args) => <Tooltip {...args} />
export const Basic = Template.bind({})
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Basic.args = {
children: 'Passez la souris sur moi',
tooltip: 'Coucou !',
id: 'test-tooltip',
}

View File

@ -0,0 +1 @@
export { Tooltip } from './Tooltip'

View File

@ -4,6 +4,7 @@ import { useLocation, useNavigationType } from 'react-router-dom'
import { debounce, getSessionStorage } from '@/utils'
const POP_ACTION_LABEL = 'POP'
const REPLACE_ACTION_LABEL = 'REPLACE'
const sessionStorage = getSessionStorage()
export const useSaveAndRestoreScrollPosition = () => {
@ -13,7 +14,11 @@ export const useSaveAndRestoreScrollPosition = () => {
useEffect(() => {
const scrollPosition = sessionStorage?.getItem(location.pathname)
if (scrollPosition && navigationType === POP_ACTION_LABEL) {
if (
scrollPosition &&
(navigationType === POP_ACTION_LABEL ||
navigationType === REPLACE_ACTION_LABEL)
) {
window.scrollTo(0, parseInt(scrollPosition))
}
}, [location, navigationType])

View File

@ -5202,48 +5202,6 @@ protection sociale:
formation professionnelle et le transport.
titre.en: social welfare
titre.fr: protection sociale
protection sociale . accidents du travail et maladies professionnelles:
description.en: >
[automatic] Occupational injury and disease insurance is the
oldest branch of Social Security: it is based on principles dating back to
1898 and included in the law of December 31, 1946.
[🎞️ See the video](https://www.youtube.com/watch?v=NaGI_deZJD8 )
The AT/MP contribution covers the risks of accidents at work, commuting accidents and occupational diseases for employees under the general scheme.
To find out about occupational risks and set up prevention actions, the [AT/MP account](https://www.ameli.fr/entreprise/votre-entreprise/compte-atmp/ouvrir-compte-atmp) is a service open to all companies under the general Social Security scheme.
In the event of an occupational injury, medical and surgical care is fully reimbursed within the limits of the Social Security rates.
description.fr: >
Lassurance AT/MP (accident du travail et maladie
professionnelle) est la plus ancienne branche de la Sécurité sociale : elle
relève de principes qui remontent à lannée 1898 et qui ont été repris dans
la loi du 31 décembre 1946.
[🎞️ Voir la vidéo](https://www.youtube.com/watch?v=NaGI_deZJD8 )
La cotisation AT/MP couvre les risques accidents du travail, accidents de trajet et maladies professionnelles pour les salariés relevant du régime général.
Pour connaître les risques professionnels et mettre en place des actions de prévention, le [compte AT/MP](https://www.ameli.fr/entreprise/votre-entreprise/compte-atmp/ouvrir-compte-atmp) est un service ouvert à toutes les entreprises du régime général de la Sécurité sociale.
En cas dAT/MP, les soins médicaux et chirurgicaux sont remboursés intégralement dans la limite des tarifs de la Sécurité sociale.
note.en: |
[automatic] The rate is 80% from the 29th day of the stoppage.
note.fr: |
Le taux est de 80% à partir du 29e jour d'arrêt.
résumé.en: Provides comprehensive coverage for occupational diseases or accidents.
résumé.fr: Offre une couverture complète des maladies ou accidents du travail.
titre.en: Work accidents / occupational diseases
titre.fr: accidents du travail et maladies professionnelles
protection sociale . assurance chômage:
description.en: >
Since 1958, the Unemployment Insurance has been protecting all
@ -5408,9 +5366,33 @@ protection sociale . maladie:
des maladies graves comme les séjours à l'hôpital.
titre.en: '[automatic] health insurance'
titre.fr: assurance maladie
protection sociale . maladie . ATMP:
titre.en: '[automatic] Workplace injury and occupational disease'
titre.fr: Accident du travail et maladie professionnelle
protection sociale . maladie . accidents du travail et maladies professionnelles:
description.en: >
[automatic] Have you suffered an accident at work or an
occupational disease?
Your medical expenses are covered at 100%.
To compensate for your loss of salary, you can receive a daily allowance.
If you are declared unfit as a result of your accident/illness, you may receive a temporary incapacity benefit.
description.fr: >
Vous avez subi un accident du travail ou êtes atteint dune
maladie professionnelle ?
Vos frais médicaux sont pris en charge à 100 %.
Pour compenser votre perte de salaire, vous pouvez percevoir des indemnités journalières.
Si vous êtes déclaré inapte suite à votre accident / maladie, vous pouvez recevoir une indemnité temporaire d'inaptitude.
résumé.en:
'[automatic] Provides comprehensive coverage for work-related illness
or injury.'
résumé.fr: Offre une couverture complète des maladies ou accidents du travail.
titre.en: '[automatic] work accidents and occupational diseases'
titre.fr: accidents du travail et maladies professionnelles
protection sociale . maladie . arrêt maladie:
description.en: >-
[automatic] If you are off work due to illness, you are entitled
@ -5432,6 +5414,9 @@ protection sociale . maladie . arrêt maladie . indépendant:
protection sociale . maladie . arrêt maladie . salarié:
titre.en: '[automatic] employee'
titre.fr: salarié
protection sociale . maladie . maternité paternité adoption:
titre.en: '[automatic] maternity and paternity leave benefits'
titre.fr: indemnités congé maternité paternité adoption
protection sociale . retraite:
description.en: >
[automatic] ### A mandatory system ...
@ -5544,9 +5529,9 @@ protection sociale . retraite . base:
This estimate of your retirement pension is calculated based on the following principles:
- Your earnings are calculated on the basis of your best 25 years
- The earnings calculated in the simulator correspond to your best 25 years
- You have contributed enough quarters and you leave at the age required to benefit from the full rate
- You have contributed enough quarters and you are leaving at the age required to benefit from the full rate
description.fr: >
Le montant de votre pension pour la retraite de base est calculé
à partir la moyenne de vos revenus des 25 meilleures années.
@ -5554,7 +5539,7 @@ protection sociale . retraite . base:
Cet estimation de votre pension de retraite est calculée en se basant sur les principes suivants :
- La rémunération calculée correspond à celle de vos 25 meilleures années
- La rémunération calculée dans le simulateur correspond à celle de vos 25 meilleures années
- Vous avez cotisé suffisement de trimestres et vous partez à l'âge requis pour bénéficier du taux plein
résumé.en: '[automatic] Full basic retirement pension assuming your earnings

View File

@ -191,6 +191,8 @@ Mois non concerné: Month not concerned
Mon entreprise: My company
Mon revenu: My income
Montant: Amount
Montant annuel: Yearly amount
Montant mensuel: Monthly amount
Montant de l'impôt sur les sociétés: Amount of corporate income tax
Montant de lexonération sociale liée à la crise sanitaire pour les cotisations de lannée 2021: Amount of the health crisis exemption for contributions in 2021
Montant de lexonération sociale liée à la crise sanitaire sur lannée 2021: Amount of the social exemption related to the health crisis in 2021

View File

@ -137,6 +137,8 @@ Modifier l'entreprise: Modifier l'entreprise
Modifier mes réponses: Modifier mes réponses
Mois non concerné: Mois non concerné
Mon entreprise: Mon entreprise
Montant annuel: Montant annuel
Montant mensuel: Montant mensuel
Montant de l'impôt sur les sociétés: Montant de l'impôt sur les sociétés
Montant de lexonération sociale liée à la crise sanitaire pour les cotisations de lannée 2021:
Montant de lexonération sociale liée à la crise sanitaire pour les

View File

@ -86,7 +86,7 @@ export default function Landing() {
>
<SimulateurCard {...simulators.salarié} />
<SimulateurCard {...simulators['auto-entrepreneur']} />
<SimulateurCard {...simulators['profession-libérale']} />
<SimulateurCard {...simulators['comparaison-statuts']} />
<Grid
item

View File

@ -0,0 +1,582 @@
import Engine, { PublicodesExpression } from 'publicodes'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import styled from 'styled-components'
import { DottedName } from '@/../../modele-social'
import { answerQuestion } from '@/actions/actions'
import Value from '@/components/EngineValue'
import { SwitchInput } from '@/components/conversation/ChoicesInput'
import { ExplicableRule } from '@/components/conversation/Explicable'
import RuleInput from '@/components/conversation/RuleInput'
import { useEngine } from '@/components/utils/EngineContext'
import { Message } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Drawer } from '@/design-system/drawer'
import { ArrowRightIcon, InfoIcon } from '@/design-system/icons'
import { Grid, Spacing } from '@/design-system/layout'
import { Tag, TagType } from '@/design-system/tag'
import { Tooltip } from '@/design-system/tooltip'
import { Strong } from '@/design-system/typography'
import { H1, H4, H5 } from '@/design-system/typography/heading'
import { Link, StyledLink } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { useCasParticuliers } from '../contexts/CasParticuliers'
import { StatusTagIcon } from './StatusCard'
const DOTTEDNAME_SOCIETE_IMPOT = 'entreprise . imposition'
const DOTTEDNAME_SOCIETE_VERSEMENT_LIBERATOIRE =
'dirigeant . auto-entrepreneur . impôt . versement libératoire'
const DOTTEDNAME_ACRE = 'dirigeant . exonérations . ACRE'
const AllerPlusLoinRevenus = ({
engines: [assimiléEngine, autoEntrepreneurEngine, indépendantEngine],
}: {
engines: [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
}) => {
const defaultValueImpot = useEngine().evaluate(
DOTTEDNAME_SOCIETE_IMPOT
).nodeValue
const defaultValueVersementLiberatoire = autoEntrepreneurEngine.evaluate(
DOTTEDNAME_SOCIETE_VERSEMENT_LIBERATOIRE
).nodeValue
const defaultValueACRE = assimiléEngine.evaluate(DOTTEDNAME_ACRE).nodeValue
const [impotValue, setImpotValue] = useState(
`'${String(defaultValueImpot)}'` || "'IS'"
)
const [versementLiberatoireValue, setVersementLiberatoireValue] = useState(
defaultValueVersementLiberatoire
)
const [acreValue, setAcreValue] = useState(defaultValueACRE)
const { isAutoEntrepreneurACREEnabled, setIsAutoEntrepreneurACREEnabled } =
useCasParticuliers()
const [AEAcreValue, setAEAcreValue] = useState<boolean | null>(null)
const { t } = useTranslation()
const dispatch = useDispatch()
return (
<Drawer
trigger={(buttonProps: { onClick: () => void }) => (
<Button color="secondary" light size="XS" {...buttonProps}>
<Trans>Aller plus loin</Trans> <StyledArrowRightIcon />
</Button>
)}
confirmLabel="Enregistrer les options"
onConfirm={() => {
dispatch(
answerQuestion(
DOTTEDNAME_SOCIETE_IMPOT,
impotValue as PublicodesExpression
)
)
const versementLibératoireValuePassed =
versementLiberatoireValue === null
? defaultValueVersementLiberatoire
: versementLiberatoireValue
dispatch(
answerQuestion(
DOTTEDNAME_SOCIETE_VERSEMENT_LIBERATOIRE,
versementLibératoireValuePassed ? 'oui' : 'non'
)
)
const acreValuePassed =
acreValue === null ? defaultValueACRE : acreValue
dispatch(
answerQuestion(DOTTEDNAME_ACRE, acreValuePassed ? 'oui' : 'non')
)
if (AEAcreValue !== null) {
setIsAutoEntrepreneurACREEnabled(AEAcreValue)
}
}}
onCancel={() => {
setAcreValue(null)
setVersementLiberatoireValue(null)
}}
>
<>
<H1>
<Trans>Aller plus loin sur les revenus</Trans>
</H1>
<H4
as="h2"
css={`
margin-bottom: 1rem;
`}
>
<Trans>Calculer vos revenus</Trans>
</H4>
<StyledTable>
<caption className="sr-only">
{t(
'comparateur.allerPlusLoin.tableCaption',
"Tableau affichant le détail du calcul du revenu net pour la SASU, l'entreprise individuelle (EI) et l'auto-entreprise (AE)."
)}
</caption>
<thead>
<tr>
<th className="sr-only">Type de structure</th>
<th scope="col">
<span className="table-title-sasu">
<StatusTagIcon status="sasu" /> SASU
</span>
</th>
<th scope="col">
<Tooltip
tooltip="Entreprise individuelle"
id="tooltip-ei-table"
>
<span className="table-title-ei">
<StatusTagIcon status="ei" /> EI
</span>
</Tooltip>
</th>
<th scope="col">
<Tooltip tooltip="Auto-entreprise" id="tooltip-ae-table">
<span className="table-title-ae">
<StatusTagIcon status="ae" /> AE
</span>
</Tooltip>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
<Minus
css={`
opacity: 0;
`}
aria-hidden
>
-
</Minus>{' '}
<Trans>Chiffre d'affaires</Trans>
</th>
<td colSpan={3}>
<StyledTag $color={'grey' as TagType}>
<Value
expression="entreprise . chiffre d'affaires"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus> <Trans>Charges</Trans>
</th>
<td colSpan={3}>
<StyledTag $color={'grey' as TagType}>
<Value
expression="entreprise . charges"
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus>{' '}
<Trans>Cotisations</Trans>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus> <Trans>Impôts</Trans>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">
<Minus
css={`
opacity: 0;
`}
aria-hidden
>
-
</Minus>{' '}
<StyledStrong>
<Trans>Revenu net</Trans>
</StyledStrong>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Strong>
<Value
expression="dirigeant . rémunération . net . après impôt"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Strong>
<Value
expression="dirigeant . rémunération . net . après impôt"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Strong>
<Value
expression="dirigeant . rémunération . net . après impôt"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
</tr>
</tfoot>
</StyledTable>
<Spacing md />
<Flex>
<H4 as="h2">Bénéficier de l'ACRE</H4>
<ExplicableRule
dottedName="dirigeant . exonérations . ACRE"
title="Bénéficier de l'ACRE"
/>
</Flex>
<Body>
L'aide à la création ou à la reprise d'une entreprise (Acre) consiste
en une <Strong>exonération partielle de charges sociales</Strong>,
dite exonération de début d'activité <Strong>pendant 12 mois</Strong>.
</Body>
{
// TODO : décommenter une fois le simulateur créé
<Button
href="https://entreprendre.service-public.fr/vosdroits/F23282"
aria-label={t('En savoir plus, nouvelle fenêtre')}
color="secondary"
light
size="XS"
>
<Trans>En savoir plus</Trans>
</Button>
}
<H5 as="h3">Choisir mon option de simulation</H5>
<div aria-live="polite">
<FlexCentered>
<SwitchInput
id="activation-acre"
onChange={(value: boolean) => setAcreValue(value)}
defaultSelected={defaultValueACRE as boolean}
/>
<Label htmlFor="activation-acre">
Activer l'ACRE dans la simulation
</Label>
</FlexCentered>
{(acreValue || defaultValueACRE) && (
<>
<Body>
Les{' '}
<StyledLink href="https://www.urssaf.fr/portail/home/independant/je-beneficie-dexonerations/accre/qui-peut-en-beneficier.html">
conditions d'accès
</StyledLink>{' '}
à l'ACRE sont plus restrictives pour les auto-entrepreneurs.
</Body>
<FlexCentered>
<SwitchInput
id="activation-acre-ae"
onChange={(value: boolean) => setAEAcreValue(value)}
defaultSelected={isAutoEntrepreneurACREEnabled}
/>
<Label htmlFor="activation-acre-ae">
Je suis éligible à l'ACRE pour mon auto-entreprise
</Label>
</FlexCentered>
</>
)}
</div>
<Spacing md />
<H4 as="h2">
Impôt sur le revenu, impôt sur les sociétés : que choisir ?
</H4>
<Body>
LEI et la SASU permettent de{' '}
<Strong>
choisir entre limposition sur les sociétés et sur le revenu
</Strong>{' '}
durant les 5 premières années. En auto-entreprise (AE), cest l
<Strong>impôt sur le revenu</Strong> qui est appliqué automatiquement
; dans certaines situations, vous pouvez aussi opter pour le{' '}
<Strong>
<Link href="https://www.impots.gouv.fr/professionnel/le-versement-liberatoire">
versement libératoire
</Link>
</Strong>
.
</Body>
<H5 as="h3">Choisir mon option de simulation (pour EI)</H5>
<Message type="secondary">
<Grid
container
css={`
flex-wrap: nowrap;
align-items: baseline;
`}
spacing={3}
>
<Grid item>
<InfoIcon
css={`
padding-top: 0.15rem;
display: inline-block;
`}
aria-label={t('Message à caractère informatif')}
/>
</Grid>
<Grid item>
<Body
css={`
font-size: 0.875rem;
`}
>
<Trans>
À ce jour, ce comparateur ne prend pas en compte le calcul de
l'impôt sur le revenu pour les SASU. La modification du
paramètre ci-dessous influera donc uniquement les calculs liés
au statut d'entreprise individuelle (EI).
</Trans>
</Body>
</Grid>
</Grid>
</Message>
<RuleInput
dottedName={DOTTEDNAME_SOCIETE_IMPOT}
onChange={(value: PublicodesExpression | undefined) => {
setImpotValue(String(value))
}}
key="imposition"
aria-labelledby="questionHeader"
engine={indépendantEngine}
/>
<H5 as="h3">
Choisir mon option de versement libératoire (pour AE){' '}
<ExplicableRule
dottedName={DOTTEDNAME_SOCIETE_VERSEMENT_LIBERATOIRE}
/>
</H5>
<FlexCentered>
<SwitchInput
id="versement-liberatoire"
onChange={setVersementLiberatoireValue}
defaultSelected={defaultValueVersementLiberatoire as boolean}
/>
<Label htmlFor="versement-liberatoire">
Activer le versement libératoire dans la simulation.
</Label>
</FlexCentered>
</>
</Drawer>
)
}
const StyledTag = styled(Tag)<{ $color: TagType }>`
width: 100%;
justify-content: center;
font-size: 0.75rem;
`
const Minus = styled.span`
color: ${({ theme }) => theme.colors.bases.secondary[500]};
margin-right: ${({ theme }) => theme.spacings.sm};
`
const StyledStrong = styled(Strong)`
font-family: ${({ theme }) => theme.fonts.main};
`
const Flex = styled.div`
display: flex;
align-items: baseline;
`
const FlexCentered = styled.div`
display: flex;
align-items: center;
`
const Label = styled.label`
margin-left: ${({ theme }) => theme.spacings.md};
font-family: ${({ theme }) => theme.fonts.main};
font-size: 1rem;
`
const StyledArrowRightIcon = styled(ArrowRightIcon)`
margin-left: ${({ theme }) => theme.spacings.sm};
`
const StyledTable = styled.table`
width: 100%;
text-align: left;
font-family: ${({ theme }) => theme.fonts.main};
border-collapse: separate;
border-spacing: 0.5rem;
border: transparent;
tr {
border-spacing: ${({ theme }) => theme.spacings.md}!important;
}
thead th {
text-align: center;
font-size: 0.75rem;
}
.table-title-sasu {
display: flex;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.colors.bases.secondary[600]};
svg {
fill: ${({ theme }) => theme.colors.bases.secondary[600]};
margin-right: ${({ theme }) => theme.spacings.xxs};
}
}
.table-title-ei {
display: flex;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.colors.publics.independant[600]};
svg {
fill: ${({ theme }) => theme.colors.publics.independant[600]};
margin-right: ${({ theme }) => theme.spacings.xxs};
}
}
.table-title-ae {
display: flex;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.colors.bases.tertiary[500]};
svg {
fill: ${({ theme }) => theme.colors.bases.tertiary[500]};
margin-right: ${({ theme }) => theme.spacings.xxs};
}
}
tbody th {
font-weight: normal;
}
tbody tr:last-of-type td {
padding-bottom: 0.5rem;
}
tfoot {
position: relative;
}
tfoot:after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background-color: ${({ theme }) => theme.colors.extended.grey[500]};
}
tfoot td,
tfoot th {
padding-top: 1rem;
}
`
export default AllerPlusLoinRevenus

View File

@ -0,0 +1,123 @@
import Engine from 'publicodes'
import { useTranslation } from 'react-i18next'
import { Route, Routes, redirect, useNavigate } from 'react-router-dom'
import { DottedName } from '@/../../modele-social'
import PeriodSwitch from '@/components/PeriodSwitch'
import Simulation, {
SimulationGoal,
SimulationGoals,
} from '@/components/Simulation'
import { Spacing } from '@/design-system/layout'
import Popover from '@/design-system/popover/Popover'
import Documentation from '@/pages/Documentation'
import { useSitePaths } from '@/sitePaths'
import Détails from './Détails'
import Résultats from './Résultats'
type ComparateurProps = {
engines: [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
}
function Comparateur({ engines }: ComparateurProps) {
const { t } = useTranslation()
const navigate = useNavigate()
const [assimiléEngine, autoEntrepreneurEngine, indépendantEngine] = engines
const { absoluteSitePaths } = useSitePaths()
return (
<>
<Simulation
engines={engines}
hideDetails
showQuestionsFromBeginning
fullWidth
id="simulation-comparateur"
>
<SimulationGoals
toggles={<PeriodSwitch />}
legend={'Estimations sur votre rémunération brute et vos charges'}
>
<SimulationGoal
dottedName="entreprise . chiffre d'affaires"
isInfoMode
label={t("Chiffre d'affaires estimé")}
/>
<SimulationGoal dottedName="entreprise . charges" isInfoMode />
</SimulationGoals>
</Simulation>
<Spacing md />
<Résultats engines={engines} />
<Détails engines={engines} />
<Routes>
<Route
path="SASU/*"
element={
<div>
<Popover
isOpen
isDismissable
onClose={() => {
navigate(absoluteSitePaths.simulateurs.comparaison, {
replace: true,
})
}}
>
<Documentation
engine={assimiléEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
/>
</Popover>
</div>
}
/>
<Route
path="EI/*"
element={
<div>
<Popover
isOpen
isDismissable
onClose={() => {
navigate(absoluteSitePaths.simulateurs.comparaison, {
replace: true,
})
}}
>
<Documentation
engine={indépendantEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/EI"
/>
</Popover>
</div>
}
/>
<Route
path="auto-entrepreneur/*"
element={
<div>
<Popover
isOpen
isDismissable
onClose={() => {
navigate(absoluteSitePaths.simulateurs.comparaison, {
replace: true,
})
}}
>
<Documentation
engine={autoEntrepreneurEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/auto-entrepreneur"
/>
</Popover>
</div>
}
/>
</Routes>
</>
)
}
export default Comparateur

View File

@ -0,0 +1,543 @@
import Engine, { formatValue } from 'publicodes'
import { ReactNode } from 'react'
import styled from 'styled-components'
import { DottedName } from '@/../../modele-social'
import Value, {
WhenApplicable,
WhenNotApplicable,
} from '@/components/EngineValue'
import RuleLink from '@/components/RuleLink'
import { HelpIcon } from '@/design-system/icons'
import { Grid } from '@/design-system/layout'
import { BestOption, getBestOption } from '../utils'
import StatusCard from './StatusCard'
const DetailsRowCards = ({
engines: [assimiléEngine, autoEntrepreneurEngine, indépendantEngine],
dottedName,
unit,
bestOption,
evolutionDottedName,
evolutionLabel,
footers,
label,
warnings,
}: {
engines: [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
dottedName: DottedName
unit?: string
bestOption?: 'sasu' | 'ei' | 'ae'
evolutionDottedName?: DottedName
evolutionLabel?: ReactNode | string
footers?: { sasu: ReactNode; ei: ReactNode; ae: ReactNode }
label?: ReactNode | string
warnings?: { sasu?: ReactNode; ei?: ReactNode; ae?: ReactNode }
}) => {
const assimiléEvaluation = assimiléEngine.evaluate({
valeur: dottedName,
...(unit && { unité: unit }),
})
const assimiléValue = formatValue(assimiléEvaluation, {
precision: 0,
}) as string
const indépendantEvaluation = indépendantEngine.evaluate({
valeur: dottedName,
...(unit && { unité: unit }),
})
const indépendantValue = formatValue(indépendantEvaluation, {
precision: 0,
}) as string
const autoEntrepreneurEvaluation = autoEntrepreneurEngine.evaluate({
valeur: dottedName,
...(unit && { unité: unit }),
})
const autoEntrepreneurValue = formatValue(autoEntrepreneurEvaluation, {
precision: 0,
}) as string
const options: BestOption[] = [
{
type: 'sasu',
value: assimiléEvaluation.nodeValue,
},
{
type: 'ei',
value: indépendantEvaluation.nodeValue,
},
{
type: 'ae',
value: autoEntrepreneurEvaluation.nodeValue,
},
]
const bestOptionValue = bestOption ?? getBestOption(options)
if (
assimiléValue === indépendantValue &&
indépendantValue === autoEntrepreneurValue
) {
return (
<Grid container spacing={4}>
<Grid item xs={12} lg={12}>
<StatusCard
status={['sasu', 'ei', 'ae']}
footerContent={footers?.sasu}
>
<WhenNotApplicable dottedName={dottedName} engine={assimiléEngine}>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable dottedName={dottedName} engine={assimiléEngine}>
<StyledDiv>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
dottedName={dottedName}
engine={assimiléEngine}
>
<HelpIcon />
</StyledRuleLink>
{warnings?.sasu && warnings?.sasu}
</StyledDiv>
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
</Grid>
)
}
if (assimiléValue === indépendantValue) {
return (
<Grid container spacing={4}>
<Grid item xs={12} lg={8}>
<StatusCard
status={['sasu', 'ei']}
isBestOption={bestOptionValue === 'sasu'}
footerContent={footers?.sasu}
>
<WhenNotApplicable dottedName={dottedName} engine={assimiléEngine}>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable dottedName={dottedName} engine={assimiléEngine}>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
dottedName={dottedName}
engine={assimiléEngine}
>
<HelpIcon />
</StyledRuleLink>
{warnings?.sasu || warnings?.ei
? warnings?.sasu
? warnings?.sasu
: warnings?.ei
: ''}
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard
status={['ae']}
footerContent={footers?.ei}
isBestOption={bestOptionValue === 'ae'}
>
<WhenNotApplicable
dottedName={dottedName}
engine={autoEntrepreneurEngine}
>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable
dottedName={dottedName}
engine={autoEntrepreneurEngine}
>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={autoEntrepreneurEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
documentationPath="/simulateurs/comparaison-régimes-sociaux/auto-entrepreneur"
dottedName={dottedName}
engine={autoEntrepreneurEngine}
>
<HelpIcon />
</StyledRuleLink>
{warnings?.ae && warnings?.ae}
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={autoEntrepreneurEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
</Grid>
)
}
if (indépendantValue === autoEntrepreneurValue) {
return (
<Grid container spacing={4}>
<Grid item xs={12} lg={4}>
<StatusCard
status={['sasu']}
footerContent={footers?.sasu}
isBestOption={bestOptionValue === 'sasu'}
>
<WhenNotApplicable dottedName={dottedName} engine={assimiléEngine}>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable dottedName={dottedName} engine={assimiléEngine}>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
dottedName={dottedName}
engine={assimiléEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
>
<HelpIcon />
</StyledRuleLink>
{warnings?.sasu && warnings?.sasu}
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
<Grid item xs={12} lg={8}>
<StatusCard
status={['ei', 'ae']}
footerContent={footers?.ei}
isBestOption={bestOptionValue === 'ei'}
>
<WhenNotApplicable
dottedName={dottedName}
engine={indépendantEngine}
>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable dottedName={dottedName} engine={indépendantEngine}>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={indépendantEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
dottedName={dottedName}
engine={indépendantEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/EI"
>
<HelpIcon />
</StyledRuleLink>
{warnings?.ei || warnings?.ae
? warnings?.ei
? warnings?.ei
: warnings?.ae
: ''}
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={indépendantEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
</Grid>
)
}
return (
<Grid container spacing={4}>
<Grid item xs={12} lg={4}>
<StatusCard
status={['sasu']}
footerContent={footers?.sasu}
isBestOption={bestOptionValue === 'sasu'}
>
<WhenNotApplicable dottedName={dottedName} engine={assimiléEngine}>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable dottedName={dottedName} engine={assimiléEngine}>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
dottedName={dottedName}
engine={assimiléEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
>
<HelpIcon />
</StyledRuleLink>
{warnings?.sasu && warnings?.sasu}
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={assimiléEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard
status={['ei']}
footerContent={footers?.ei}
isBestOption={bestOptionValue === 'ei'}
>
<WhenNotApplicable dottedName={dottedName} engine={indépendantEngine}>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable dottedName={dottedName} engine={indépendantEngine}>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={indépendantEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
dottedName={dottedName}
engine={indépendantEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/EI"
>
<HelpIcon />
</StyledRuleLink>
{warnings?.ei && warnings?.ei}
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={indépendantEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard
status={['ae']}
footerContent={footers?.ae}
isBestOption={bestOptionValue === 'ae'}
>
<WhenNotApplicable
dottedName={dottedName}
engine={autoEntrepreneurEngine}
>
<DisabledLabel>Ne s'applique pas</DisabledLabel>
</WhenNotApplicable>
<WhenApplicable
dottedName={dottedName}
engine={autoEntrepreneurEngine}
>
<span>
<Value
linkToRule={false}
expression={dottedName}
engine={autoEntrepreneurEngine}
precision={0}
unit={unit}
/>
{label && ' '}
{label && label}
</span>
<StyledRuleLink
dottedName={dottedName}
engine={autoEntrepreneurEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/auto-entrepreneur"
>
<HelpIcon />
</StyledRuleLink>
{warnings?.ae && warnings?.ae}
{evolutionDottedName && (
<Precisions>
<Value
linkToRule={false}
expression={evolutionDottedName}
engine={autoEntrepreneurEngine}
precision={0}
unit={unit}
/>{' '}
{evolutionLabel}
</Precisions>
)}
{!evolutionDottedName && evolutionLabel && (
<Precisions>{evolutionLabel}</Precisions>
)}
</WhenApplicable>
</StatusCard>
</Grid>
</Grid>
)
}
const StyledRuleLink = styled(RuleLink)`
display: inline-flex;
margin-left: ${({ theme }) => theme.spacings.xxs};
&:hover {
opacity: 0.8;
}
`
const DisabledLabel = styled.span`
color: ${({ theme }) => theme.colors.extended.grey[600]}!important;
font-size: 1.25rem;
font-weight: 700;
font-style: italic;
margin: 0 !important;
`
const Precisions = styled.span`
display: block;
font-family: ${({ theme }) => theme.fonts.main};
font-weight: normal;
font-size: 1rem;
color: ${({ theme }) => theme.colors.extended.grey[700]};
margin: 0 !important;
margin-top: 0.5rem;
width: 100%;
`
const StyledDiv = styled.div`
width: 100%;
display: flex;
align-items: center;
`
export default DetailsRowCards

View File

@ -0,0 +1,749 @@
import { Item } from '@react-stately/collections'
import Engine from 'publicodes'
import { Trans } from 'react-i18next'
import styled from 'styled-components'
import { DottedName } from '@/../../modele-social'
import Value, {
WhenAlreadyDefined,
WhenValueEquals,
} from '@/components/EngineValue'
import { ExplicableRule } from '@/components/conversation/Explicable'
import { Accordion } from '@/design-system'
import { Emoji } from '@/design-system/emoji'
import { ExternalLinkIcon, PlusCircleIcon } from '@/design-system/icons'
import { Container, Grid, Spacing } from '@/design-system/layout'
import { Strong } from '@/design-system/typography'
import { H2, H4 } from '@/design-system/typography/heading'
import { StyledLink } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import DetailsRowCards from './DetailsRowCards'
import ItemTitle from './ItemTitle'
import StatusCard from './StatusCard'
import WarningTooltip from './WarningTooltip'
const Détails = ({
engines: [assimiléEngine, autoEntrepreneurEngine, indépendantEngine],
}: {
engines: [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
}) => {
return (
<StyledContainer
backgroundColor={(theme) =>
theme.darkMode
? theme.colors.extended.dark[800]
: theme.colors.bases.primary[200]
}
>
<Accordion
variant="light"
title={
<H2>
<Trans>Zoom sur...</Trans>
</H2>
}
isFoldable
>
<Item
title={
<ItemTitle>
<Trans>La retraite</Trans> <Emoji emoji="🧐" />
</ItemTitle>
}
key="retraite"
hasChildItems={false}
>
<Body>
<Trans>
Le montant de votre retraite est constitué de{' '}
<Strong>
votre retraite de base + votre retraite complémentaire
</Strong>
.
</Trans>
</Body>
<StyledH4>
<Trans>Retraite de base</Trans>
<ExplicableRule dottedName="protection sociale . retraite . base" />
</StyledH4>
<Body>
<Trans>
La pension calculée correspond à celle de{' '}
<Strong>vos 25 meilleures années</Strong>, en considérant que vous
avez cotisé suffisamment de trimestres (4 trimestres par an) et
que vous partez en retraite à lâge requis pour obtenir un taux
plein.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . retraite . base"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/mois"
/>
<StyledH4>
<Trans>Retraite complémentaire</Trans>
<ExplicableRule dottedName="protection sociale . retraite . complémentaire" />
</StyledH4>
<Body>
<Trans>
Tous les ans, selon votre rémunération,{' '}
<Strong>
vous gagnez des points qui constituent votre pension de retraite
complémentaire
</Strong>
. En fin de carrière, vos points sont transformés en{' '}
<Strong>
un montant qui sajoute chaque mois à votre retraite de base
</Strong>
. Cette valeur se calcule sur le long terme. Par exemple, au bout
de 10 ans, vous auriez droit à :
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . retraite . complémentaire"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/mois"
evolutionLabel={<Trans>au bout de 10 ans</Trans>}
/>
</Item>
<Item
title={
<ItemTitle>
<Trans>La santé</Trans> <Emoji emoji="😷" />
</ItemTitle>
}
key="santé"
hasChildItems={false}
>
<Body
css={`
margin-bottom: 0;
`}
>
<Trans>
Tous les statuts vous ouvrent le droit au{' '}
<Strong>remboursement des soins.</Strong>
</Trans>
</Body>
<BodyNoMargin>
<Trans>
Pour tous les statuts, il est conseillé de souscrire à une{' '}
<Strong>prévoyance complémentaire (mutuelle)</Strong> pour
améliorer le remboursement des frais de santé.
</Trans>
</BodyNoMargin>
<StyledH4>
<Trans>Arrêt maladie</Trans>
<ExplicableRule dottedName="protection sociale . maladie . arrêt maladie" />
</StyledH4>
<Body>
<Trans>
Pour tous les statuts, vous aurez un{' '}
<Strong>délai de carence de 3 jours</Strong>. En cas darrêt
maladie, lassurance maladie vous versera :
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . maladie . arrêt maladie"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/jour"
warnings={{
sasu: (
<WhenValueEquals
engine={assimiléEngine}
expression="protection sociale . maladie . arrêt maladie"
value={0}
>
<WarningTooltip
tooltip={
<span
css={`
font-weight: normal;
`}
>
<Trans>
Votre <Strong>rémunération</Strong> est{' '}
<Strong>trop faible</Strong> pour bénéficier darrêt
maladie en SASU.
</Trans>
</span>
}
id="tooltip-sasu-arrêt-maladie"
/>
</WhenValueEquals>
),
}}
footers={{
sasu: (
<StyledDiv>
<PlusCircleIcon
css={`
margin-top: 0 !important;
`}
/>
<Body
css={`
margin: 0;
`}
>
<Trans>
Pour y prétendre, vous devez voir cotisé au moins{' '}
<Strong>3 mois</Strong>
</Trans>
</Body>
</StyledDiv>
),
ei: (
<StyledDiv>
<PlusCircleIcon
css={`
margin-top: 0 !important;
`}
/>
<Body
css={`
margin: 0;
`}
>
<Trans>
Pour y prétendre, vous devez voir cotisé au moins{' '}
<Strong>12 mois</Strong>
</Trans>
</Body>
</StyledDiv>
),
ae: (
<StyledDiv>
<PlusCircleIcon
css={`
margin-top: 0 !important;
`}
/>
<Body
css={`
margin: 0;
`}
>
<Trans>
Pour y prétendre, vous devez voir cotisé au moins{' '}
<Strong>12 mois</Strong>
</Trans>
</Body>
</StyledDiv>
),
}}
/>
<StyledH4>
<Trans>Accident du travail et maladie professionnelle</Trans>
<ExplicableRule dottedName="protection sociale . maladie . accidents du travail et maladies professionnelles . indemmnités" />
</StyledH4>
<Body>
<Trans>
En cas d<Strong>accident de travail</Strong>, de{' '}
<Strong>maladie professionnelle</Strong> ou dun{' '}
<Strong>accident sur le trajet domicile-travail</Strong>, vous
serez indemnisé(e) à hauteur de :
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . maladie . accidents du travail et maladies professionnelles . indemmnités"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/mois"
evolutionDottedName="protection sociale . maladie . accidents du travail et maladies professionnelles . indemmnités . à partir du 29ème jour"
evolutionLabel={<Trans>à partir du 29ème jour</Trans>}
/>
</Item>
<Item
title={
<ItemTitle>
<Trans>La maternité, paternité et adoption</Trans>{' '}
<Emoji emoji="🤗" />
</ItemTitle>
}
key="enfants"
hasChildItems={false}
>
<Body
css={`
margin-bottom: 0;
`}
>
<Trans>
Tous les statuts vous ouvrent le droit aux{' '}
<Strong>indemnités journalières</Strong> de congé maternité,
paternité, adoption.
</Trans>
</Body>
<Body
css={`
margin-top: 0;
`}
>
<Trans>
Pour y prétendre, vous devez avoir cotisé{' '}
<Strong>au moins 10 mois</Strong>.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . maladie . maternité paternité adoption"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/jour"
/>
<StyledH4>
<Trans>Maternité</Trans>
<ExplicableRule dottedName="protection sociale . maladie . maternité paternité adoption . allocation forfaitaire de repos maternel" />
</StyledH4>
<Body>
<Trans>
En plus des indemnités journalières, vous pouvez aussi prétendre à
une{' '}
<Strong>
allocation forfaitaire de repos maternel supplémentaire
</Strong>
.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . maladie . maternité paternité adoption . allocation forfaitaire de repos maternel"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
label={<Trans>versés en deux fois</Trans>}
/>
<StyledH4>
<Trans>Adoption</Trans>
<ExplicableRule dottedName="protection sociale . maladie . maternité paternité adoption . allocation forfaitaire de repos adoption" />
</StyledH4>
<Body>
<Trans>
En plus des indemnités journalières, vous pouvez aussi prétendre à
une{' '}
<Strong>
allocation forfaitaire de repos parental supplémentaire
</Strong>
.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . maladie . maternité paternité adoption . allocation forfaitaire de repos adoption"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
label={<Trans>versés en une fois</Trans>}
/>
</Item>
<Item
title={
<ItemTitle>
<Trans>L'invalidité et le décès</Trans> <Emoji emoji="🤕" />
</ItemTitle>
}
key="maladie"
hasChildItems={false}
>
<Body>
<Trans>
Tous les statuts cotisent pour une{' '}
<Strong>pension invalidité-décès</Strong> qui les{' '}
<Strong>protège en cas dinvalidité</Strong> et assure à leurs
proches une{' '}
<Strong>
pension de réversion et un capital en cas de décès
</Strong>
.
</Trans>
</Body>
<StyledH4>
<Trans>Invalidité</Trans>
<ExplicableRule dottedName="protection sociale . invalidité et décès" />
</StyledH4>
<BodyNoMargin>
<Trans>
Vous pouvez bénéficier dune pension invalidité{' '}
<Strong>
en cas de maladie ou daccident conduisant à une incapacité à
poursuivre votre activité professionnelle
</Strong>
.
</Trans>
</BodyNoMargin>
<BodyNoMargin
css={`
margin-bottom: 1rem;
`}
>
<Trans>
Pour y prétendre, vous devez respecter{' '}
<BlackColoredLink href="https://www.service-public.fr/particuliers/vosdroits/F672">
certaines règles
<StyledExternalLinkIcon />
</BlackColoredLink>
.
</Trans>
</BodyNoMargin>
<DetailsRowCards
dottedName="protection sociale . invalidité et décès . pension invalidité . invalidité partielle"
evolutionDottedName="protection sociale . invalidité et décès . pension invalidité . invalidité totale"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/mois"
label={
<span style={{ fontSize: '1rem' }}>
<Trans>(invalidité partielle)</Trans>
</span>
}
evolutionLabel={
<span style={{ fontSize: '0.75rem' }}>
<Trans>(invalidité totale)</Trans>
</span>
}
/>
<Spacing md />
<Body
css={`
margin-top: 2rem;
`}
>
<Trans>
Pour une invalidité causée par un accident professionnel, vous
pouvez bénéficier dune <Strong>rente dincapacité</Strong>.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . invalidité et décès . accidents du travail et maladies professionnelles . rente incapacité"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/mois"
/>
<StyledH4>
<Trans>Décès</Trans>
<ExplicableRule dottedName="protection sociale . invalidité et décès . capital décès" />
</StyledH4>
<Body>
<Trans>
La Sécurité Sociale garantit un{' '}
<Strong>capital décès pour vos ayants droits</Strong> (personnes
qui sont à votre charge) sous certaines conditions.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . invalidité et décès . capital décès"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
/>
<Body
css={`
margin-top: 2rem;
`}
>
<Trans>
En plus du capital décès, une{' '}
<Strong>pension de réversion</Strong> peut être versée au conjoint
survivant. Elle correspond aux{' '}
<Strong>droits à la retraite acquis par le défunt</Strong> durant
sa vie professionnelle.
</Trans>
</Body>
<StatusCard status={['sasu', 'ei', 'ae']}>
<span>
<Value
linkToRule={false}
expression="protection sociale . invalidité et décès . pension de reversion"
engine={assimiléEngine}
precision={0}
unit="€/mois"
/>{' '}
<WhenAlreadyDefined
engine={assimiléEngine}
dottedName="protection sociale . invalidité et décès . pension de reversion"
>
<Trans>maximum</Trans>
</WhenAlreadyDefined>
</span>
</StatusCard>
<Body
css={`
margin-top: 2rem;
`}
>
<Trans>
Pour un décès survenu dans le cadre dun accident professionnel,
vous pouvez bénéficier dune <Strong>rente de décès</Strong>.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . invalidité et décès . accidents du travail et maladies professionnelles . rente décès"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/mois"
/>
<Body
css={`
margin-top: 2rem;
`}
>
<Trans>
Un <Strong>capital « orphelin »</Strong> est versé aux enfants des
travailleurs indépendants décédés, sous certaines conditions.
</Trans>
</Body>
<DetailsRowCards
dottedName="protection sociale . invalidité et décès . capital décès . orphelin"
engines={[
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
]}
unit="€/enfant"
/>
</Item>
<Item
title={
<ItemTitle>
<Trans>La gestion juridique et comptable</Trans>{' '}
<Emoji emoji="🤓" />
</ItemTitle>
}
key="administratif"
hasChildItems={false}
>
{
// TODO : implémenter les valeurs correspondantes dans modèle-social
// Ressource : https://entreprendre.service-public.fr/vosdroits/F23282
/*
<StyledH4>
<Trans>Coût de création</Trans>
<ExplicableRule dottedName="protection sociale . maladie . arrêt maladie" />
</StyledH4>
<Body>
<Trans>
Les formalités de création d'une entreprise diffèrent selon les
statuts et la nature de l'activité. Le calcul se concentre ici sur
les <Strong>procédures obligatoires</Strong> (immatriculation,
annonces légales, rédaction des statuts...).
</Trans>
</Body>
<Grid container spacing={4}>
<Grid item xs={12} lg={4}>
<StatusCard status={['sasu']}>
<span>
<Value
linkToRule={false}
expression="protection sociale . maladie . arrêt maladie"
engine={assimiléEngine}
precision={0}
unit="€"
/>
</span>
<StyledRuleLink
dottedName="protection sociale . maladie . arrêt maladie"
engine={assimiléEngine}
>
<HelpIcon />
</StyledRuleLink>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard status={['ei']}>
<span>
<Value
linkToRule={false}
expression="protection sociale . maladie . arrêt maladie"
engine={indépendantEngine}
precision={0}
unit="€"
/>
</span>
<StyledRuleLink
dottedName="protection sociale . maladie . arrêt maladie"
engine={assimiléEngine}
>
<HelpIcon />
</StyledRuleLink>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard status={['ae']}>
<Trans>Aucun</Trans>
</StatusCard>
</Grid>
</Grid>
*/
}
<StyledH4>
<Trans>Dépôt de capital</Trans>
<ExplicableRule dottedName="entreprise . capital social" />
</StyledH4>
<Body>
<Trans>
Selon les statuts, il est indispensable deffectuer un{' '}
<Strong>apport en capital</Strong> à la création de lentreprise.
Le <Strong>montant minimum</Strong> du capital social est de{' '}
<Strong>1 </Strong>.
</Trans>
</Body>
<Grid container spacing={4}>
<Grid item xs={12} lg={4}>
<StatusCard status={['sasu']}>
<Trans>1 minimum</Trans>
</StatusCard>
</Grid>
<Grid item xs={12} lg={8}>
<StatusCard status={['ei', 'ae']}>
<DisabledLabel>
<Trans>Aucun</Trans>
</DisabledLabel>
</StatusCard>
</Grid>
</Grid>
<StyledH4>
<Trans>Statut du conjoint</Trans>
<ExplicableRule dottedName="protection sociale . maladie . arrêt maladie" />
</StyledH4>
<Body>
<Trans>
Vous êtes marié(e), pacsé(e) ou en union libre avec un chef
dentreprise : il existe <Strong>3 statuts possibles</Strong> pour
vous (<Strong>conjoint collaborateur</Strong>,{' '}
<Strong>conjoint associé</Strong> ou{' '}
<Strong>conjoint salarié</Strong>).
</Trans>
</Body>
<Grid container spacing={4}>
<Grid item xs={12} lg={4}>
<StatusCard status={['sasu']}>
<Trans>Conjoint associé ou salarié</Trans>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard status={['ei']}>
<Trans>Conjoint collaborateur ou salarié</Trans>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard status={['ae']}>
<Trans>Conjoint collaborateur</Trans>
</StatusCard>
</Grid>
</Grid>
</Item>
</Accordion>
</StyledContainer>
)
}
const StyledContainer = styled(Container)`
padding: ${({ theme }) => theme.spacings.lg};
`
const StyledH4 = styled(H4)`
font-size: 1.25rem;
color: ${({ theme }) => theme.colors.bases.primary[600]};
`
// TODO : décommenter une fois l'implémentation du calcul des coûts de créations
// ajouté à modèle-social
/*
const StyledRuleLink = styled(RuleLink)`
display: inline-flex;
margin-left: ${({ theme }) => theme.spacings.xxs};
&:hover {
opacity: 0.8;
}
`
const Precisions = styled.span`
display: block;
font-family: ${({ theme }) => theme.fonts.main};
font-weight: normal;
font-size: 1rem;
color: ${({ theme }) => theme.colors.extended.grey[700]};
margin: 0;
margin-top: 0.5rem;
width: 100%;
`
*/
const StyledDiv = styled.div`
display: flex;
svg {
width: 2.5rem;
margin-right: 1rem;
margin-top: 1rem;
}
`
const BodyNoMargin = styled(Body)`
margin: 0;
`
const StyledExternalLinkIcon = styled(ExternalLinkIcon)`
margin-left: 0.25rem;
`
const BlackColoredLink = styled(StyledLink)`
color: ${({ theme }) => theme.colors.extended.grey[800]};
`
const DisabledLabel = styled(Body)`
color: ${({ theme }) => theme.colors.extended.grey[600]}!important;
font-size: 1.25rem;
font-weight: 700;
font-style: italic;
margin: 0;
`
export default Détails

View File

@ -0,0 +1,39 @@
import { ReactNode } from 'react'
import styled from 'styled-components'
import { CircledArrowIcon } from '@/design-system/icons'
import { H3 } from '@/design-system/typography/heading'
const ItemTitle = ({ children }: { children: ReactNode }) => {
return (
<StyledH3>
<StyledCircledArrowIcon /> {children}
</StyledH3>
)
}
const StyledCircledArrowIcon = styled(CircledArrowIcon)`
flex-shrink: 0;
width: 2.5rem;
@media (max-width: ${({ theme }) => theme.breakpointsWidth.md}) {
width: 1.5rem;
}
`
const StyledH3 = styled(H3)`
display: inline-flex;
align-items: center;
justify-content: flex-start;
font-size: 1.625rem;
margin: 0;
gap: 1rem;
text-align: left;
@media (max-width: ${({ theme }) => theme.breakpointsWidth.md}) {
font-size: 1.25rem;
}
@media (max-width: ${({ theme }) => theme.breakpointsWidth.sm}) {
font-size: 1rem;
}
`
export default ItemTitle

View File

@ -0,0 +1,35 @@
import { Item } from '@react-stately/collections'
import { Trans } from 'react-i18next'
import { ExplicableRule } from '@/components/conversation/Explicable'
import { Emoji } from '@/design-system/emoji'
import { Strong } from '@/design-system/typography'
import { H4 } from '@/design-system/typography/heading'
import { Body } from '@/design-system/typography/paragraphs'
import ItemTitle from './ItemTitle'
const RetraiteItem = () => {
return (
<Item
title={
<ItemTitle>
La retraite <Emoji emoji="🧐" />
</ItemTitle>
}
key="retraite"
hasChildItems={false}
>
<H4>
<Trans>Retraite de base</Trans>
<ExplicableRule dottedName="protection sociale . retraite . base" />
</H4>
<Body>
Le montant de votre retraite est constitué de{' '}
<Strong>votre retraite de base + votre retraite complémentaire</Strong>.
</Body>
</Item>
)
}
export default RetraiteItem

View File

@ -0,0 +1,317 @@
import Engine from 'publicodes'
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { DottedName } from '@/../../modele-social'
import Value, { Condition, WhenAlreadyDefined } from '@/components/EngineValue'
import RuleLink from '@/components/RuleLink'
import { CheckList } from '@/design-system'
import { ExternalLinkIcon, HelpIcon } from '@/design-system/icons'
import { Grid } from '@/design-system/layout'
import { H2 } from '@/design-system/typography/heading'
import { StyledLink } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { BestOption, getBestOption } from '../utils'
import AllerPlusLoinRevenus from './AllerPlusLoinRevenus'
import StatusCard from './StatusCard'
import WarningTooltip from './WarningTooltip'
const RevenuAprèsImpot = ({
engines,
}: {
engines: [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
}) => {
const [assimiléEngine, autoEntrepreneurEngine, indépendantEngine] = engines
const { t } = useTranslation()
const assimiléValue = assimiléEngine.evaluate({
valeur: 'dirigeant . rémunération . net . après impôt',
unité: '€/mois',
}).nodeValue
const indépendantValue = indépendantEngine.evaluate({
valeur: 'dirigeant . rémunération . net . après impôt',
unité: '€/mois',
}).nodeValue
const autoEntrepreneurValue = autoEntrepreneurEngine.evaluate({
valeur: 'dirigeant . rémunération . net . après impôt',
unité: '€/mois',
}).nodeValue
const options: BestOption[] = [
{
type: 'sasu',
value: assimiléValue,
},
{
type: 'ei',
value: indépendantValue,
},
{
type: 'ae',
value: autoEntrepreneurValue,
},
]
const bestOption = getBestOption(options)
return (
<>
<H2>
<Trans>Revenu après impôt</Trans>
</H2>
<Grid container spacing={4}>
<Grid item xs={12} lg={4}>
<StatusCard
status={['sasu']}
isBestOption={bestOption === 'sasu'}
footerContent={
<CheckList
items={[
{
isChecked: assimiléEngine.evaluate({
valeur: 'dirigeant . exonérations . ACRE',
}).nodeValue as boolean,
label: assimiléEngine.evaluate({
valeur: 'dirigeant . exonérations . ACRE',
}).nodeValue
? t("Tient compte de l'ACRE")
: t("Ne prend pas l'ACRE en compte"),
},
{
isChecked: true,
label: t("Choix d'imposition : impôt sur les sociétés"),
},
]}
/>
}
>
<span>
<Value
linkToRule={false}
expression="dirigeant . rémunération . net . après impôt"
engine={assimiléEngine}
precision={0}
unit="€/mois"
/>{' '}
<Condition
engine={assimiléEngine}
expression="dirigeant . exonérations . ACRE"
>
<WhenAlreadyDefined
dottedName="dirigeant . rémunération . net . après impôt"
engine={assimiléEngine}
>
<Trans>la première année</Trans>
</WhenAlreadyDefined>
</Condition>
</span>
<StyledRuleLink
dottedName="dirigeant . rémunération . net . après impôt"
engine={assimiléEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
>
<HelpIcon />
</StyledRuleLink>
</StatusCard>
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard
status={['ei']}
isBestOption={bestOption === 'ei'}
footerContent={
<CheckList
items={[
{
isChecked: indépendantEngine.evaluate({
valeur: 'dirigeant . exonérations . ACRE',
}).nodeValue as boolean,
label: indépendantEngine.evaluate({
valeur: 'dirigeant . exonérations . ACRE',
}).nodeValue
? t("Tient compte de l'ACRE")
: t("Ne prend pas l'ACRE en compte"),
},
{
isChecked: true,
label: t(
`Choix d'imposition : impôt sur ${
indépendantEngine.evaluate({
valeur: 'entreprise . imposition',
}).nodeValue === 'IS'
? 'les sociétés'
: 'le revenu'
}`
),
},
]}
/>
}
>
<span>
<Value
linkToRule={false}
expression="dirigeant . rémunération . net . après impôt"
engine={indépendantEngine}
precision={0}
unit="€/mois"
/>{' '}
<Condition
engine={indépendantEngine}
expression="dirigeant . exonérations . ACRE"
>
<WhenAlreadyDefined
dottedName="dirigeant . rémunération . net . après impôt"
engine={indépendantEngine}
>
<Trans>la première année</Trans>
</WhenAlreadyDefined>
</Condition>
</span>
<StyledRuleLink
dottedName="dirigeant . rémunération . net . après impôt"
engine={indépendantEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/EI"
>
<HelpIcon />
</StyledRuleLink>
</StatusCard>{' '}
</Grid>
<Grid item xs={12} lg={4}>
<StatusCard
status={['ae']}
isBestOption={bestOption === 'ae'}
footerContent={
<CheckList
items={[
{
isChecked: autoEntrepreneurEngine.evaluate({
valeur: 'dirigeant . exonérations . ACRE',
}).nodeValue as boolean,
label: (
<Trans i18nKey="revenu_après_impots.acre">
<span>
ACRE sous{' '}
<BlackColoredLink href="https://www.urssaf.fr/portail/home/independant/je-beneficie-dexonerations/accre.html">
certaines conditions
<StyledExternalLinkIcon />
</BlackColoredLink>
</span>
</Trans>
),
},
{
isChecked: autoEntrepreneurEngine.evaluate({
valeur:
'dirigeant . auto-entrepreneur . impôt . versement libératoire',
}).nodeValue as boolean,
label: t("Versement libératoire de l'impôt sur le revenu"),
},
]}
/>
}
>
<Value
linkToRule={false}
expression="dirigeant . rémunération . net . après impôt"
engine={autoEntrepreneurEngine}
precision={0}
unit="€/mois"
/>
<Condition
engine={autoEntrepreneurEngine}
expression="dirigeant . exonérations . ACRE"
>
<WhenAlreadyDefined
dottedName="dirigeant . rémunération . net . après impôt"
engine={assimiléEngine}
>
<span
css={`
margin-left: 0.25rem;
`}
>
<Trans>la première année</Trans>
</span>
</WhenAlreadyDefined>
</Condition>
<StyledRuleLink
dottedName="dirigeant . rémunération . net . après impôt"
engine={autoEntrepreneurEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/auto-entrepreneur"
>
<HelpIcon />
</StyledRuleLink>
<Condition
engine={autoEntrepreneurEngine}
expression="entreprise . chiffre d'affaires . seuil micro . dépassé"
>
<WarningTooltip
tooltip={
<StyledBody id="warning-ae-tooltip">
<Trans>
Vous allez dépasser le plafond de la micro-entreprise
</Trans>{' '}
<span>
(
<Value
linkToRule={false}
displayedUnit="€"
expression={
String(
autoEntrepreneurEngine.evaluate(
'entreprise . activité . nature'
).nodeValue
) === 'libérale'
? "entreprise . chiffre d'affaires . seuil micro . service"
: "entreprise . chiffre d'affaires . seuil micro . total"
}
/>{' '}
<Trans>de chiffre daffaires</Trans>).
</span>
</StyledBody>
}
id="tooltip-ae"
/>
</Condition>
</StatusCard>
</Grid>
</Grid>
<DivAlignRight>
<AllerPlusLoinRevenus engines={engines} />
</DivAlignRight>
</>
)
}
export default RevenuAprèsImpot
const StyledRuleLink = styled(RuleLink)`
display: inline-flex;
margin-left: 0.15rem;
&:hover {
opacity: 0.8;
}
`
const StyledExternalLinkIcon = styled(ExternalLinkIcon)`
margin-left: 0.25rem;
`
const BlackColoredLink = styled(StyledLink)`
color: ${({ theme }) => theme.colors.extended.grey[800]};
`
const DivAlignRight = styled.div`
margin-top: ${({ theme }) => theme.spacings.lg};
text-align: right;
`
const StyledBody = styled(Body)`
color: ${({ theme }) => theme.colors.extended.grey[100]}!important;
`

View File

@ -0,0 +1,122 @@
import { Trans } from 'react-i18next'
import styled from 'styled-components'
import Value from '@/components/EngineValue'
import { CardContainer } from '@/design-system/card/Card'
import { EditIcon } from '@/design-system/icons'
import { Grid } from '@/design-system/layout'
import { StyledLink } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { useGetFullURL } from '@/hooks/useGetFullURL'
const RevenuEstimé = () => {
const fullURL = useGetFullURL()
return (
<CardContainer
css={`
padding: 1.5rem !important;
`}
$inert
>
<Grid container>
<Grid
css={`
padding-right: 1.5rem;
`}
item
xs={12}
sm={6}
lg={3}
>
<Label>
<Trans>Votre chiffre d'affaires estimé</Trans>
</Label>
<StyledValue
linkToRule={false}
expression="entreprise . chiffre d'affaires"
/>
</Grid>
<StyledGrid item xs={12} sm={6} lg={5}>
<Label>
<Trans>Vos charges estimées</Trans>
</Label>
<StyledValue
linkToRule={false}
unit="€/an"
expression="entreprise . charges"
/>
</StyledGrid>
<GridEditLink item xs={12} lg={3}>
<StyledA
as={StyledLink}
href={`${fullURL}#simulation-comparateur`}
$noUnderline
css={`
display: inline-flex;
align-items: center;
`}
>
<StyledEditIcon /> Modifier les informations
</StyledA>
</GridEditLink>
</Grid>
</CardContainer>
)
}
const Label = styled(Body)`
margin: 0;
color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[200]
: theme.colors.extended.grey[600]}!important;
font-size: 0.875rem;
`
const StyledValue = styled(Value)`
margin: 0;
color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[100]
: theme.colors.bases.primary[700]}!important;
font-size: 1.25rem;
font-weight: 700;
font-family: ${({ theme }) => theme.fonts.main};
`
const StyledGrid = styled(Grid)`
border-left: 1px solid ${({ theme }) => theme.colors.extended.grey[400]};
padding-left: 1.5rem;
@media (max-width: ${({ theme }) => theme.breakpointsWidth.sm}) {
border-left: none;
padding-left: 0;
margin-top: ${({ theme }) => theme.spacings.md};
}
`
const StyledEditIcon = styled(EditIcon)`
margin-right: ${({ theme }) => theme.spacings.xxs};
fill: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[100]
: theme.colors.bases.primary[700]}!important;
`
const GridEditLink = styled(Grid)`
justify-content: flex-end;
align-items: center;
display: flex;
@media (max-width: ${({ theme }) => theme.breakpointsWidth.lg}) {
padding-top: ${({ theme }) => theme.spacings.lg};
justify-content: center;
}
`
const StyledA = styled.a`
text-decoration: none;
`
export default RevenuEstimé

View File

@ -0,0 +1,33 @@
import Engine from 'publicodes'
import styled from 'styled-components'
import { DottedName } from '@/../../modele-social'
import { Container } from '@/design-system/layout'
import RevenuAprèsImpot from './RevenuAprèsImpot'
import RevenuEstimé from './RevenuEstimé'
const Résultats = ({
engines,
}: {
engines: [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
}) => {
return (
<StyledContainer
backgroundColor={(theme) =>
theme.darkMode
? theme.colors.extended.dark[700]
: theme.colors.bases.primary[200]
}
>
<RevenuEstimé />
<RevenuAprèsImpot engines={engines} />
</StyledContainer>
)
}
export default Résultats
const StyledContainer = styled(Container)`
padding: ${({ theme }) => theme.spacings.lg};
`

View File

@ -0,0 +1,160 @@
import { ReactNode, useRef } from 'react'
import { Trans } from 'react-i18next'
import styled from 'styled-components'
import { CardContainer } from '@/design-system/card/Card'
import { Emoji } from '@/design-system/emoji'
import { CircleIcon, HexagonIcon, TriangleIcon } from '@/design-system/icons'
import { Grid } from '@/design-system/layout'
import { Tag, TagType } from '@/design-system/tag'
import { Tooltip } from '@/design-system/tooltip'
import { Body } from '@/design-system/typography/paragraphs'
import { generateUuid } from '@/utils'
type StatusCardType = {
status: ('sasu' | 'ei' | 'ae')[]
footerContent?: ReactNode
isBestOption?: boolean
children: ReactNode
}
const STATUS_DATA = {
sasu: {
color: 'secondary',
label: 'Société (SASU)',
},
ei: {
color: 'independant',
label: 'Entreprise individuelle (EI)',
},
ae: {
color: 'tertiary',
label: 'Auto-entrepreneur',
},
}
const StatusCard = ({
status,
children,
footerContent,
isBestOption,
}: StatusCardType) => {
const tooltipIdRef = useRef(generateUuid())
return (
<StyledCardContainer $inert>
<CardBody>
<Grid container spacing={1}>
{status.map((statusString) => (
<Grid item key={statusString}>
<StyledTag
key={statusString}
$color={STATUS_DATA[statusString].color as TagType}
$size="sm"
>
<StatusTagIcon
style={{ marginRight: '0.25rem' }}
status={statusString}
/>
{STATUS_DATA[statusString].label}
</StyledTag>
</Grid>
))}
</Grid>
<StyledBody>{children}</StyledBody>
</CardBody>
{isBestOption && (
<Tooltip
tooltip={
<StyledBodyTooltip
css={`
font-weight: normal;
`}
>
<Trans>Option la plus avantageuse.</Trans>
</StyledBodyTooltip>
}
id={`tooltip-option-avantageuse-${String(tooltipIdRef.current)}`}
>
<StyledEmoji
emoji="🥇"
aria-describedby={`tooltip-option-avantageuse-${String(
tooltipIdRef.current
)}`}
/>
</Tooltip>
)}
{footerContent && <CardFooter>{footerContent}</CardFooter>}
</StyledCardContainer>
)
}
export default StatusCard
const StyledCardContainer = styled(CardContainer)`
position: relative;
align-items: flex-start;
padding: 0;
`
const StyledTag = styled(Tag)`
display: inline-flex;
&:not(:last-child) {
margin-right: 0.5rem;
}
`
const StyledEmoji = styled(Emoji)`
position: absolute;
top: 0;
right: 1.5rem;
font-size: 1.5rem;
`
const StyledBody = styled(Body)`
font-size: 1.25rem;
display: flex;
flex-wrap: wrap;
align-items: center;
font-weight: 700;
margin: 0;
margin-top: 0.75rem;
`
const CardBody = styled.div`
padding: 1.5rem;
width: 100%;
`
const CardFooter = styled.div`
width: 100%;
border-top: 1px solid ${({ theme }) => theme.colors.extended.grey[300]};
padding: 1.5rem;
`
const StyledBodyTooltip = styled(Body)`
color: ${({ theme }) => theme.colors.extended.grey[100]}!important;
font-size: 0.75rem;
margin: 0;
`
export const StatusTagIcon = ({
status,
...props
}: {
status: 'sasu' | 'ei' | 'ae'
style?: { marginRight: string }
}) => {
switch (true) {
case status.includes('sasu'):
return <HexagonIcon {...props} />
case status.includes('ei'):
return <TriangleIcon {...props} />
case status.includes('ae'):
return <CircleIcon {...props} />
default:
return null
}
}

View File

@ -0,0 +1,51 @@
import Engine from 'publicodes'
import { ComponentProps } from 'react'
import { DottedName } from '@/../../modele-social'
import Value from '@/components/EngineValue'
import { H3 } from '@/design-system/typography/heading'
function TableRow({
dottedName,
engines: [assimiléEngine, autoEntrepreneurEngine, indépendantEngine],
precision,
unit,
}: {
dottedName: DottedName
engines: readonly [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
} & Pick<ComponentProps<typeof Value>, 'precision' | 'unit'>) {
return (
<>
<H3 className="legend">{assimiléEngine.getRule(dottedName).title}</H3>
<div className="AS">
<Value
engine={assimiléEngine}
expression={dottedName}
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
precision={precision}
unit={unit}
/>
</div>
<div className="indep">
<Value
engine={indépendantEngine}
expression={dottedName}
documentationPath="/simulateurs/comparaison-régimes-sociaux/EI"
precision={precision}
unit={unit}
/>
</div>
<div className="auto">
<Value
engine={autoEntrepreneurEngine}
expression={dottedName}
documentationPath="/simulateurs/comparaison-régimes-sociaux/auto-entrepreneur"
precision={precision}
unit={unit}
/>
</div>
</>
)
}
export default TableRow

View File

@ -0,0 +1,29 @@
import { ReactNode } from 'react'
import styled from 'styled-components'
import { WarningIcon } from '@/design-system/icons'
import { Tooltip } from '@/design-system/tooltip'
const WarningTooltip = ({
id,
tooltip,
}: {
id: string
tooltip: ReactNode
}) => {
return (
<Tooltip tooltip={tooltip} id={id}>
<StyledWarningIcon
id={id}
aria-label="Attention"
aria-describedby={`${id}-description`}
/>
</Tooltip>
)
}
export default WarningTooltip
const StyledWarningIcon = styled(WarningIcon)`
margin-left: 0.5rem;
`

View File

@ -0,0 +1,40 @@
import {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useContext,
useState,
} from 'react'
type CasParticuliersType = {
isAutoEntrepreneurACREEnabled: boolean
setIsAutoEntrepreneurACREEnabled: Dispatch<SetStateAction<boolean>>
}
const CasParticuliersContext = createContext<CasParticuliersType>({
isAutoEntrepreneurACREEnabled: false,
setIsAutoEntrepreneurACREEnabled: () => null,
})
export const CasParticuliersProvider = ({
children,
}: {
children: ReactNode
}) => {
const [isAutoEntrepreneurACREEnabled, setIsAutoEntrepreneurACREEnabled] =
useState(false)
return (
<CasParticuliersContext.Provider
value={{
isAutoEntrepreneurACREEnabled,
setIsAutoEntrepreneurACREEnabled,
}}
>
{children}
</CasParticuliersContext.Provider>
)
}
export const useCasParticuliers = () => useContext(CasParticuliersContext)

View File

@ -0,0 +1,99 @@
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import { useMemo } from 'react'
import { Trans } from 'react-i18next'
import { useEngine, useRawSituation } from '@/components/utils/EngineContext'
import useSimulationConfig from '@/components/utils/useSimulationConfig'
import { Strong } from '@/design-system/typography'
import { Intro } from '@/design-system/typography/paragraphs'
import { useSitePaths } from '@/sitePaths'
import { configComparateurStatuts } from '../configs/comparateurStatuts'
import Comparateur from './components/Comparateur'
import {
CasParticuliersProvider,
useCasParticuliers,
} from './contexts/CasParticuliers'
function ComparateurStatutsUI() {
const engine = useEngine()
const situation = useRawSituation()
const { isAutoEntrepreneurACREEnabled } = useCasParticuliers()
const { absoluteSitePaths } = useSitePaths()
useSimulationConfig({
path: absoluteSitePaths.simulateurs.comparaison,
config: configComparateurStatuts,
})
const assimiléEngine = useMemo(
() =>
engine.shallowCopy().setSituation({
...situation,
'entreprise . imposition': "'IS'",
'entreprise . catégorie juridique': "'SAS'",
'entreprise . catégorie juridique . SAS . unipersonnelle': 'oui',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[situation]
)
const autoEntrepreneurEngine = useMemo(
() =>
engine.shallowCopy().setSituation({
...situation,
'entreprise . catégorie juridique': "'EI'",
'entreprise . catégorie juridique . EI . auto-entrepreneur': 'oui',
...(isAutoEntrepreneurACREEnabled
? { 'dirigeant . exonérations . ACRE': 'oui' }
: { 'dirigeant . exonérations . ACRE': 'non' }),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[situation, isAutoEntrepreneurACREEnabled]
)
const indépendantEngine = useMemo(
() =>
engine.shallowCopy().setSituation({
...situation,
'entreprise . imposition':
situation['entreprise . imposition'] ?? "'IS'",
'entreprise . catégorie juridique': "'EI'",
'entreprise . catégorie juridique . EI . auto-entrepreneur': 'non',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[situation]
)
const engines = [
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
] as [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
return (
<>
<Intro>
<Trans i18nKey="comparaisonRégimes.description">
Lorsque vous créez votre société, le choix du statut juridique va{' '}
<Strong>
déterminer à quel régime social le dirigeant est affilié
</Strong>
. Il en existe <Strong>trois différents</Strong>, avec chacun ses
avantages et inconvénients. Avec ce comparatif, trouvez celui qui vous
correspond le mieux.
</Trans>
</Intro>
<Comparateur engines={engines} />
</>
)
}
export default function ComparateurStatuts() {
return (
<CasParticuliersProvider>
<ComparateurStatutsUI />
</CasParticuliersProvider>
)
}

View File

@ -0,0 +1,29 @@
export type ValueType =
| string
| number
| boolean
| null
| Record<string, unknown>
export type BestOption = {
type: 'sasu' | 'ei' | 'ae'
value?: ValueType
}
export const getBestOption = (options: BestOption[]) => {
const sortedOptions = options.sort(
(option1: BestOption, option2: BestOption) => {
if (option1.value === null || option1.value === undefined) {
return 1
}
if (option2.value === null || option2.value === undefined) {
return -1
}
if (option1.value === option2.value) return 0
// console.log(option1.value, option2.value, option1.value > option2.value)
return option1.value > option2.value ? -1 : 1
}
)
return sortedOptions?.[0]?.type
}

View File

@ -4,7 +4,7 @@ import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Button } from '@/design-system/buttons'
import HelpButton from '@/design-system/buttons/HelpButton'
import HelpButtonWithPopover from '@/design-system/buttons/HelpButtonWithPopover'
import { CardContainer } from '@/design-system/card/Card'
import { Emoji } from '@/design-system/emoji'
import { Checkbox } from '@/design-system/field'
@ -88,9 +88,9 @@ export const ActiviteCard = ({
)}
<ActiviteContent>
<H4 as="h3">{titre}</H4>
<HelpButton title={titre} type="aide">
<HelpButtonWithPopover title={titre} type="aide">
<Body>{explication}</Body>
</HelpButton>
</HelpButtonWithPopover>
<SmallBody
css={`
flex: 1;

View File

@ -1,235 +0,0 @@
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import { ComponentProps, useMemo } from 'react'
import { Trans } from 'react-i18next'
import { Route, Routes } from 'react-router-dom'
import Value from '@/components/EngineValue'
import PeriodSwitch from '@/components/PeriodSwitch'
import { StyledGrid } from '@/components/SchemeComparaison'
import Simulation, {
SimulationGoal,
SimulationGoals,
} from '@/components/Simulation'
import { useEngine, useRawSituation } from '@/components/utils/EngineContext'
import useSimulationConfig from '@/components/utils/useSimulationConfig'
import { Emoji } from '@/design-system/emoji'
import { Spacing } from '@/design-system/layout'
import { H2, H3 } from '@/design-system/typography/heading'
import { Intro } from '@/design-system/typography/paragraphs'
import { useSitePaths } from '@/sitePaths'
import Documentation from '../Documentation'
import { configComparateurStatuts } from './configs/comparateurStatuts'
export default function SchemeComparaisonPage() {
const engine = useEngine()
const situation = useRawSituation()
const { absoluteSitePaths } = useSitePaths()
useSimulationConfig({
path: absoluteSitePaths.simulateurs.comparaison,
config: configComparateurStatuts,
})
const assimiléEngine = useMemo(
() =>
engine.shallowCopy().setSituation({
...situation,
'entreprise . catégorie juridique': "'SAS'",
'entreprise . catégorie juridique . SAS . unipersonnelle': 'oui',
}),
[situation]
)
const autoEntrepreneurEngine = useMemo(
() =>
engine.shallowCopy().setSituation({
...situation,
'entreprise . catégorie juridique': "'EI'",
'entreprise . catégorie juridique . EI . auto-entrepreneur': 'oui',
}),
[situation]
)
const indépendantEngine = useMemo(
() =>
engine.shallowCopy().setSituation({
...situation,
'entreprise . catégorie juridique': "'EI'",
'entreprise . catégorie juridique . EI . auto-entrepreneur': 'non',
}),
[situation]
)
const engines = [
assimiléEngine,
autoEntrepreneurEngine,
indépendantEngine,
] as [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
return (
<Routes>
<Route
path="SASU/*"
element={
<>
<Documentation
engine={assimiléEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
/>
</>
}
/>
<Route
path="EI/*"
element={
<Documentation
engine={indépendantEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/EI"
/>
}
/>
<Route
path="auto-entrepreneur/*"
element={
<Documentation
engine={autoEntrepreneurEngine}
documentationPath="/simulateurs/comparaison-régimes-sociaux/auto-entrepreneur"
/>
}
/>
<Route
path=""
element={
<>
<Intro>
<Trans i18nKey="comparaisonRégimes.description">
Lorsque vous créez votre société, le choix du statut juridique
va déterminer à quel régime social le dirigeant est affilié. Il
en existe trois différents, avec chacun ses avantages et
inconvénients. Avec ce comparatif, trouvez celui qui vous
correspond le mieux.
</Trans>
</Intro>
<Comparateur engines={engines} />
</>
}
/>
</Routes>
)
}
type ComparateurProps = {
engines: [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
}
function Comparateur({ engines }: ComparateurProps) {
return (
<>
<Simulation engines={engines} hideDetails showQuestionsFromBeginning>
<SimulationGoals
toggles={<PeriodSwitch />}
legend={'Estimations sur votre rémunération brute et vos charges'}
>
<SimulationGoal dottedName="entreprise . chiffre d'affaires" />
<SimulationGoal dottedName="entreprise . charges" />
</SimulationGoals>
</Simulation>
<Spacing md />
<StyledGrid>
<H3 className="AS">
<Emoji emoji="☂" /> <Trans>SASU</Trans>
</H3>
<H3 className="indep">
<Emoji emoji="👩‍🔧" /> <Trans>EI / EURL</Trans>
</H3>
<H3 className="auto">
<Emoji emoji="🚶‍♂️" /> <Trans>Auto-entrepreneur</Trans>
</H3>
<TableRow
dottedName="dirigeant . rémunération . net . après impôt"
unit="€/mois"
precision={0}
engines={engines}
/>
<H2 className="all">
<Spacing lg /> Retraite
</H2>
<TableRow
dottedName="protection sociale . retraite . trimestres"
engines={engines}
/>
<TableRow
dottedName="protection sociale . retraite . base"
engines={engines}
/>
<TableRow
dottedName="protection sociale . retraite . complémentaire"
engines={engines}
/>
<H2 className="all">
<Spacing lg /> Santé
</H2>
<TableRow
dottedName="protection sociale . maladie . arrêt maladie"
precision={0}
engines={engines}
/>
<TableRow
dottedName="protection sociale . maladie . arrêt maladie . délai d'attente"
engines={engines}
/>
<TableRow
dottedName="protection sociale . maladie . arrêt maladie . délai de carence"
engines={engines}
/>
</StyledGrid>
</>
)
}
function TableRow({
dottedName,
engines: [assimiléEngine, autoEntrepreneurEngine, indépendantEngine],
precision,
unit,
}: {
dottedName: DottedName
engines: readonly [Engine<DottedName>, Engine<DottedName>, Engine<DottedName>]
} & Pick<ComponentProps<typeof Value>, 'precision' | 'unit'>) {
return (
<>
<H3 className="legend">{assimiléEngine.getRule(dottedName).title}</H3>
<div className="AS">
<Value
engine={assimiléEngine}
expression={dottedName}
documentationPath="/simulateurs/comparaison-régimes-sociaux/SASU"
precision={precision}
unit={unit}
/>
</div>
<div className="indep">
<Value
engine={indépendantEngine}
expression={dottedName}
documentationPath="/simulateurs/comparaison-régimes-sociaux/EI"
precision={precision}
unit={unit}
/>
</div>
<div className="auto">
<Value
engine={autoEntrepreneurEngine}
expression={dottedName}
documentationPath="/simulateurs/comparaison-régimes-sociaux/auto-entrepreneur"
precision={precision}
unit={unit}
/>
</div>
</>
)
}

View File

@ -38,5 +38,6 @@ export const configComparateurStatuts: SimulationConfig = {
"entreprise . chiffre d'affaires": '4000 €/mois',
'entreprise . charges': '1000 €/mois',
'entreprise . date de création': "période . début d'année",
'dirigeant . exonérations . ACRE': 'non',
},
}

View File

@ -21,6 +21,7 @@ import FormulaireMobilitéIndépendant from '../gerer/demande-mobilité'
import ArtisteAuteur from './ArtisteAuteur'
import AutoEntrepreneur from './AutoEntrepreneur'
import ChômagePartielComponent from './ChômagePartiel'
import SchemeComparaisonPage from './ComparateurStatuts'
import DividendesSimulation from './Dividendes'
import ÉconomieCollaborative from './EconomieCollaborative'
import ExonérationCovid from './ExonerationCovid'
@ -32,7 +33,6 @@ import IndépendantSimulation, {
import PAMCHome from './PAMCHome'
import { SASUSimulation } from './SASU'
import SalariéSimulation from './Salarié'
import SchemeComparaisonPage from './SchemeComparaison'
import { configAutoEntrepreneur } from './configs/autoEntrepreneur'
import { configChômagePartiel } from './configs/chômagePartiel'
import { configSASU } from './configs/dirigeantSASU'

View File

@ -1,7 +1,7 @@
import { Trans } from 'react-i18next'
import styled from 'styled-components'
import HelpButton from '@/design-system/buttons/HelpButton'
import HelpButtonWithPopover from '@/design-system/buttons/HelpButtonWithPopover'
import { Li, Ul } from '@/design-system/typography/list'
import { Body, baseParagraphStyle } from '@/design-system/typography/paragraphs'
@ -36,7 +36,11 @@ const suramortissementHeader = 'suramortissementHeader'
export function ExplicationsResultatFiscal() {
return (
<HelpButton title="Quelles exonérations inclure ?" type="aide" bigPopover>
<HelpButtonWithPopover
title="Quelles exonérations inclure ?"
type="aide"
bigPopover
>
<Body>
Pour calculer le montant du résultat fiscal avant déduction des
exonérations et des charges sociales à indiquer dans ce simulateur, vous
@ -207,6 +211,6 @@ export function ExplicationsResultatFiscal() {
</tr>
</tbody>
</StyledTable>
</HelpButton>
</HelpButtonWithPopover>
)
}

View File

@ -240,3 +240,7 @@ export const catchDivideByZeroError = <T>(func: () => T) => {
throw err
}
}
export const generateUuid = () => {
return Math.floor(Math.random() * Date.now()).toString(16)
}

View File

@ -3188,6 +3188,22 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.0.5":
version: 1.1.0
resolution: "@floating-ui/core@npm:1.1.0"
checksum: ac48969915247320e52d173480c224e2ded94d557ba4cc504547bb314d126348dcc0aeef05686673e1b289596e6ce15118edc84900dd310c613d805f83b4e27d
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.0.4":
version: 1.1.0
resolution: "@floating-ui/dom@npm:1.1.0"
dependencies:
"@floating-ui/core": ^1.0.5
checksum: 717551da6f470101cd1de0edc449b229fade7f94c2ff98d09e14ced041e27092aac94bd78756c4247a42b57129f187292f145f0001a81ece399a89b20b4be60b
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:1.13.0":
version: 1.13.0
resolution: "@formatjs/ecma402-abstract@npm:1.13.0"
@ -11905,7 +11921,7 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:^2.2.5":
"classnames@npm:^2.2.5, classnames@npm:^2.3.2":
version: 2.3.2
resolution: "classnames@npm:2.3.2"
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e
@ -24676,6 +24692,19 @@ __metadata:
languageName: node
linkType: hard
"react-tooltip@npm:^5.4.0":
version: 5.4.0
resolution: "react-tooltip@npm:5.4.0"
dependencies:
"@floating-ui/dom": ^1.0.4
classnames: ^2.3.2
peerDependencies:
react: ">=18.0.0"
react-dom: ">=18.0.0"
checksum: d6947e849a2d89ae9dca6211b9750217ffcd76778a9a66dbfaf745420d1412512a86343a36c81136d3122b368f90899a150fcc94107f9e68ba89ffbe8b426e6e
languageName: node
linkType: hard
"react-transition-group@npm:2.9.0":
version: 2.9.0
resolution: "react-transition-group@npm:2.9.0"
@ -26231,6 +26260,7 @@ __metadata:
react-router-dom: ^6.4.4
react-signature-pad-wrapper: ^3.3.1
react-spring: ^9.5.5
react-tooltip: ^5.4.0
react-use-measure: ^2.1.1
recharts: 2.3.2
reduce-reducers: ^1.0.4