Termine le formulaire de demande de mobilité en Europe

- Ajoute la possibilité de signer via un écran tactile
- Améliore le parcours du formulaire
- Prends en compte les retours ACOSS
- Ajoute une possibilité d'intégration en iframe (non listée dans les intégrations officielles)
pull/1086/head
Johan Girod 2020-06-25 19:41:58 +02:00
parent efe1068f6a
commit bbdfa151c6
26 changed files with 959 additions and 467 deletions

View File

@ -52,6 +52,7 @@
"react-redux": "^7.0.3",
"react-router-dom": "^5.1.1",
"react-router-hash-link": "^1.2.2",
"react-signature-pad-wrapper": "^1.2.11",
"react-spring": "=8.0.27",
"react-syntax-highlighter": "^10.1.1",
"react-transition-group": "^2.2.1",

View File

@ -32,6 +32,9 @@ export default function DateInput({
if (+year < 1700) {
return
}
if (year.length > 4) {
return
}
if ([day, month, year].some(x => Number.isNaN(+x))) {
return
}

View File

@ -0,0 +1,33 @@
import React, { useCallback } from 'react'
import { debounce } from '../../utils'
export default function ParagrapheInput({
onChange,
dottedName,
value,
defaultValue,
autoFocus
}) {
const debouncedOnChange = useCallback(debounce(1000, onChange), [])
return (
<div className="step input">
<textarea
autoFocus={autoFocus}
className="ui__"
rows={6}
style={{ resize: 'none' }}
id={'step-' + dottedName}
placeholder={(defaultValue?.nodeValue ?? defaultValue)?.replace(
'\\n',
'\n'
)}
onChange={({ target }) => {
debouncedOnChange(`'${target.value.replace(/\n/g, '\\n')}'`)
}}
defaultValue={value?.replace('\\n', '\n')}
autoComplete="off"
/>
</div>
)
}

View File

@ -12,6 +12,8 @@ import { useTranslation } from 'react-i18next'
import { DottedName } from 'Rules'
import DateInput from './DateInput'
import TextInput from './TextInput'
import SelectEuropeCountry from './select/SelectEuropeCountry'
import ParagrapheInput from './ParagrapheInput'
type Value = string | number | object | boolean | null
export type RuleInputProps<Name extends string = DottedName> = {
@ -68,6 +70,8 @@ export default function RuleInput<Name extends string = DottedName>({
}
if (rule.API && rule.API === 'commune')
return <SelectCommune {...commonProps} onSubmit={onSubmit} />
if (rule.API && rule.API === 'pays européen')
return <SelectEuropeCountry {...commonProps} onSubmit={onSubmit} />
if (rule.API) throw new Error("Les seules API implémentées sont 'commune'")
if (rule.dottedName == 'contrat salarié . ATMP . taux collectif ATMP')
@ -130,6 +134,9 @@ export default function RuleInput<Name extends string = DottedName>({
if (rule.type === 'texte') {
return <TextInput {...commonProps} />
}
if (rule.type === 'paragraphe') {
return <ParagrapheInput {...commonProps} />
}
return <Input {...commonProps} unit={unit} onSubmit={onSubmit} />
}

View File

@ -0,0 +1,53 @@
import React from 'react'
const STATES = [
'Allemagne',
'Autriche',
'Belgique',
'Bulgarie',
'Chypre',
'Croatie',
'Danemark',
'Espagne',
'Estonie',
'Finlande',
'Grèce',
'Hongrie',
'Irlande',
'Islande',
'Italie',
'Lettonie',
'Liechtenstein',
'Lituanie',
'Luxembourg',
'Malte',
'Norvège',
'Pays-Bas',
'Pologne',
'Portugal',
'République Tchèque',
'Roumanie',
'Royaume-Uni',
'Slovaquie',
'Slovénie',
'Suède',
'Suisse'
]
export default function SelectEuropeCountry({ value, onChange, onSubmit }) {
return (
<div>
<select
name="country"
className="ui__"
defaultValue={value?.slice(1, -1)}
onChange={e => onChange(`'${e.target.value}'`)}
>
{STATES.map(state => (
<option key={state} value={state}>
{state}
</option>
))}
</select>
</div>
)
}

View File

@ -49,14 +49,14 @@
.ui__.checkbox:hover svg {
stroke: var(--color);
}
.ui__.checkbox-input:checked + .ui__.checkbox svg {
.ui__.checkbox-input:checked + label .ui__.checkbox svg {
stroke: var(--color);
}
.ui__.checkbox-input:checked + .ui__.checkbox svg path {
.ui__.checkbox-input:checked + label .ui__.checkbox svg path {
stroke-dashoffset: 60;
transition: all 0.15s linear;
}
.ui__.checkbox-input:checked + .ui__.checkbox svg polyline {
.ui__.checkbox-input:checked + label .ui__.checkbox svg polyline {
stroke-dashoffset: 42;
transition: all 0.1s linear;
transition-delay: 0.075s;

View File

@ -1,7 +1,9 @@
import React from 'react'
import './index.css'
export default function Checkbox(props: React.ComponentProps<'input'>) {
export default function Checkbox(
props: React.ComponentProps<'input'> & { label?: string }
) {
return (
<>
<input
@ -10,11 +12,20 @@ export default function Checkbox(props: React.ComponentProps<'input'>) {
style={{ display: 'none' }}
{...props}
/>
<label htmlFor={props.id} className="ui__ checkbox" tabIndex={0}>
<svg width="1em" height="1em" viewBox="0 0 18 18">
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z" />
<polyline points="1 9 7 14 15 4" />
</svg>
<label
htmlFor={props.id}
tabIndex={0}
style={{ display: 'flex', alignItems: 'center' }}
>
<div className="ui__ checkbox">
<svg width="1em" height="1em" viewBox="0 0 18 18">
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z" />
<polyline points="1 9 7 14 15 4" />
</svg>
</div>
{'label' in props && (
<span style={{ marginLeft: '0.6rem' }}>{props.label}</span>
)}
</label>
</>
)

View File

@ -196,18 +196,28 @@ pre.ui__.code > code {
hyphens: none;
}
input.ui__ {
input.ui__,
textarea.ui__,
select.ui__ {
padding: 0.4rem;
width: 25rem;
max-width: 100%;
margin-bottom: 0.6rem;
border: 1px solid var(--lighterTextColor);
border-radius: 0.3rem;
background-color: inherit;
color: inherit;
font-size: inherit;
transition: border-color 0.1s;
position: relative;
font-family: inherit;
}
input.ui__ {
width: 25rem;
}
textarea.ui__ {
width: 100%;
}
input.ui__[inputmode='numeric'] {
width: 10rem;
}
@ -217,7 +227,8 @@ input.ui__[type='date'] {
input.ui__.suffixed {
text-align: right;
}
input.ui__:focus {
input.ui__:focus,
select.ui__:focus {
border-color: var(--color);
}

View File

@ -1,7 +1,6 @@
import { iframeResizer } from 'iframe-resizer'
import logoEnSvg from 'Images/logo-mycompany.svg'
import logoFrSvg from 'Images/logo.svg'
import marianneSvg from 'Images/marianne.svg'
import urssafSvg from 'Images/urssaf.svg'
let script =
@ -71,17 +70,10 @@ links.innerHTML = `
<a href="https://www.urssaf.fr" target="_blank">
<img
style="height: 2.5rem; margin-right: 1rem"
src="${process.env.FR_SITE.replace('${path}', '/' + urssafSvg)}"
src="${urssafSvg}"
alt="un service fourni par l'Urssaf"
/>
</a>
<a href="https://beta.gouv.fr" target="_blank">
<img
style="height: 2.5rem"
src="${process.env.FR_SITE.replace('${path}', '/' + marianneSvg)}"
alt="incubé par beta.gouv.fr (direction du numérique)"
/>
</a>
</div>
`

View File

@ -0,0 +1,204 @@
import { BlobProvider } from '@react-pdf/renderer'
import Overlay from 'Components/Overlay'
import Checkbox from 'Components/ui/Checkbox'
import { ThemeColorsContext } from 'Components/utils/colors'
import React, { useContext, useRef, useState, Suspense } from 'react'
import emoji from 'react-easy-emoji'
import SignaturePad from 'react-signature-pad-wrapper'
import PDFDocument from './PDFDocument'
const IS_TOUCH_DEVICE = isOnTouchDevice()
type SignaturePadInstance = {
clear: () => void
toDataURL: () => string
}
export default function EndBlock({ fields, isMissingValues }) {
const [isCertified, setCertified] = useState(false)
const [place, setPlace] = useState<string>()
const [showDownloadLink, toggleDownloadLink] = useState(false)
const { darkColor } = useContext(ThemeColorsContext)
const signatureRef = useRef<SignaturePadInstance>()
if (isMissingValues) {
return (
<blockquote>
<strong>Certains champs ne sont pas renseignés.</strong>
<br />{' '}
<small>
Vous devez compléter l'intégralité du formulaire avant de pouvoir le
signer et générer votre demande.
</small>
</blockquote>
)
}
return (
<>
<h2>Déclaration sur l'honneur</h2>
<Checkbox
name="certified"
id="certified"
onChange={e => setCertified(e.target.checked)}
checked={isCertified}
label="Je certifie lexactitude des informations communiquées ci-dessus."
/>
<p className="ui__ notice">
Lauteur dune fausse déclaration est passible dune condamnation au
titre de larticle 441-1 du code pénal.
</p>
<label>
<small>Fait à :</small>
<br />
<input
type="text"
className="ui__"
value={place}
onChange={e => setPlace(e.target.value)}
/>
</label>
{IS_TOUCH_DEVICE && (
<div>
<small>
Signez ci dessous en utilisant la totalité de l'espace :{' '}
</small>
<div
css={`
border: 1px solid var(--darkColor);
border-radius: 0.3rem;
position: relative;
`}
>
<SignaturePad
height={200}
options={{ penColor: darkColor }}
ref={signatureRef}
/>
</div>
<div
css={`
text-align: right;
`}
>
<button
className="ui__ simple small button"
onClick={() => signatureRef.current?.clear()}
>
{emoji('🗑️')} Recommencer{' '}
</button>
</div>
</div>
)}
<div>
<button
className="ui__ cta plain button"
disabled={!place || !isCertified}
onClick={() => toggleDownloadLink(true)}
>
Générer la demande
</button>
</div>
<p className="ui__ notice">
<strong>Vie privée :</strong> aucune donnée n'est transmise à nos
serveurs, la génération du formulaire se fait entièrement depuis votre
navigateur.
</p>
{showDownloadLink && (
<Overlay onClose={() => toggleDownloadLink(false)}>
<h2>Votre demande de mobilité</h2>
<p>
Afin dexaminer votre situation au regard des règlements
communautaires UE/EEE de Sécurité sociale (CE 883/2004), veuillez
envoyer ce document à{' '}
<a href="mailto:relations.internationales@urssaf.fr">
relations.internationales@urssaf.fr
</a>
</p>
<Suspense
fallback={
<blockquote>
<small>Génération du pdf en cours...</small>
</blockquote>
}
>
<LazyBlobProvider
document={
<PDFDocument
fields={fields}
signatureURL={
IS_TOUCH_DEVICE && signatureRef.current?.toDataURL()
}
place={place}
/>
}
>
{({ url, loading, error }) =>
error ? (
<blockquote>
<strong>Erreur lors de la génération du pdf</strong>
<br />
<small>
Veuillez envoyer un mail à
contact@mon-entreprise.beat.gouv.fr
</small>
</blockquote>
) : loading ? (
<blockquote>
<small>Génération du pdf en cours...</small>
</blockquote>
) : (
url && (
<>
{!IS_TOUCH_DEVICE && (
<blockquote>
<strong>
N'oubliez pas de signer le document avant de
l'envoyer
</strong>
</blockquote>
)}
<a
href={url}
className="ui__ cta plain button"
download="demande-mobilité-europe.pdf"
>
Télécharger le fichier
</a>
</>
)
)
}
</LazyBlobProvider>
</Suspense>
</Overlay>
)}
</>
)
}
const LazyBlobProvider = React.lazy<typeof BlobProvider>(
() =>
new Promise(resolve =>
setTimeout(() => resolve({ default: BlobProvider }), 300)
)
)
// From https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886
function isOnTouchDevice() {
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
const mq = function(query) {
return window.matchMedia(query).matches
}
if (
'ontouchstart' in window ||
('DocumentTouch' in window &&
document instanceof (window as any).DocumentTouch)
) {
return true
}
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
// https://git.io/vznFH
const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('')
return mq(query)
}

View File

@ -0,0 +1,57 @@
import { StyleSheet, Text, View } from '@react-pdf/renderer'
import { formatValue } from 'publicodes'
import React from 'react'
export default function FieldsPDF({ fields }) {
return fields.map(field => (
<View style={styles.field} key={field.dottedName} wrap={false}>
{field.type === 'groupe' ? (
<>
<Text style={styles.subtitle}>
{field.title}{' '}
{field.note && (
<Text style={styles.fieldNumber}>({field.note})</Text>
)}
</Text>
</>
) : (
<>
<Text style={styles.name}>
{field.question ?? field.title}{' '}
{field.note && (
<Text style={styles.fieldNumber}>({field.note})</Text>
)}
</Text>
{field.nodeValue != null && (
<Text style={styles.value}>{formatValue(field)}</Text>
)}
</>
)}
</View>
))
}
export const styles = StyleSheet.create({
fieldNumber: {
opacity: 0.7
},
subtitle: {
paddingTop: 10,
fontFamily: 'Montserrat',
fontSize: 16
},
field: {
marginBottom: 12,
lineHeight: 1.2
},
name: {
fontSize: 11,
marginBottom: 4,
opacity: 0.7,
fontFamily: 'Roboto'
},
value: {
fontSize: 14,
fontFamily: 'Roboto'
}
})

View File

@ -0,0 +1,131 @@
import {
Document,
Font,
Image,
Page,
StyleSheet,
Text,
View
} from '@react-pdf/renderer'
import urssafPng from 'Images/destinataires/URSSAF.png'
import React from 'react'
import FieldsPDF, { styles as fieldStyles } from './FieldsPDF'
import montserratUrl from './Montserrat-SemiBold.ttf'
import robotoUrl from './Roboto-Regular.ttf'
export default function PDFDocument({ fields, signatureURL, place }) {
return (
<Document>
<Page style={styles.body} wrap>
<View style={styles.header}>
<Image src={urssafPng} style={styles.logo} />
</View>
<View>
<Text style={styles.title}>
{fields.find(({ dottedName }) => dottedName === 'détachement')
? 'Demande de détachement en Europe'
: "Demande d'activité transfrontalière simultanée en Europe"}
</Text>
<Text style={styles.texte}>
Afin dexaminer votre situation au regard des règlements
communautaires de Sécurité sociale (CE 883/2004 et CE 987/2009),
veuillez envoyer ce document à relations.internationales@urssaf.fr
</Text>
</View>
<FieldsPDF fields={fields} />
<View style={styles.texte}>
<View style={fieldStyles.field}>
<Text style={fieldStyles.subtitle}>Déclaration sur l'honneur</Text>
</View>
<View style={fieldStyles.field}>
<Text style={fieldStyles.value}>
Je certifie lexactitude des informations communiquées ci-dessus.
</Text>
</View>
<View style={fieldStyles.field}>
<Text style={fieldStyles.value}>
Fait le{' '}
{new Date()
.toISOString()
.split('T')[0]
.replace(/([\d]{4})-([\d]{2})-([\d]{2})/, '$3/$2/$1')}{' '}
à {place}
</Text>
</View>
<View style={fieldStyles.field}>
<Text style={fieldStyles.name}>Signature :</Text>
</View>
{signatureURL ? (
<Image src={signatureURL} style={styles.signature} />
) : (
<View style={styles.signatureBox} />
)}
</View>
<View fixed style={styles.footer}>
<Text>
La loi n° 78-17 du 6 janvier 1978 relative à linformatique, aux
fichiers et aux libertés, sapplique aux réponses faites sur ce
formulaire. Elle garantit un droit daccès et de rectification pour
les données vous concernant auprès de notre organisme.
</Text>
</View>
</Page>
</Document>
)
}
Font.register({
family: 'Roboto',
src: robotoUrl
})
Font.register({
family: 'Montserrat',
src: montserratUrl
})
const styles = StyleSheet.create({
body: {
paddingTop: 35,
color: '#18457B',
lineHeight: 1.5,
paddingBottom: 65,
paddingHorizontal: 35
},
header: {
display: 'flex',
justifyContent: 'flex-end',
marginBottom: 20
},
logo: {
objectFit: 'scale-down',
width: 100
},
title: {
fontSize: 20,
marginBottom: 20,
fontFamily: 'Montserrat'
},
texte: {
fontFamily: 'Roboto',
marginBottom: 12,
fontSize: 14
},
signature: {
objectFit: 'scale-down',
maxWidth: 300
},
signatureBox: {
height: 100
},
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
marginHorizontal: 35,
paddingVertical: 5,
opacity: 0.7,
fontSize: 6
}
})

View File

@ -0,0 +1,324 @@
activité france . SIREN:
note: 4.2
type: texte
coordonnées assuré:
titre: Vos coordonnées
type: groupe
formule: oui
note: 1
coordonnées assuré . nom:
type: texte
note: 1.2
coordonnées assuré . prénoms:
type: texte
note: 1.3
coordonnées assuré . nationalité:
type: texte
note: 1.6
coordonnées assuré . numéro de sécurité sociale:
description: Saisissez les 15 chiffres de votre numéro tel qu'il apparaît sur votre carte vitale par exemple.
type: texte
# API: numéro sécurité sociale
note: 1.1
coordonnées assuré . date de naissance:
type: date
note: 1.5
coordonnées assuré . commune de naissance:
API: commune
note: 1.7
coordonnées assuré . domicile personnel:
type: groupe
formule: oui
note: 1.8
coordonnées assuré . domicile personnel . adresse:
type: texte
note: 1.9.1
coordonnées assuré . domicile personnel . commune:
API: commune
note: 1.9.2 / 1.9.3
coordonnées assuré . contact:
type: groupe
formule: oui
coordonnées assuré . contact . email:
type: texte
coordonnées assuré . contact . téléphone:
type: texte
activité france:
note: 4 / 4.1.2
type: groupe
formule: oui
titre: Votre activité indépendante en France
activité france . nom:
note: 4.3
titre: Nom de l'entreprise
type: texte
activité france . adresse:
note: 4.4.1
type: texte
activité france . commune:
API: commune
note:: 4.4.3 / 4.4.4
activité france . organisme urssaf:
type: texte
description: >
Nom de l'organisme Urssaf dont vous relevez en france
activité france . nature de l'activité:
question: Quel est la nature de votre activité en France ?
type: texte
description: >-
Saisissez une courte description de votre activité en France (ex: plombier, coach sportif, aménagement intérieur)
demande:
titre: Votre demande
formule: oui
type: groupe
demande . pays unique:
question: >
Allez-vous exercer une activité non salariée dans un seul et unique pays ?
demande . infrastructure sauvegardée:
applicable si: pays unique
question: >
Pendant votre travail en dehors de la France, l'infrastructure de votre
entreprise en France reste-elle en place ?
description: >-
Par infrastructure, est entendu tout ce qui est nécessaire pour poursuivre votre travail en
France après votre retour (local, matériel, etc.).
demande . activité semblable:
applicable si: infrastructure sauvegardée
question: >
Allez-vous exercer cette mission dans un domaine d'activité semblable ?
demande . date de fin connue:
applicable si: activité semblable
question: Votre activité à l'étranger est-elle limitée dans le temps ?
demande . détachement possible:
formule:
toutes ces conditions:
- date de fin connue
- pays unique
- infrastructure sauvegardée
- activité semblable
détachement:
note: 3.3
titre: Demande de détachement
applicable si: demande . détachement possible
formule: oui
type: groupe
détachement . pays:
note: 4.4.2
API: pays européen
type: texte
détachement . date de début:
type: date
note: 2.2
détachement . date de fin:
note: 2.2
type: date
détachement . nature de l'activité:
question: Quel sera la nature de votre activité pendant la période de détachement ?
type: texte
description: >-
Saisissez une courte description de votre activité pendant le détachement (ex: plombier, coach sportif, aménagement intérieur)
détachement . base fixe:
question: Connaissez vous l'adresse de votre activité à l'étranger ?
note: 5.3
détachement . activité:
applicable si: base fixe
note: 5.2
titre: Coordonnées de votre client / chantier / lieu d'activité à l'étranger
type: groupe
formule: oui
détachement . activité . nom:
type: texte
détachement . activité . adresse:
type: texte
détachement . activité . ville:
type: texte
détachement . activité . code postal:
type: texte
détachement . commentaires additionnels:
question: Souhaitez-vous partager d'autres informations pour l'instruction de votre dossier (adresses supplémentaires, commentaires, etc) ?
détachement . commentaires additionnels . commentaires:
type: paragraphe
activité transfrontalière simultanée:
non applicable si: demande . détachement possible
titre: Demande d'activité transfrontalière simultanée
formule: oui
type: groupe
activité transfrontalière simultanée . salarié hors France:
question: >
Travaillez-vous en tant que salarié dans un autre pays ?
activité transfrontalière simultanée . activité salariée:
applicable si: salarié hors France
note: 3.8 / 4.1.2
titre: Activité salariée à l'étranger
type: groupe
formule: oui
activité transfrontalière simultanée . activité salariée . pays:
API: pays européen
note: 5.1
type: texte
activité transfrontalière simultanée . activité salariée . profession:
type: texte
activité transfrontalière simultanée . activité salariée . période:
type: groupe
formule: oui
activité transfrontalière simultanée . activité salariée . période . date de début du contrat:
type: date
activité transfrontalière simultanée . activité salariée . période . date de fin connue:
question: Votre contrat salarié a-t'il une date de fin ?
activité transfrontalière simultanée . activité salariée . période . date de fin:
applicable si: date de fin connue
type: date
activité transfrontalière simultanée . activité salariée . employeur:
note: 5.1
type: groupe
formule: oui
activité transfrontalière simultanée . activité salariée . employeur . nom:
type: texte
activité transfrontalière simultanée . activité salariée . employeur . adresse:
type: texte
activité transfrontalière simultanée . activité salariée . employeur . ville:
type: texte
activité transfrontalière simultanée . activité salariée . employeur . code postal:
type: texte
activité transfrontalière simultanée . activité salariée . activité substantielle:
question: >
Le salaire de cette activité representera-t'il plus de 5% de vos revenus pour
les 12 prochains mois ?
activité transfrontalière simultanée . activité salariée . activité indépendante additionnelle:
question: >
Exercez-vous également une activité non salariée à l'étranger ?
activité transfrontalière simultanée . part subtentielle France:
non applicable si: activité salariée . activité substantielle
applicable si:
une de ces conditions:
- activité salariée = non
- activité salariée . activité indépendante additionnelle
question: Est-ce qu'au moins 25% de votre activité professionnelle des 12 prochains mois aura lieu en France ?
description: >-
Ce pourcentage peut être celui du temps de travail ou du chiffre d'affaires
activité transfrontalière simultanée . activité non salariée:
applicable si:
une de ces conditions:
- activité salariée = non
- activité salariée . activité indépendante additionnelle
titre: Activités non salariées à l'étranger
type: groupe
formule: oui
activité transfrontalière simultanée . activité non salariée . nombre:
question: Dans combien de pays autre que la France exercerez-vous une activité non salariée ?
type: nombre
contrôles:
- si: nombre > 2
message: >
Ce formulaire ne permet pas de déclarer une activité dans plus de
3 pays
niveau: avertissement
- si: nombre < 1
message: >
Vous devez déclarer un pays au moins
niveau: avertissement
activité transfrontalière simultanée . activité non salariée . n°1:
titre: activité non salarié n°1
type: groupe
applicable si: nombre >= 1
formule: oui
activité transfrontalière simultanée . activité non salariée . n°1 . pays:
API: pays européen
type: texte
activité transfrontalière simultanée . activité non salariée . n°1 . nom de l'entreprise:
type: texte
activité transfrontalière simultanée . activité non salariée . n°1 . adresse:
type: texte
activité transfrontalière simultanée . activité non salariée . n°1 . ville:
type: texte
activité transfrontalière simultanée . activité non salariée . n°1 . code postal:
type: texte
activité transfrontalière simultanée . activité non salariée . n°1 . date de début d'activité:
type: date
activité transfrontalière simultanée . activité non salariée . n°2:
titre: activité non salarié n°2
type: groupe
applicable si: nombre >= 2
formule: oui
activité transfrontalière simultanée . activité non salariée . n°2 . pays:
API: pays européen
type: texte
activité transfrontalière simultanée . activité non salariée . n°2 . nom de l'entreprise:
type: texte
activité transfrontalière simultanée . activité non salariée . n°2 . adresse:
type: texte
activité transfrontalière simultanée . activité non salariée . n°2 . ville:
type: texte
activité transfrontalière simultanée . activité non salariée . n°2 . code postal:
type: texte
activité transfrontalière simultanée . activité non salariée . n°2 . date de début d'activité:
type: date
activité transfrontalière simultanée . activité non salariée . n°3:
titre: activité non salarié n°3
type: groupe
applicable si: nombre >= 3
formule: oui
activité transfrontalière simultanée . activité non salariée . n°3 . pays:
API: pays européen
type: texte
activité transfrontalière simultanée . activité non salariée . n°3 . nom de l'entreprise:
type: texte
activité transfrontalière simultanée . activité non salariée . n°3 . adresse:
type: texte
activité transfrontalière simultanée . activité non salariée . n°3 . ville:
type: texte
activité transfrontalière simultanée . activité non salariée . n°3 . code postal:
type: texte
activité transfrontalière simultanée . activité non salariée . n°3 . date de début d'activité:
type: date
activité transfrontalière simultanée . activité non salariée . pays centre d'intérêt:
type: groupe
formule: oui
titre: Quel sera votre pays "centre d'intêret" pour les 12 prochains mois ?
non applicable si:
une de ces conditions:
- part subtentielle France
- activité salariée . activité substantielle
applicable si: nombre >= 2
description: >-
Pour determiner votre pays "centre d'intêret", vous devez prendre en compte les critères suivants (au choix) :
- Le pays d'où découlera la majorité des revenus de vos activités / de votre chiffre d'affaires
- Le pays où vous réaliserez vos activités de manière habituelle / où vous passerez le plus de temps de travail
- En cas d'ambiguité pour les deux critères ci-dessus, le pays où sera situé le siège fixe de vos activités
activité transfrontalière simultanée . activité non salariée . pays centre d'intérêt . pays:
API: pays européen
titre: Pays "centre d'intêret"
type: texte

View File

@ -2,15 +2,17 @@ import RuleInput from 'Components/conversation/RuleInput'
import * as Animate from 'Components/ui/animate'
import InfoBulle from 'Components/ui/InfoBulle'
import { Markdown } from 'Components/utils/markdown'
import Engine from 'publicodes'
import React, { useCallback, useState, Suspense } from 'react'
import formulaire from './formulaire-détachement.yaml'
import { usePersistingState } from 'Components/utils/persistState'
import { hash } from '../../../../../utils'
import Overlay from 'Components/Overlay'
import { useDebounce } from 'Components/utils'
import Engine from 'publicodes'
import React, { Suspense, useCallback } from 'react'
import emoji from 'react-easy-emoji'
export default function FormulaireDétachementIndépendant() {
import { hash } from '../../../../../utils'
import formulaire from './formulaire-détachement.yaml'
import { Explicable } from 'Components/conversation/Explicable'
const LazyEndBlock = React.lazy(() => import('./EndBlock'))
export default function formulaireMobilitéIndépendant() {
const engine = new Engine(formulaire)
return (
<>
@ -25,6 +27,19 @@ export default function FormulaireDétachementIndépendant() {
</a>
.
</p>
<blockquote>
<p className="ui__ lead">
<strong>Ce document nécessite votre signature {emoji('✍️')}</strong>
</p>
<p>
Nous vous suggérons d'utiliser un appareil avec écran tactile pour
compléter ce formulaire (téléphone, tablette, etc.).{' '}
</p>
<p>
Autremenent, il vous faudra imprimer, signer et scanner le document
généré.
</p>
</blockquote>
<FormulairePublicodes engine={engine} />
</>
)
@ -33,7 +48,12 @@ export default function FormulaireDétachementIndépendant() {
const useFields = (engine: Engine<string>, fieldNames: Array<string>) => {
const fields = fieldNames
.map(name => engine.evaluate(name))
.filter(node => node.isApplicable !== false && node.isApplicable !== null)
.filter(
node =>
node.isApplicable !== false &&
node.isApplicable !== null &&
(node.question || node.type || node.API)
)
return fields
}
@ -79,20 +99,21 @@ function FormulairePublicodes({ engine }) {
`}
>
{field.question ? (
<div
<span
css={`
margin-top: 0.6rem;
`}
>
{field.question}
</div>
</span>
) : (
<small>{field.title}</small>
)}{' '}
{field.description && (
<InfoBulle>
<Explicable>
<h3>{field.title}</h3>
<Markdown source={field.description} />
</InfoBulle>
</Explicable>
)}
<RuleInput
dottedName={field.dottedName}
@ -104,45 +125,9 @@ function FormulairePublicodes({ engine }) {
)}
</Animate.fromTop>
))}
<LazyPDFButton
className="ui__ plain cta button"
fields={fields}
disabled={isMissingValues}
>
Générer la demande
</LazyPDFButton>
{isMissingValues && (
<p className="ui__ notice">
Vous devez compléter l'intégralité du formulaire pour générer la
demande.{' '}
</p>
)}
<Suspense fallback={null}>
<LazyEndBlock fields={fields} isMissingValues={isMissingValues} />
</Suspense>
</Animate.fromTop>
)
}
const LazyPDFDownloadLink = React.lazy(() => import('./FormPDF'))
function LazyPDFButton({ fields, className, disabled, children }) {
const fieldsDebounced = useDebounce(fields.slice(1), 1000)
return (
<Suspense
fallback={
<button className={className} disabled>
{children}
</button>
}
>
<LazyPDFDownloadLink
className={className}
disabled={disabled}
fields={fieldsDebounced}
fileName={'demande-détachement.pdf'}
title={'Demande de mobilité en Europe'}
description="Afin dexaminer votre situation au regard des règlements communautaires UE/EEE de Sécurité sociale (CE 883/2004), veuillez envoyer ce document à relations.internationales@urssaf.fr"
>
{children}
</LazyPDFDownloadLink>
</Suspense>
)
}

View File

@ -1,163 +0,0 @@
import {
Document,
Page,
Text,
View,
StyleSheet,
Font,
PDFViewer,
PDFDownloadLink
} from '@react-pdf/renderer'
import React from 'react'
import robotoUrl from './Roboto-Regular.ttf'
import montserratUrl from './Montserrat-SemiBold.ttf'
import { formatValue } from 'publicodes'
import emoji from 'react-easy-emoji'
// function FormPDFViewer({ fields, title, description }) {
// return (
// <div
// css={`
// width: 100%;
// padding-bottom: 141.4%;
// position: relative;
// `}
// >
// <PDFViewer
// css={`
// position: absolute;
// width: 100%;
// height: 100%;
// top: 0;
// bottom: 0;
// left: 0;
// right: 0;
// `}
// >
// <FormPDF fields={fields} title={title} description={description} />
// </PDFViewer>
// </div>
// )
// }
export default function FormPDFDownloadLink({
fields,
title,
className,
description,
fileName,
disabled,
children
}) {
if (disabled) {
return (
<button disabled className={className}>
{children}
</button>
)
}
return (
<PDFDownloadLink
document={
<FormPDF fields={fields} title={title} description={description} />
}
fileName={fileName}
className={className}
>
{({ blob, url, loading, error }) => children}
</PDFDownloadLink>
)
// <PDFDownloadLink document={<MyDoc />} fileName="somename.pdf">
// {({ blob, url, loading, error }) => (loading ? 'Loading document...' : 'Download now!')}
// </PDFDownloadLink>
}
function FormPDF({ fields, title, description }) {
return (
<Document>
<Page style={styles.body}>
<View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
</View>
{fields.map(field => (
<View style={styles.field} key={field.dottedName} wrap={false}>
{field.type === 'groupe' ? (
<>
<Text style={styles.subtitle}>
{field.title}{' '}
{field.note && (
<Text style={styles.fieldNumber}>({field.note})</Text>
)}
</Text>
</>
) : (
<>
<Text style={styles.name}>
{field.question ?? field.title}{' '}
{field.note && (
<Text style={styles.fieldNumber}>({field.note})</Text>
)}
</Text>
{field.nodeValue != null && (
<Text style={styles.value}>{formatValue(field)}</Text>
)}
</>
)}
</View>
))}
</Page>
</Document>
)
}
Font.register({
family: 'Roboto',
src: robotoUrl
})
Font.register({
family: 'Montserrat',
src: montserratUrl
})
const styles = StyleSheet.create({
body: {
paddingTop: 35,
color: '#18457B',
lineHeight: 1.5,
paddingBottom: 65,
paddingHorizontal: 35
},
fieldNumber: {
opacity: 0.7
},
title: {
fontSize: 20,
marginBottom: 20,
fontFamily: 'Montserrat'
},
description: {
fontFamily: 'Roboto',
marginBottom: 12,
fontSize: 14
},
subtitle: {
paddingTop: 10,
fontFamily: 'Montserrat',
fontSize: 16
},
field: {
marginBottom: 12,
lineHeight: 1.2
},
name: {
fontSize: 11,
marginBottom: 4,
opacity: 0.7,
fontFamily: 'Roboto'
},
value: {
fontSize: 14,
fontFamily: 'Roboto'
}
})

View File

@ -1,212 +0,0 @@
coordonnées assuré:
titre: Vos coordonnées
type: groupe
formule: oui
note: 1
coordonnées assuré . nom:
type: texte
note: 1.2
coordonnées assuré . prénoms:
type: texte
note: 1.3
coordonnées assuré . nationalité:
type: texte
note: 1.6
coordonnées assuré . date de naissance:
type: date
note: 1.5
coordonnées assuré . commune de naissance:
API: commune
note: 1.7
coordonnées assuré . numéro de sécurité sociale:
type: texte
note: 1.1
coordonnées assuré . organisme urssaf:
type: texte
description: >
Nom de l'organisme Urssaf dont vous relevez en france
coordonnées assuré . domicile personnel:
type: groupe
formule: oui
note: 1.8
coordonnées assuré . domicile personnel . adresse:
type: texte
note: 1.9.1
coordonnées assuré . domicile personnel . commune:
API: commune
note: 1.9.2 / 1.9.3
activité france:
note: 4 / 4.1.2
type: groupe
formule: oui
titre: Votre activité indépendante en France
activité france . SIREN:
note: 4.2
type: texte
activité france . nom:
note: 4.3
titre: Nom de l'entreprise
type: texte
activité france . adresse:
note: 4.4.1
type: texte
activité france . commune:
API: commune
note:: 4.4.3 / 4.4.4
demande:
question: Quelle demande souhaitez-vous effectuer ?
formule:
une possibilité:
choix obligatoire: oui
possibilités:
- détachement
- activité transfrontalière simultanée
demande . détachement:
note: 3.3
type: groupe
formule: oui
applicable si: demande = 'détachement'
description: >
Vous vous détachez temporairement dans un autre état (pays) de lunion
europeenne (ou eee ou suisse).
demande . détachement . infrastructure sauvegardée:
question: >
Pendant votre travail en dehors de la France, l'infrastructure de votre
entreprise en France, qui est nécessaire pour poursuivre votre travail en
France après votre retour, reste-elle en place ?
demande . détachement . activité identique:
question: >
L'activité que vous exercez temporairement dans un autre Etat membre
est-elle la même que celle que vous exercez en France ?
demande . détachement . activité:
titre: Activité non salarié dans le pays étranger
type: groupe
formule: oui
demande . détachement . activité . nature:
titre: Nature de l'activité
type: texte
applicable si: activité identique = non
description: >
La nature de lactivité pendant la période de détachement (ex: plombier)
demande . détachement . activité . localisation:
type: groupe
formule: oui
demande . détachement . activité . localisation . pays:
type: texte
demande . détachement . activité . localisation . adresse:
type: texte
demande . détachement . activité . localisation . code postal:
type: texte
demande . détachement . activité . localisation . commune:
type: texte
demande . détachement . période:
type: groupe
formule: oui
titre: Période du détachement
demande . détachement . période . date de début:
type: date
demande . détachement . période . date de fin:
type: date
demande . activité transfrontalière simultanée:
note: 3.4
applicable si: demande = 'activité transfrontalière simultanée'
type: groupe
formule: oui
description: >
Vous êtes actif en tant qu'indépendant en France et travaillez également
dans un ou plusieurs autres Etats membres de l'Espace Economique Européen
demande . activité transfrontalière simultanée . indépendant hors France:
question: >
Êtes-vous actif en tant qu'indépendant dans un pays autre que la France ?
demande . activité transfrontalière simultanée . pourcentage en France:
applicable si: indépendant hors France
question: >
Est-ce que votre activité en France représente au moins 25% de votre
temps de travail global et/ou de votre rémunération / revenu ?
demande . activité transfrontalière simultanée . nombre pays:
unité: pays
applicable si: indépendant hors France
question: >
Dans combien de pays (autre que la France) exercez vous une activité
non salariée ?
contrôles:
- si: nombre pays > 3
message: >
Ce formulaire ne permet pas de déclarer une activité dans plus de
3 pays
niveau: avertissement
- si: nombre pays < 1
message: >
Vous devez déclarer un pays au moins
niveau: avertissement
demande . activité transfrontalière simultanée . pays 1:
type: groupe
applicable si: nombre pays >= 1
formule: oui
demande . activité transfrontalière simultanée . pays 1 . pays:
type: texte
demande . activité transfrontalière simultanée . pays 1 . adresse:
type: texte
demande . activité transfrontalière simultanée . pays 1 . code postal:
type: texte
demande . activité transfrontalière simultanée . pays 1 . commune:
type: texte
demande . activité transfrontalière simultanée . pays 1 . date de début:
titre: date de début d'activité dans ce pays
type: date
demande . activité transfrontalière simultanée . pays 1 . pourcentage:
question: >
Est-ce que votre activité dans ce pays représente au moins 25% de votre
temps de travail global et/ou de votre rémunération / revenu ?
demande . activité transfrontalière simultanée . pays 2:
type: groupe
applicable si: nombre pays >= 2
formule: oui
demande . activité transfrontalière simultanée . pays 2 . pays:
type: texte
demande . activité transfrontalière simultanée . pays 2 . adresse:
type: texte
demande . activité transfrontalière simultanée . pays 2 . code postal:
type: texte
demande . activité transfrontalière simultanée . pays 2 . commune:
type: texte
demande . activité transfrontalière simultanée . pays 2 . date de début:
titre: date de début d'activité dans ce pays
type: date
demande . activité transfrontalière simultanée . pays 2 . pourcentage:
question: >
Est-ce que votre activité dans ce pays représente au moins 25% de votre
temps de travail global et/ou de votre rémunération / revenu ?
demande . activité transfrontalière simultanée . pays 3:
type: groupe
applicable si: nombre pays >= 3
formule: oui
demande . activité transfrontalière simultanée . pays 3 . pays:
type: texte
demande . activité transfrontalière simultanée . pays 3 . adresse:
type: texte
demande . activité transfrontalière simultanée . pays 3 . code postal:
type: texte
demande . activité transfrontalière simultanée . pays 3 . commune:
type: texte
demande . activité transfrontalière simultanée . pays 3 . date de début:
titre: date de début d'activité dans ce pays
type: date
demande . activité transfrontalière simultanée . pays 3 . pourcentage:
question: >
Est-ce que votre activité dans ce pays représente au moins 25% de votre
temps de travail global et/ou de votre rémunération / revenu ?

View File

@ -45,7 +45,7 @@ const infereRégimeFromCompanyDetails = (company: Company | null) => {
}
export default function SocialSecurity() {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const company = useSelector(
(state: RootState) => state.inFranceApp.existingCompany
)
@ -189,6 +189,22 @@ export default function SocialSecurity() {
}
`}
>
{régime === 'indépendant' &&
i18n.language === 'fr' &&
process.env.HEAD !== 'master' && (
<Link
className="ui__ interactive card button-choice lighter-bg"
to={sitePaths.gérer.formulaireMobilité}
>
<Trans i18nKey="gérer.ressources.embaucher">
<p>Exporter son activité en Europe</p>
<p className="ui__ notice">
Le formulaire pour effectuer une demande de mobilité en
Europe (détachement ou pluriactivité)
</p>
</Trans>
</Link>
)}
{!company?.isAutoEntrepreneur && (
<Link
className="ui__ interactive card button-choice lighter-bg"

View File

@ -8,7 +8,7 @@ import AideDéclarationIndépendant from './AideDéclarationIndépendant/index'
import Embaucher from './Embaucher'
import Home from './Home'
import SécuritéSociale from './SécuritéSociale'
import FormulaireDétachementIndépendant from './DétachementIndépendant'
import formulaireMobilitéIndépendant from './DemandeMobilite'
export default function Gérer() {
const sitePaths = useContext(SitePathsContext)
@ -38,8 +38,8 @@ export default function Gérer() {
/>
<Route
exact
path={sitePaths.gérer.formulaireDétachement}
component={FormulaireDétachementIndépendant}
path={sitePaths.gérer.formulaireMobilité}
component={formulaireMobilitéIndépendant}
/>
</Switch>
</>

View File

@ -7,6 +7,7 @@ import SimulateurArtisteAuteur from '../Simulateurs/ArtisteAuteur'
import SimulateurAssimiléSalarié from '../Simulateurs/AssimiléSalarié'
import SimulateurAutoEntrepreneur from '../Simulateurs/AutoEntrepreneur'
import SimulateurIndépendant from '../Simulateurs/Indépendant'
import DemandeMobilite from '../Gérer/DemandeMobilite'
import IframeFooter from './IframeFooter'
import SimulateurEmbauche from './SimulateurEmbauche'
@ -42,6 +43,8 @@ export default function Iframes() {
path="/iframes/simulateur-chomage-partiel"
component={SimulateurChômagePartiel}
/>
<Route path="/iframes/demande-mobilite" component={DemandeMobilite} />
{inIframe() && <IframeFooter />}
</div>
</IsEmbeddedContext.Provider>

View File

@ -48,10 +48,8 @@ function IntegrationCustomizer() {
margin: auto;
.ui__.left-side {
width: 40%;
padding-right: 25px;
margin-right: 35px;
border-right: 2px solid var(--lighterColor);
width: 30%;
padding-right: 1rem;
select {
padding: 7px;
@ -59,7 +57,8 @@ function IntegrationCustomizer() {
}
.ui__.right-side {
width: 60%;
width: 70%;
padding-left: 1rem;
}
@media (max-width: 800px) {
@ -127,14 +126,23 @@ function IntegrationCustomizer() {
<h3>
<Trans>Prévisualisation</Trans>
</h3>
<MemoryRouter
key={currentModule}
initialEntries={[`/iframes/${currentModule}`]}
<div
css={`
background-color: white;
border: 2px solid var(--lighterColor);
border-radius: 0.3rem;
padding: 1rem;
`}
>
<ThemeColorsProvider color={color}>
<Iframes />
</ThemeColorsProvider>
</MemoryRouter>
<MemoryRouter
key={currentModule}
initialEntries={[`/iframes/${currentModule}`]}
>
<ThemeColorsProvider color={color}>
<Iframes />
</ThemeColorsProvider>
</MemoryRouter>
</div>
</div>
</div>
</div>

View File

@ -45,7 +45,7 @@ const sitePathsFr = {
embaucher: '/embaucher',
sécuritéSociale: '/sécurité-sociale',
déclarationIndépendant: '/aide-declaration-independants',
formulaireDétachement: '/demande-détachement'
formulaireMobilité: '/demande-mobilité'
},
simulateurs: {
index: '/simulateurs',
@ -95,7 +95,7 @@ const sitePathsEn = {
embaucher: '/hiring',
sécuritéSociale: '/social-security',
déclarationIndépendant: '/declaration-aid-independent',
formulaireDétachement: '/posting-demand'
formulaireMobilité: '/posting-demand'
},
simulateurs: {
index: '/simulators',

View File

@ -148,7 +148,7 @@ export function formatValue(
return '-'
}
return typeof nodeValue === 'string'
? capitalise0(nodeValue)
? capitalise0(nodeValue.replace('\\n', '\n'))
: typeof value === 'object' && 'API' in value && value.API === 'commune'
? formatCommune(nodeValue as Commune)
: typeof nodeValue === 'object'

View File

@ -16,7 +16,7 @@ const moo = require("moo");
const dateRegexp = `(?:(?:0?[1-9]|[12][0-9]|3[01])\\/)?(?:0?[1-9]|1[012])\\/\\d{4}`
const letter = '[a-zA-Z\u00C0-\u017F€$%]';
const letterOrNumber = '[a-zA-Z\u00C0-\u017F0-9\']';
const letterOrNumber = '[a-zA-Z\u00C0-\u017F0-9\'°]';
const word = `${letter}(?:[-']?${letterOrNumber}+)*`;
const wordOrNumber = `(?:${word}|${letterOrNumber}+)`
const words = `${word}(?:[\\s]?${wordOrNumber}+)*`

View File

@ -847,6 +847,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.9.6":
version "7.10.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@ -9992,6 +9999,17 @@ react-side-effect@^1.1.0:
dependencies:
shallowequal "^1.0.1"
react-signature-pad-wrapper@^1.2.11:
version "1.2.11"
resolved "https://registry.yarnpkg.com/react-signature-pad-wrapper/-/react-signature-pad-wrapper-1.2.11.tgz#fb017012611e2e5c09b5210d35d56c1180f19137"
integrity sha512-SPa5LtK6K9gvaAUx1w7Qek8U1GTeSv4mGWqZUpj7bDCzAKkM4g4XIXx6V89odV8VGVWHjjllRzkcFM6talmOsA==
dependencies:
"@babel/runtime" "^7.9.6"
prop-types "^15.7.2"
react "^16.13.1"
signature_pad "^2.3.2"
throttle-debounce "^2.1.0"
react-smooth@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.5.tgz#94ae161d7951cdd893ccb7099d031d342cb762ad"
@ -10927,6 +10945,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
signature_pad@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/signature_pad/-/signature_pad-2.3.2.tgz#ca7230021c89cedeead27b33d8d16ff254e5f04a"
integrity sha512-peYXLxOsIY6MES2TrRLDiNg2T++8gGbpP2yaC+6Ohtxr+a2dzoaqWosWDY9sWqTAAk6E/TyQO+LJw9zQwyu5kA==
simple-concat@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
@ -11672,6 +11695,11 @@ throat@^4.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
throttle-debounce@^2.1.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.2.1.tgz#fbd933ae6793448816f7d5b3cae259d464c98137"
integrity sha512-i9hAVld1f+woAiyNGqWelpDD5W1tpMroL3NofTz9xzwq6acWBlO2dC8k5EFSZepU6oOINtV5Q3aSPoRg7o4+fA==
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"