diff --git a/modele-social/CHANGELOG.md b/modele-social/CHANGELOG.md index 70bc904d8..ec35427d6 100644 --- a/modele-social/CHANGELOG.md +++ b/modele-social/CHANGELOG.md @@ -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 d’attente en cad d’arrê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 d’invalidité + + +### 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 : l’espace 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) diff --git a/modele-social/règles/dirigeant.yaml b/modele-social/règles/dirigeant.yaml index 4d3b3784b..0ad8cac98 100644 --- a/modele-social/règles/dirigeant.yaml +++ b/modele-social/règles/dirigeant.yaml @@ -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 diff --git a/modele-social/règles/entreprise/entreprise.yaml b/modele-social/règles/entreprise/entreprise.yaml index f79a2452a..6426c0b1e 100644 --- a/modele-social/règles/entreprise/entreprise.yaml +++ b/modele-social/règles/entreprise/entreprise.yaml @@ -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 somme des montants des ventes réalisées + pendant votre exercice comptable (un an) : CA = prix de vente x quantités + vendues. variations: - si: dirigeant . auto-entrepreneur alors: dirigeant . auto-entrepreneur . chiffre d'affaires diff --git a/modele-social/règles/entreprise/imposition.yaml b/modele-social/règles/entreprise/imposition.yaml index 9a143cc8e..0dd8186db 100644 --- a/modele-social/règles/entreprise/imposition.yaml +++ b/modele-social/règles/entreprise/imposition.yaml @@ -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 diff --git a/modele-social/règles/protection-sociale.yaml b/modele-social/règles/protection-sociale.yaml index 239432222..0b121a19c 100644 --- a/modele-social/règles/protection-sociale.yaml +++ b/modele-social/règles/protection-sociale.yaml @@ -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 d’activité 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 d’un délai d’affiliation 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 à l’entreprise 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 d’activité 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 d’activité 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 l’enfant : 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é d’adoption : 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 d’affiliation à 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 l’accouchement + ## Paternité / Adoption + Pour en bénéficier, vous devez justifier de 10 mois d’affiliation à la naissance / à l’adoption. + 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 l’enfant : 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 d’activité de 8 semaines. La totalité du montant de l’allocation est versée après l’accouchement 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: + 'L’allocation 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 d’invalidité': 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 d’invalidité': 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 qu’une seule pension d’invalidité : soit au titre de votre activité salariée, soit au titre de votre activité d’artisan/commerçant. Toutefois, le montant de la pension tient compte de tous vos revenus perçus qu’ils 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 d’invalidité ': 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 l’assuré 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: | - L’assurance 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 à l’anné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 d’AT/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 d’une 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: 👩‍🎓 diff --git a/site/cypress/integration/mon-entreprise/english/prerender.ts b/site/cypress/integration/mon-entreprise/english/prerender.ts index 7690178a5..b110b0440 100644 --- a/site/cypress/integration/mon-entreprise/english/prerender.ts +++ b/site/cypress/integration/mon-entreprise/english/prerender.ts @@ -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') }, }, diff --git a/site/cypress/integration/mon-entreprise/simulateur-ae.ts b/site/cypress/integration/mon-entreprise/simulateur-ae.ts index 911593902..0079b8fcd 100644 --- a/site/cypress/integration/mon-entreprise/simulateur-ae.ts +++ b/site/cypress/integration/mon-entreprise/simulateur-ae.ts @@ -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) => { diff --git a/site/cypress/support/simulateur.js b/site/cypress/support/simulateur.js index a652da1e9..14555af35 100644 --- a/site/cypress/support/simulateur.js +++ b/site/cypress/support/simulateur.js @@ -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 () { diff --git a/site/package.json b/site/package.json index 88d3dd451..56603de2f 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/source/components/EngineValue.tsx b/site/source/components/EngineValue.tsx index f30c92730..d02432ed4 100644 --- a/site/source/components/EngineValue.tsx +++ b/site/source/components/EngineValue.tsx @@ -109,10 +109,16 @@ const StyledValue = styled.span<{ $flashOnChange: boolean }>` type ConditionProps = { expression: PublicodesExpression | ASTNode children: React.ReactNode + engine?: Engine } -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 }) { - 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 }) { - 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 }) { - 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 } diff --git a/site/source/components/PeriodSwitch.tsx b/site/source/components/PeriodSwitch.tsx index f317eb222..7db6b75ab 100644 --- a/site/source/components/PeriodSwitch.tsx +++ b/site/source/components/PeriodSwitch.tsx @@ -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() { dispatch(updateUnit(unit))} + mode="tab" + hideRadio > {periods.map(({ label, unit }) => ( } & Omit, '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( ` ${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)` diff --git a/site/source/components/Simulation/SimulationGoal.tsx b/site/source/components/Simulation/SimulationGoal.tsx index 71af1b383..5091ceb4b 100644 --- a/site/source/components/Simulation/SimulationGoal.tsx +++ b/site/source/components/Simulation/SimulationGoal.tsx @@ -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({ > - - {label} - + {isInfoMode ? ( + + + + {label || rule.title} + + + + + + + + + ) : ( + + {label} + + )} {rule.rawNode.résumé && ( theme.colors.extended.grey[100]}; + margin: 0; +` diff --git a/site/source/components/Simulation/SimulationGoals.tsx b/site/source/components/Simulation/SimulationGoals.tsx index 64fdcacc8..8db7ac5a6 100644 --- a/site/source/components/Simulation/SimulationGoals.tsx +++ b/site/source/components/Simulation/SimulationGoals.tsx @@ -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 ( - +
+ - - -
- {legend} -
- {children} -
-
+ + +
+ {legend} +
+ + + + Les données de simulations se mettront automatiquement à jour + après la modification d'un champ. + + + + {children} +
+
+
) } -const StyledSimulationGoals = styled.div< +export const SimulationGoalsContainer = styled.div< Pick & { 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; diff --git a/site/source/components/Simulation/index.tsx b/site/source/components/Simulation/index.tsx index 186f5ccb0..f6a5f82b3 100644 --- a/site/source/components/Simulation/index.tsx +++ b/site/source/components/Simulation/index.tsx @@ -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} > - + - - - Les données de simulations se mettront automatiquement à jour - après la modification d'un champ. - - {children} {(firstStepCompleted || showQuestionsFromBeginning) && ( diff --git a/site/source/components/conversation/ChoicesInput.tsx b/site/source/components/conversation/ChoicesInput.tsx index 07d246898..7a7c84424 100644 --- a/site/source/components/conversation/ChoicesInput.tsx +++ b/site/source/components/conversation/ChoicesInput.tsx @@ -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({ return ( <> - {choice.children.map((node) => ( - - {' '} - {hiddenOptions.includes( - node.dottedName as DottedName - ) ? null : 'children' in node ? ( -
-

- {node.title} -

- - - - -
- ) : ( - - { + return ( + + {' '} + {hiddenOptions.includes( + node.dottedName as DottedName + ) ? null : 'children' in node ? ( +
- {node.title}{' '} - {node.rawNode.icônes && } - {' '} - {type !== 'toggle' && ( - - )} - - )} - - ))} +

+ {node.title} +

+ + + + +
+ ) : ( + + + {node.title}{' '} + {node.rawNode.icônes && } + {' '} + {type !== 'toggle' && ( + + )} + + )} +
+ ) + })} {choice.canGiveUp && ( <> {t('Aucun')} @@ -334,3 +344,25 @@ export function useSelection({ 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 ( + onChange && onChange(isSelected)} + light + id={id} + key={key} + > + {label} + + ) +} diff --git a/site/source/components/conversation/Conversation.tsx b/site/source/components/conversation/Conversation.tsx index f3add979b..d39680e16 100644 --- a/site/source/components/conversation/Conversation.tsx +++ b/site/source/components/conversation/Conversation.tsx @@ -132,7 +132,7 @@ export default function Conversation({ {previousAnswers.length > 0 && ( - @@ -141,7 +141,8 @@ export default function Conversation({ + + {label ?? ( + <> + Modifier mes réponses + + )} + )} > {(close) => {children}} @@ -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} + } +` diff --git a/site/source/contexts/DarkModeContext.tsx b/site/source/contexts/DarkModeContext.tsx index 27ef90812..7b0afdb18 100644 --- a/site/source/contexts/DarkModeContext.tsx +++ b/site/source/contexts/DarkModeContext.tsx @@ -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 ( ({ ...theme, darkMode: - forceTheme === undefined ? theme.darkMode : forceTheme === 'dark', + forceTheme === undefined || forceTheme === 'default' + ? darkMode + : forceTheme === 'dark', })} > {children} diff --git a/site/source/design-system/accordion/index.tsx b/site/source/design-system/accordion/index.tsx index 4a57696b8..9697aa4eb 100644 --- a/site/source/design-system/accordion/index.tsx +++ b/site/source/design-system/accordion/index.tsx @@ -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 = (props: AriaAccordionProps) => { +const SAVE_STATE_LOCALSTORAGE_KEY = 'accordion-state' + +export const Accordion = ( + props: AriaAccordionProps & { + variant?: 'light' + shouldToggleAll?: boolean + title?: ReactNode + isFoldable?: boolean + shouldSaveState?: boolean + } +) => { + const { title, isFoldable, shouldSaveState } = props const state = useTreeState(props) const ref = useRef(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 ( - - {[...state.collection].map((item) => ( - key={item.key} item={item} state={state} /> - ))} - + <> + {title && ( + + {title} + {isFoldable && ( + + (allItemsOpen ? closeAll() : openAll())} + > + + {allItemsOpen ? 'Tout plier' : 'Tout déplier'} + + + )} + + )} + + {[...state.collection].map((item) => { + return ( + + key={item.key} + item={item} + state={state} + $variant={props?.variant} + /> + ) + })} + + ) } -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 { item: Node state: TreeState + $variant?: 'light' } function AccordionItem(props: AccordionItemProps) { const ref = useRef(null) - const { state, item } = props + const { state, item, $variant } = props const { buttonProps, regionProps } = useAccordionItem(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(props: AccordionItemProps) { return ( x.stopPropagation()}> - + {item.props.title} {/* @ts-ignore: https://github.com/pmndrs/react-spring/issues/1515 */} - @@ -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 })` `} ` -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 diff --git a/site/source/design-system/buttons/Button.tsx b/site/source/design-system/buttons/Button.tsx index 843155833..c5b98236e 100644 --- a/site/source/design-system/buttons/Button.tsx +++ b/site/source/design-system/buttons/Button.tsx @@ -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 ( ` @@ -143,7 +148,7 @@ export const StyledButton = styled.button` ${$color === 'secondary' && css` border-color: ${theme.colors.bases[$color][500]}; - `} + `}; `} @media not print { @@ -207,4 +212,30 @@ export const StyledButton = styled.button` `} } } + + ${({ $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} + } + `} ` diff --git a/site/source/design-system/buttons/HelpButton.tsx b/site/source/design-system/buttons/HelpButtonWithPopover.tsx similarity index 99% rename from site/source/design-system/buttons/HelpButton.tsx rename to site/source/design-system/buttons/HelpButtonWithPopover.tsx index 469b11510..a1cc3f06e 100644 --- a/site/source/design-system/buttons/HelpButton.tsx +++ b/site/source/design-system/buttons/HelpButtonWithPopover.tsx @@ -15,7 +15,7 @@ type HelpButtonProps = { className?: string } -export default function HelpButton({ +export default function HelpButtonWithPopover({ children, title, type, diff --git a/site/source/design-system/buttons/index.stories.tsx b/site/source/design-system/buttons/index.stories.tsx index b2a001601..8df09f7ba 100644 --- a/site/source/design-system/buttons/index.stories.tsx +++ b/site/source/design-system/buttons/index.stories.tsx @@ -29,4 +29,4 @@ Secondary.args = { children: 'Secondary XS button', } -export { CloseButton, HelpButton } from '@/design-system/buttons' +export { CloseButton, HelpButtonWithPopover } from '@/design-system/buttons' diff --git a/site/source/design-system/buttons/index.ts b/site/source/design-system/buttons/index.ts index 9c259c889..d35d99421 100644 --- a/site/source/design-system/buttons/index.ts +++ b/site/source/design-system/buttons/index.ts @@ -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' diff --git a/site/source/design-system/card/Card.tsx b/site/source/design-system/card/Card.tsx index 2575e2c33..bd25a47d0 100644 --- a/site/source/design-system/card/Card.tsx +++ b/site/source/design-system/card/Card.tsx @@ -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 diff --git a/site/source/design-system/checklist/index.stories.tsx b/site/source/design-system/checklist/index.stories.tsx new file mode 100644 index 000000000..a20c9d73d --- /dev/null +++ b/site/source/design-system/checklist/index.stories.tsx @@ -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 + +export const CheckListWithData: ComponentStory = () => ( + +) diff --git a/site/source/design-system/checklist/index.tsx b/site/source/design-system/checklist/index.tsx new file mode 100644 index 000000000..80cc51e89 --- /dev/null +++ b/site/source/design-system/checklist/index.tsx @@ -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 ( + + {items.map((item, index) => { + const { isChecked, label } = item + + return ( + + {isChecked ? : } + {label} + + ) + })} + + ) +} + +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; + } +` diff --git a/site/source/design-system/drawer/Drawer.tsx b/site/source/design-system/drawer/Drawer.tsx new file mode 100644 index 000000000..62e23f43d --- /dev/null +++ b/site/source/design-system/drawer/Drawer.tsx @@ -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( + + + { + closeDrawer() + handleDeactivate() + }, + }} + > + + {isDismissable && ( + + {/* TODO : replace with Link when in design system */} + closeDrawer()}> + Fermer + + + + + + + )} + + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { closeDrawer } as { + closeDrawer: () => void + }) + } + })} + + + {onConfirm && ( + + + + + + + + + + + )} + + + , + 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; +` diff --git a/site/source/design-system/drawer/index.stories.tsx b/site/source/design-system/drawer/index.stories.tsx new file mode 100644 index 000000000..a4a99e3aa --- /dev/null +++ b/site/source/design-system/drawer/index.stories.tsx @@ -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 + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template: ComponentStory = (args) => + +export const Basic = Template.bind({}) +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Basic.args = { + children:
Coucou, je suis ouvert !
, + trigger: (buttonProps: DrawerButtonProps) => ( + + ), + isDismissable: true, + // eslint-disable-next-line no-console + onConfirm: () => console.log('onConfirm appelé !'), + confirmLabel: 'Sauvegarder', + cancelLabel: 'Annuler', +} diff --git a/site/source/design-system/drawer/index.tsx b/site/source/design-system/drawer/index.tsx new file mode 100644 index 000000000..6bcbde96c --- /dev/null +++ b/site/source/design-system/drawer/index.tsx @@ -0,0 +1 @@ +export { Drawer } from './Drawer' diff --git a/site/source/design-system/field/Radio/Radio.tsx b/site/source/design-system/field/Radio/Radio.tsx index 436c300a1..0697c1971 100644 --- a/site/source/design-system/field/Radio/Radio.tsx +++ b/site/source/design-system/field/Radio/Radio.tsx @@ -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` diff --git a/site/source/design-system/field/Radio/ToggleGroup.tsx b/site/source/design-system/field/Radio/ToggleGroup.tsx index 162e6fb92..cdca713ed 100644 --- a/site/source/design-system/field/Radio/ToggleGroup.tsx +++ b/site/source/design-system/field/Radio/ToggleGroup.tsx @@ -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 && {label}} - + {children} ) } -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 { diff --git a/site/source/design-system/icons/index.stories.tsx b/site/source/design-system/icons/index.stories.tsx index a27cd0970..e00d76a80 100644 --- a/site/source/design-system/icons/index.stories.tsx +++ b/site/source/design-system/icons/index.stories.tsx @@ -15,4 +15,8 @@ export { ReturnIcon, SearchIcon, SuccessIcon, + EditIcon, + HexagonIcon, + TriangleIcon, + CircleIcon, } from '@/design-system/icons' diff --git a/site/source/design-system/icons/index.tsx b/site/source/design-system/icons/index.tsx index 9930b2494..22e832ec8 100644 --- a/site/source/design-system/icons/index.tsx +++ b/site/source/design-system/icons/index.tsx @@ -151,3 +151,257 @@ export const ReturnIcon = (props: HTMLAttributes) => ( /> ) + +export const EditIcon = (props: HTMLAttributes) => ( + + + +) + +export const HexagonIcon = (props: HTMLAttributes) => ( + + + +) + +export const TriangleIcon = (props: HTMLAttributes) => ( + + + +) + +export const CircleIcon = (props: HTMLAttributes) => ( + + + +) + +export const HelpIcon = (props: HTMLAttributes) => ( + + + +) + +export const CheckmarkIcon = (props: HTMLAttributes) => ( + + + +) + +export const CrossIcon = (props: HTMLAttributes) => ( + + + + +) + +export const ExternalLinkIcon = (props: HTMLAttributes) => ( + + + + +) + +export const CircledArrowIcon = (props: HTMLAttributes) => ( + + + + + +) + +export const PlusCircleIcon = (props: HTMLAttributes) => ( + + + + + +) + +export const ArrowRightIcon = (props: HTMLAttributes) => ( + + + + +) + +export const WarningIcon = (props: HTMLAttributes) => ( + + + +) diff --git a/site/source/design-system/index.ts b/site/source/design-system/index.ts index 2929aa127..41471237e 100644 --- a/site/source/design-system/index.ts +++ b/site/source/design-system/index.ts @@ -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' diff --git a/site/source/design-system/layout/Container.tsx b/site/source/design-system/layout/Container.tsx index 5f99deab4..81d3f95f8 100644 --- a/site/source/design-system/layout/Container.tsx +++ b/site/source/design-system/layout/Container.tsx @@ -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 ( - + {children} diff --git a/site/source/design-system/popover/Popover.tsx b/site/source/design-system/popover/Popover.tsx index 7e68213ae..1aae416b4 100644 --- a/site/source/design-system/popover/Popover.tsx +++ b/site/source/design-system/popover/Popover.tsx @@ -127,13 +127,9 @@ export default function Popover( )} - {/* tabIndex -1 is for text selection in popover, see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 */} + - {title && ( -

- {title} -

- )} + {title &&

{title}

} {children}
@@ -162,7 +158,7 @@ const Underlay = styled.div` 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; diff --git a/site/source/design-system/popover/PopoverConfirm.tsx b/site/source/design-system/popover/PopoverConfirm.tsx index 1be192f66..be4cadee5 100644 --- a/site/source/design-system/popover/PopoverConfirm.tsx +++ b/site/source/design-system/popover/PopoverConfirm.tsx @@ -35,9 +35,10 @@ export default function PopoverConfirm({ return ( {(closePopover) => ( - -

{title}

- {children} +
+ {title &&

{title}

} + +
{children}
@@ -56,7 +57,7 @@ export default function PopoverConfirm({ - +
)}
) @@ -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}; -` diff --git a/site/source/design-system/tag/index.tsx b/site/source/design-system/tag/index.tsx index 70cdec28d..3efc35aa4 100644 --- a/site/source/design-system/tag/index.tsx +++ b/site/source/design-system/tag/index.tsx @@ -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]}; + } ` diff --git a/site/source/design-system/theme.ts b/site/source/design-system/theme.ts index 37bdc7a2e..709b7667e 100644 --- a/site/source/design-system/theme.ts +++ b/site/source/design-system/theme.ts @@ -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 + +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 diff --git a/site/source/design-system/tooltip/Tooltip.tsx b/site/source/design-system/tooltip/Tooltip.tsx new file mode 100644 index 000000000..af5d4c787 --- /dev/null +++ b/site/source/design-system/tooltip/Tooltip.tsx @@ -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 ( + + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { id } as { + id: string + }) + } + })} + + {tooltip} + + + ) +} + +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; + } +` diff --git a/site/source/design-system/tooltip/index.stories.tsx b/site/source/design-system/tooltip/index.stories.tsx new file mode 100644 index 000000000..6aade290f --- /dev/null +++ b/site/source/design-system/tooltip/index.stories.tsx @@ -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 + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template: ComponentStory = (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', +} diff --git a/site/source/design-system/tooltip/index.ts b/site/source/design-system/tooltip/index.ts new file mode 100644 index 000000000..1a0b4fb06 --- /dev/null +++ b/site/source/design-system/tooltip/index.ts @@ -0,0 +1 @@ +export { Tooltip } from './Tooltip' diff --git a/site/source/hooks/useSaveAndRestoreScrollPosition.ts b/site/source/hooks/useSaveAndRestoreScrollPosition.ts index 68e530bc5..386badde1 100644 --- a/site/source/hooks/useSaveAndRestoreScrollPosition.ts +++ b/site/source/hooks/useSaveAndRestoreScrollPosition.ts @@ -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]) diff --git a/site/source/locales/rules-en.yaml b/site/source/locales/rules-en.yaml index 037e0caaf..fc026a045 100644 --- a/site/source/locales/rules-en.yaml +++ b/site/source/locales/rules-en.yaml @@ -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: > - L’assurance 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 à l’anné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 d’AT/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 d’une + 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 diff --git a/site/source/locales/ui-en.yaml b/site/source/locales/ui-en.yaml index da6b74525..63095aa5c 100644 --- a/site/source/locales/ui-en.yaml +++ b/site/source/locales/ui-en.yaml @@ -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 l’exonération sociale liée à la crise sanitaire pour les cotisations de l’année 2021: Amount of the health crisis exemption for contributions in 2021 Montant de l’exonération sociale liée à la crise sanitaire sur l’année 2021: Amount of the social exemption related to the health crisis in 2021 diff --git a/site/source/locales/ui-fr.yaml b/site/source/locales/ui-fr.yaml index aaa13df0a..ffed9e5c6 100644 --- a/site/source/locales/ui-fr.yaml +++ b/site/source/locales/ui-fr.yaml @@ -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 l’exonération sociale liée à la crise sanitaire pour les cotisations de l’année 2021: Montant de l’exonération sociale liée à la crise sanitaire pour les diff --git a/site/source/pages/Landing/Landing.tsx b/site/source/pages/Landing/Landing.tsx index 506bf370c..ad220fe95 100644 --- a/site/source/pages/Landing/Landing.tsx +++ b/site/source/pages/Landing/Landing.tsx @@ -86,7 +86,7 @@ export default function Landing() { > - + , Engine, Engine] +}) => { + 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(null) + + const { t } = useTranslation() + + const dispatch = useDispatch() + + return ( + void }) => ( + + )} + 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) + }} + > + <> +

+ Aller plus loin sur les revenus +

+

+ Calculer vos revenus +

+ + + {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)." + )} + + + + Type de structure + + + SASU + + + + + + + EI + + + + + + + + AE + + + + + + + + + + - + {' '} + Chiffre d'affaires + + + + + + + + + + - Charges + + + + + + + + + + -{' '} + Cotisations + + + + + + + + + + + + + + + + + + + + - Impôts + + + + + + + + + + + + + + + + + + + + + + + - + {' '} + + Revenu net + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Bénéficier de l'ACRE

+ +
+ + + L'aide à la création ou à la reprise d'une entreprise (Acre) consiste + en une exonération partielle de charges sociales, + dite exonération de début d'activité pendant 12 mois. + + { + // TODO : décommenter une fois le simulateur créé + + } +
Choisir mon option de simulation
+
+ + setAcreValue(value)} + defaultSelected={defaultValueACRE as boolean} + /> + + + + {(acreValue || defaultValueACRE) && ( + <> + + Les{' '} + + conditions d'accès + {' '} + à l'ACRE sont plus restrictives pour les auto-entrepreneurs. + + + setAEAcreValue(value)} + defaultSelected={isAutoEntrepreneurACREEnabled} + /> + + + + )} +
+ + +

+ Impôt sur le revenu, impôt sur les sociétés : que choisir ? +

+ + L’EI et la SASU permettent de{' '} + + choisir entre l’imposition sur les sociétés et sur le revenu + {' '} + durant les 5 premières années. En auto-entreprise (AE), c’est l’ + impôt sur le revenu qui est appliqué automatiquement + ; dans certaines situations, vous pouvez aussi opter pour le{' '} + + + versement libératoire + + + . + +
Choisir mon option de simulation (pour EI)
+ + + + + + + + + À 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). + + + + + + { + setImpotValue(String(value)) + }} + key="imposition" + aria-labelledby="questionHeader" + engine={indépendantEngine} + /> + +
+ Choisir mon option de versement libératoire (pour AE){' '} + +
+ + + + + +
+ ) +} + +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 diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/Comparateur.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/Comparateur.tsx new file mode 100644 index 000000000..90d77ad5d --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/Comparateur.tsx @@ -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, Engine, Engine] +} + +function Comparateur({ engines }: ComparateurProps) { + const { t } = useTranslation() + const navigate = useNavigate() + const [assimiléEngine, autoEntrepreneurEngine, indépendantEngine] = engines + + const { absoluteSitePaths } = useSitePaths() + + return ( + <> + + } + legend={'Estimations sur votre rémunération brute et vos charges'} + > + + + + + + + + + + { + navigate(absoluteSitePaths.simulateurs.comparaison, { + replace: true, + }) + }} + > + + + + } + /> + + { + navigate(absoluteSitePaths.simulateurs.comparaison, { + replace: true, + }) + }} + > + + + + } + /> + + { + navigate(absoluteSitePaths.simulateurs.comparaison, { + replace: true, + }) + }} + > + + + + } + /> + + + ) +} + +export default Comparateur diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/DetailsRowCards.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/DetailsRowCards.tsx new file mode 100644 index 000000000..64ce1aa29 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/DetailsRowCards.tsx @@ -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, Engine, Engine] + 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 ( + + + + + Ne s'applique pas + + + + + + {label && ' '} + {label && label} + + + + + {warnings?.sasu && warnings?.sasu} + + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + ) + } + + if (assimiléValue === indépendantValue) { + return ( + + + + + Ne s'applique pas + + + + + {label && ' '} + {label && label} + + + + + {warnings?.sasu || warnings?.ei + ? warnings?.sasu + ? warnings?.sasu + : warnings?.ei + : ''} + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + + + Ne s'applique pas + + + + + {label && ' '} + {label && label} + + + + + {warnings?.ae && warnings?.ae} + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + ) + } + + if (indépendantValue === autoEntrepreneurValue) { + return ( + + + + + Ne s'applique pas + + + + + {label && ' '} + {label && label} + + + + + {warnings?.sasu && warnings?.sasu} + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + + + Ne s'applique pas + + + + + {label && ' '} + {label && label} + + + + + {warnings?.ei || warnings?.ae + ? warnings?.ei + ? warnings?.ei + : warnings?.ae + : ''} + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + ) + } + + return ( + + + + + Ne s'applique pas + + + + + {label && ' '} + {label && label} + + + + + {warnings?.sasu && warnings?.sasu} + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + + + Ne s'applique pas + + + + + {label && ' '} + {label && label} + + + + + {warnings?.ei && warnings?.ei} + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + + + Ne s'applique pas + + + + + {label && ' '} + {label && label} + + + + + {warnings?.ae && warnings?.ae} + {evolutionDottedName && ( + + {' '} + {evolutionLabel} + + )} + {!evolutionDottedName && evolutionLabel && ( + {evolutionLabel} + )} + + + + + ) +} + +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 diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/Détails.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/Détails.tsx new file mode 100644 index 000000000..d1e4d463c --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/Détails.tsx @@ -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, Engine, Engine] +}) => { + return ( + + theme.darkMode + ? theme.colors.extended.dark[800] + : theme.colors.bases.primary[200] + } + > + + Zoom sur... + + } + isFoldable + > + + La retraite + + } + key="retraite" + hasChildItems={false} + > + + + Le montant de votre retraite est constitué de{' '} + + votre retraite de base + votre retraite complémentaire + + . + + + + Retraite de base + + + + + La pension calculée correspond à celle de{' '} + vos 25 meilleures années, 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. + + + + + + + Retraite complémentaire + + + + + Tous les ans, selon votre rémunération,{' '} + + vous gagnez des points qui constituent votre pension de retraite + complémentaire + + . En fin de carrière, vos points sont transformés en{' '} + + un montant qui s’ajoute chaque mois à votre retraite de base + + . Cette valeur se calcule sur le long terme. Par exemple, au bout + de 10 ans, vous auriez droit à : + + + + au bout de 10 ans} + /> + + + La santé + + } + key="santé" + hasChildItems={false} + > + + + Tous les statuts vous ouvrent le droit au{' '} + remboursement des soins. + + + + + Pour tous les statuts, il est conseillé de souscrire à une{' '} + prévoyance complémentaire (mutuelle) pour + améliorer le remboursement des frais de santé. + + + + + Arrêt maladie + + + + + Pour tous les statuts, vous aurez un{' '} + délai de carence de 3 jours. En cas d’arrêt + maladie, l’assurance maladie vous versera : + + + + + + Votre rémunération est{' '} + trop faible pour bénéficier d’arrêt + maladie en SASU. + +
+ } + id="tooltip-sasu-arrêt-maladie" + /> + + ), + }} + footers={{ + sasu: ( + + + + + Pour y prétendre, vous devez voir cotisé au moins{' '} + 3 mois + + + + ), + ei: ( + + + + + Pour y prétendre, vous devez voir cotisé au moins{' '} + 12 mois + + + + ), + ae: ( + + + + + Pour y prétendre, vous devez voir cotisé au moins{' '} + 12 mois + + + + ), + }} + /> + + + Accident du travail et maladie professionnelle + + + + + En cas d’accident de travail, de{' '} + maladie professionnelle ou d’un{' '} + accident sur le trajet domicile-travail, vous + serez indemnisé(e) à hauteur de : + + + à partir du 29ème jour} + /> + + + La maternité, paternité et adoption{' '} + + + } + key="enfants" + hasChildItems={false} + > + + + Tous les statuts vous ouvrent le droit aux{' '} + indemnités journalières de congé maternité, + paternité, adoption. + + + + + Pour y prétendre, vous devez avoir cotisé{' '} + au moins 10 mois. + + + + + + + Maternité + + + + + En plus des indemnités journalières, vous pouvez aussi prétendre à + une{' '} + + allocation forfaitaire de repos maternel supplémentaire + + . + + + versés en deux fois} + /> + + + Adoption + + + + + En plus des indemnités journalières, vous pouvez aussi prétendre à + une{' '} + + allocation forfaitaire de repos parental supplémentaire + + . + + + versés en une fois} + /> + + + L'invalidité et le décès + + } + key="maladie" + hasChildItems={false} + > + + + Tous les statuts cotisent pour une{' '} + pension invalidité-décès qui les{' '} + protège en cas d’invalidité et assure à leurs + proches une{' '} + + pension de réversion et un capital en cas de décès + + . + + + + Invalidité + + + + + Vous pouvez bénéficier d’une pension invalidité{' '} + + en cas de maladie ou d’accident conduisant à une incapacité à + poursuivre votre activité professionnelle + + . + + + + + Pour y prétendre, vous devez respecter{' '} + + certaines règles + + + . + + + + (invalidité partielle) +
+ } + evolutionLabel={ + + (invalidité totale) + + } + /> + + + + + Pour une invalidité causée par un accident professionnel, vous + pouvez bénéficier d’une rente d’incapacité. + + + + + + Décès + + + + + La Sécurité Sociale garantit un{' '} + capital décès pour vos ayants droits (personnes + qui sont à votre charge) sous certaines conditions. + + + + + + + En plus du capital décès, une{' '} + pension de réversion peut être versée au conjoint + survivant. Elle correspond aux{' '} + droits à la retraite acquis par le défunt durant + sa vie professionnelle. + + + + + {' '} + + maximum + + + + + + + Pour un décès survenu dans le cadre d’un accident professionnel, + vous pouvez bénéficier d’une rente de décès. + + + + + + + + Un capital « orphelin » est versé aux enfants des + travailleurs indépendants décédés, sous certaines conditions. + + + + + + + La gestion juridique et comptable{' '} + + + } + key="administratif" + hasChildItems={false} + > + { + // TODO : implémenter les valeurs correspondantes dans modèle-social + // Ressource : https://entreprendre.service-public.fr/vosdroits/F23282 + /* + + Coût de création + + + + + 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 procédures obligatoires (immatriculation, + annonces légales, rédaction des statuts...). + + + + + + + + + + + + + + + + + + + + + + + + + + Aucun + + + + */ + } + + + Dépôt de capital + + + + + Selon les statuts, il est indispensable d’effectuer un{' '} + apport en capital à la création de l’entreprise. + Le montant minimum du capital social est de{' '} + 1 €. + + + + + + 1 € minimum + + + + + + Aucun + + + + + + + Statut du conjoint + + + + + Vous êtes marié(e), pacsé(e) ou en union libre avec un chef + d’entreprise : il existe 3 statuts possibles pour + vous (conjoint collaborateur,{' '} + conjoint associé ou{' '} + conjoint salarié). + + + + + + Conjoint associé ou salarié + + + + + Conjoint collaborateur ou salarié + + + + + Conjoint collaborateur + + + + + + + ) +} + +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 diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/ItemTitle.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/ItemTitle.tsx new file mode 100644 index 000000000..19b06b804 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/ItemTitle.tsx @@ -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 ( + + {children} + + ) +} + +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 diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/RetraiteItem.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/RetraiteItem.tsx new file mode 100644 index 000000000..40a790e61 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/RetraiteItem.tsx @@ -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 ( + + La retraite + + } + key="retraite" + hasChildItems={false} + > +

+ Retraite de base + +

+ + Le montant de votre retraite est constitué de{' '} + votre retraite de base + votre retraite complémentaire. + +
+ ) +} + +export default RetraiteItem diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/RevenuAprèsImpot.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/RevenuAprèsImpot.tsx new file mode 100644 index 000000000..bb9acb9b9 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/RevenuAprèsImpot.tsx @@ -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, Engine, Engine] +}) => { + 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 ( + <> +

+ Revenu après impôt +

+ + + + + } + > + + {' '} + + + la première année + + + + + + + + + + + + } + > + + {' '} + + + la première année + + + + + + + {' '} + + + + + + ACRE sous{' '} + + certaines conditions + + + + + ), + }, + { + 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"), + }, + ]} + /> + } + > + + + + + la première année + + + + + + + + + + Vous allez dépasser le plafond de la micro-entreprise + {' '} + + ( + {' '} + de chiffre d’affaires). + + + } + id="tooltip-ae" + /> + + + + + + + + + ) +} + +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; +` diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/RevenuEstimé.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/RevenuEstimé.tsx new file mode 100644 index 000000000..4c35f32f8 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/RevenuEstimé.tsx @@ -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 ( + + + + + + + + + + + + + + Modifier les informations + + + + + ) +} + +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é diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/Résultats.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/Résultats.tsx new file mode 100644 index 000000000..0c075bc27 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/Résultats.tsx @@ -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, Engine, Engine] +}) => { + return ( + + theme.darkMode + ? theme.colors.extended.dark[700] + : theme.colors.bases.primary[200] + } + > + + + + ) +} + +export default Résultats + +const StyledContainer = styled(Container)` + padding: ${({ theme }) => theme.spacings.lg}; +` diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/StatusCard.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/StatusCard.tsx new file mode 100644 index 000000000..40ff40fb6 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/StatusCard.tsx @@ -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 ( + + + + {status.map((statusString) => ( + + + + {STATUS_DATA[statusString].label} + + + ))} + + + {children} + + {isBestOption && ( + + Option la plus avantageuse. + + } + id={`tooltip-option-avantageuse-${String(tooltipIdRef.current)}`} + > + + + )} + {footerContent && {footerContent}} + + ) +} + +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 + case status.includes('ei'): + return + case status.includes('ae'): + return + + default: + return null + } +} diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/TableRow.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/TableRow.tsx new file mode 100644 index 000000000..6cfb44c02 --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/TableRow.tsx @@ -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, Engine, Engine] +} & Pick, 'precision' | 'unit'>) { + return ( + <> +

{assimiléEngine.getRule(dottedName).title}

+
+ +
+
+ +
+
+ +
+ + ) +} + +export default TableRow diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/components/WarningTooltip.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/components/WarningTooltip.tsx new file mode 100644 index 000000000..598b74caf --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/components/WarningTooltip.tsx @@ -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 ( + + + + ) +} + +export default WarningTooltip + +const StyledWarningIcon = styled(WarningIcon)` + margin-left: 0.5rem; +` diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/contexts/CasParticuliers.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/contexts/CasParticuliers.tsx new file mode 100644 index 000000000..4c679650d --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/contexts/CasParticuliers.tsx @@ -0,0 +1,40 @@ +import { + Dispatch, + ReactNode, + SetStateAction, + createContext, + useContext, + useState, +} from 'react' + +type CasParticuliersType = { + isAutoEntrepreneurACREEnabled: boolean + setIsAutoEntrepreneurACREEnabled: Dispatch> +} + +const CasParticuliersContext = createContext({ + isAutoEntrepreneurACREEnabled: false, + setIsAutoEntrepreneurACREEnabled: () => null, +}) + +export const CasParticuliersProvider = ({ + children, +}: { + children: ReactNode +}) => { + const [isAutoEntrepreneurACREEnabled, setIsAutoEntrepreneurACREEnabled] = + useState(false) + + return ( + + {children} + + ) +} + +export const useCasParticuliers = () => useContext(CasParticuliersContext) diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/index.tsx b/site/source/pages/Simulateurs/ComparateurStatuts/index.tsx new file mode 100644 index 000000000..163e54b6f --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/index.tsx @@ -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, Engine, Engine] + + return ( + <> + + + 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. + + + + + ) +} + +export default function ComparateurStatuts() { + return ( + + + + ) +} diff --git a/site/source/pages/Simulateurs/ComparateurStatuts/utils.ts b/site/source/pages/Simulateurs/ComparateurStatuts/utils.ts new file mode 100644 index 000000000..dd425b0aa --- /dev/null +++ b/site/source/pages/Simulateurs/ComparateurStatuts/utils.ts @@ -0,0 +1,29 @@ +export type ValueType = + | string + | number + | boolean + | null + | Record +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 +} diff --git a/site/source/pages/Simulateurs/EconomieCollaborative/ActiviteCard/index.tsx b/site/source/pages/Simulateurs/EconomieCollaborative/ActiviteCard/index.tsx index 1db42d496..37cac0ff3 100644 --- a/site/source/pages/Simulateurs/EconomieCollaborative/ActiviteCard/index.tsx +++ b/site/source/pages/Simulateurs/EconomieCollaborative/ActiviteCard/index.tsx @@ -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 = ({ )}

{titre}

- + {explication} - + - 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, Engine, Engine] - - return ( - - - - - } - /> - - } - /> - - } - /> - - - - 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. - - - - - } - /> - - ) -} - -type ComparateurProps = { - engines: [Engine, Engine, Engine] -} - -function Comparateur({ engines }: ComparateurProps) { - return ( - <> - - } - legend={'Estimations sur votre rémunération brute et vos charges'} - > - - - - - - -

- SASU -

-

- EI / EURL -

-

- Auto-entrepreneur -

- - - -

- Retraite -

- - - - - -

- Santé -

- - - -
- - ) -} - -function TableRow({ - dottedName, - engines: [assimiléEngine, autoEntrepreneurEngine, indépendantEngine], - precision, - unit, -}: { - dottedName: DottedName - engines: readonly [Engine, Engine, Engine] -} & Pick, 'precision' | 'unit'>) { - return ( - <> -

{assimiléEngine.getRule(dottedName).title}

-
- -
-
- -
-
- -
- - ) -} diff --git a/site/source/pages/Simulateurs/configs/comparateurStatuts.ts b/site/source/pages/Simulateurs/configs/comparateurStatuts.ts index 94490e473..180b85fc0 100644 --- a/site/source/pages/Simulateurs/configs/comparateurStatuts.ts +++ b/site/source/pages/Simulateurs/configs/comparateurStatuts.ts @@ -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', }, } diff --git a/site/source/pages/Simulateurs/metadata.tsx b/site/source/pages/Simulateurs/metadata.tsx index b1b0fb828..bdd97d370 100644 --- a/site/source/pages/Simulateurs/metadata.tsx +++ b/site/source/pages/Simulateurs/metadata.tsx @@ -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' diff --git a/site/source/pages/gerer/declaration-charges-sociales-independant/_components/ExplicationResultatFiscal.tsx b/site/source/pages/gerer/declaration-charges-sociales-independant/_components/ExplicationResultatFiscal.tsx index 5b759dfba..08d9c25cb 100644 --- a/site/source/pages/gerer/declaration-charges-sociales-independant/_components/ExplicationResultatFiscal.tsx +++ b/site/source/pages/gerer/declaration-charges-sociales-independant/_components/ExplicationResultatFiscal.tsx @@ -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 ( - + 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() { - + ) } diff --git a/site/source/utils.ts b/site/source/utils.ts index e69aeba11..2697b1139 100644 --- a/site/source/utils.ts +++ b/site/source/utils.ts @@ -240,3 +240,7 @@ export const catchDivideByZeroError = (func: () => T) => { throw err } } + +export const generateUuid = () => { + return Math.floor(Math.random() * Date.now()).toString(16) +} diff --git a/yarn.lock b/yarn.lock index 6425b6574..e9415badb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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