Poursuite de la migration TypeScript

* Utilisation de la version stable de TypeScript 3.7

* Début de migration du State Redux. Plutôt que de redéfinir les types
  en doublon par rapport aux actions et reducers, on utilise les valeurs
  retournées par ces fonctions comme source pour les types globaux.

* Modification de tsconfig pour meilleur typage dans VS Code

* Meilleur typage de l'environnement : suppression de @types/node qui
  était trop large (contient tout l'environnement serveur), et
  remplacement par @types/webpack-env. Par ailleurs typage des variables
  d'environnement utilisées.

* Début de migration de l'économie collaborative

* Migration de nombreux composants UI

* Mise à jour de dépendances pour récupérer un meilleur typage

* Ajout d'un hook pour configurer les simulateurs

* Suppression du higher-order component "withSitePaths", on utilise
  systématiquement le hook useContext.

L'essentiel de l'application est maintenant migré, reste le moteur !
pull/782/head
Maxime Quandalle 2019-11-10 16:57:44 +01:00
parent 49dbac252c
commit 7e2a4085a7
141 changed files with 2501 additions and 2300 deletions

View File

@ -44,7 +44,7 @@
"react-i18next": "^11.0.0",
"react-loading-skeleton": "^1.1.2",
"react-markdown": "^4.1.0",
"react-number-format": "^4.0.8",
"react-number-format": "^4.3.1",
"react-redux": "^7.0.3",
"react-router": "^5.1.1",
"react-router-dom": "^5.1.1",
@ -53,8 +53,8 @@
"react-transition-group": "^2.2.1",
"react-virtualized": "^9.20.0",
"react-virtualized-select": "^3.1.3",
"reduce-reducers": "^0.1.2",
"redux": "^3.7.2",
"reduce-reducers": "^1.0.4",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.3",
"reselect": "^4.0.0",
@ -103,17 +103,20 @@
"@babel/preset-flow": "^7.0.0-beta.51",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.0",
"@types/classnames": "^2.2.9",
"@types/iframe-resizer": "^3.5.7",
"@types/node": "^12.11.7",
"@types/ramda": "^0.26.33",
"@types/raven-for-redux": "^1.1.1",
"@types/react": "^16.9.11",
"@types/react-addons-css-transition-group": "^15.0.5",
"@types/react-color": "^3.0.1",
"@types/react-dom": "^16.9.3",
"@types/react-helmet": "^5.0.13",
"@types/react-redux": "^7.1.5",
"@types/react-router": "^5.1.2",
"@types/react-router-dom": "^5.1.0",
"@types/styled-components": "^4.1.19",
"@types/webpack-env": "^1.14.1",
"akh": "^3.1.2",
"autoprefixer": "^9.3.1",
"babel-eslint": "^11.0.0-beta.0",
@ -155,7 +158,7 @@
"mock-local-storage": "^1.0.5",
"nearley-loader": "^2.0.0",
"postcss-loader": "^2.1.2",
"prettier": "^1.16.4",
"prettier": "^1.19.1",
"ramda-fantasy": "^0.8.0",
"raw-loader": "^0.5.1",
"react-hot-loader": "^4.12.15",
@ -167,7 +170,7 @@
"style-loader": "^0.23.1",
"styled-components": "^4.2.0",
"toml-loader": "^1.0.0",
"typescript": "^3.7.1-rc",
"typescript": "^3.7.2",
"url-loader": "^1.0.1",
"webpack": "^4.39.3",
"webpack-cli": "^3.1.2",

View File

@ -1,17 +1,25 @@
import { ThemeColoursProvider } from 'Components/utils/withColours'
import { SitePathProvider } from 'Components/utils/withSitePaths'
import { SitePathProvider, SitePaths } from 'Components/utils/withSitePaths'
import { TrackerProvider } from 'Components/utils/withTracker'
import { createBrowserHistory } from 'history'
import { createBrowserHistory, History } from 'history'
import { AvailableLangs } from 'i18n'
import i18next from 'i18next'
import React, { PureComponent } from 'react'
import { I18nextProvider } from 'react-i18next'
import { Provider as ReduxProvider } from 'react-redux'
import { Router } from 'react-router-dom'
import reducers from 'Reducers/rootReducer'
import { applyMiddleware, compose, createStore } from 'redux'
import reducers, { RootState } from 'Reducers/rootReducer'
import { applyMiddleware, compose, createStore, Middleware, Store } from 'redux'
import thunk from 'redux-thunk'
import Tracker from 'Tracker'
import { inIframe } from './utils'
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any
}
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
if (
@ -33,8 +41,20 @@ if (
})
}
export default class Provider extends PureComponent {
constructor(props) {
type ProviderProps = {
tracker: Tracker
basename: string
sitePaths: SitePaths
language: AvailableLangs
initialStore: RootState
onStoreCreated: (store: Store) => void
reduxMiddlewares: Array<Middleware>
}
export default class Provider extends PureComponent<ProviderProps> {
history: History
store: Store
constructor(props: ProviderProps) {
super(props)
this.history = createBrowserHistory({
basename: process.env.NODE_ENV === 'production' ? '' : this.props.basename
@ -56,7 +76,7 @@ export default class Provider extends PureComponent {
this.props.initialStore.lang = this.props.language
}
this.store = createStore(reducers, this.props.initialStore, storeEnhancer)
this.props.onStoreCreated && this.props.onStoreCreated(this.store)
this.props.onStoreCreated?.(this.store)
// Remove loader
var css = document.createElement('style')
@ -82,7 +102,8 @@ export default class Provider extends PureComponent {
// If IE < 11 display nothing
<ReduxProvider store={this.store}>
<ThemeColoursProvider
colour={iframeCouleur && decodeURIComponent(iframeCouleur)}>
colour={iframeCouleur && decodeURIComponent(iframeCouleur)}
>
<TrackerProvider value={this.props.tracker}>
<SitePathProvider value={this.props.sitePaths}>
<I18nextProvider i18n={i18next}>

View File

@ -1,17 +1,14 @@
import { History, Location } from 'history'
import { debounce } from './utils'
declare global {
interface Window { _paq: any; }
interface Window {
_paq: any
}
}
function push(args: ['trackPageView']): void
function push(args: ['trackEvent']): void
function push(args: ['trackEvent', string, string]): void
function push(args: ['trackEvent', string, string, string]): void
function push(args: ['trackEvent', string, string, string, string, string]): void
function push(args: ['trackEvent', string, string, string, number]): void
function push() {}
type PushType = typeof push
type PushArgs = ['trackPageView'] | ['trackEvent', ...Array<string | number>]
type PushType = (args: PushArgs) => void
export default class Tracker {
push: PushType
@ -23,7 +20,7 @@ export default class Tracker {
this.push = debounce(200, pushFunction) as PushType
}
connectToHistory(history) {
connectToHistory(history: History) {
this.unlistenFromHistory = history.listen(loc => {
this.track(loc)
})
@ -52,5 +49,5 @@ export default class Tracker {
}
export const devTracker = new Tracker(
(console?.log?.bind(console)) ?? (() => {}) // eslint-disable-line no-console
console?.log?.bind(console) ?? (() => {}) // eslint-disable-line no-console
)

View File

@ -1,98 +0,0 @@
import type {
ResetSimulationAction,
LoadPreviousSimulationAction,
StepAction,
DeletePreviousSimulationAction,
SetSimulationConfigAction,
SetSituationBranchAction
} from 'Types/ActionsTypes'
import { deletePersistedSimulation } from '../storage/persistSimulation'
import type { Thunk } from 'Types/ActionsTypes'
export const resetSimulation = () => (dispatch: any => void): void => {
dispatch(
({
type: 'RESET_SIMULATION'
}: ResetSimulationAction)
)
}
export const goToQuestion = (question: string): StepAction => ({
type: 'STEP_ACTION',
name: 'unfold',
step: question
})
export const validateStepWithValue = (
dottedName,
value: any
): Thunk<StepAction> => dispatch => {
dispatch(updateSituation(dottedName, value))
dispatch({
type: 'STEP_ACTION',
name: 'fold',
step: dottedName
})
}
export const setSituationBranch = (id: number): SetSituationBranchAction => ({
type: 'SET_SITUATION_BRANCH',
id
})
export const setSimulationConfig = (
config: Object
): Thunk<SetSimulationConfigAction> => (dispatch, _, { history }): void => {
const url = history.location.pathname
dispatch({
type: 'SET_SIMULATION',
url,
config
})
}
export const deletePreviousSimulation = () => (
dispatch: DeletePreviousSimulationAction => void
) => {
dispatch({
type: 'DELETE_PREVIOUS_SIMULATION'
})
deletePersistedSimulation()
}
export const updateSituation = (fieldName, value) => ({
type: 'UPDATE_SITUATION',
fieldName,
value
})
export const updatePeriod = toPeriod => ({
type: 'UPDATE_PERIOD',
toPeriod
})
// $FlowFixMe
export function setExample(name, situation, dottedName) {
return { type: 'SET_EXAMPLE', name, situation, dottedName }
}
export const goBackToSimulation = (): Thunk<any> => (
dispatch,
getState,
{ history }
) => {
dispatch({ type: 'SET_EXEMPLE', name: null })
history.push(getState().simulation.url)
}
export function loadPreviousSimulation(): LoadPreviousSimulationAction {
return {
type: 'LOAD_PREVIOUS_SIMULATION'
}
}
export function hideControl(id: string) {
return { type: 'HIDE_CONTROL', id }
}
export const EXPLAIN_VARIABLE = 'EXPLAIN_VARIABLE'

157
source/actions/actions.ts Normal file
View File

@ -0,0 +1,157 @@
import { SitePaths } from 'Components/utils/withSitePaths'
import { History } from 'history'
import { RootState } from 'Reducers/rootReducer'
import { ThunkAction } from 'redux-thunk'
import { DottedName } from 'Types/rule'
import { deletePersistedSimulation } from '../storage/persistSimulation'
export type Action =
| ResetSimulationAction
| StepAction
| UpdateAction
| SetSimulationConfigAction
| DeletePreviousSimulationAction
| SetExempleAction
| ExplainVariableAction
| UpdatePeriodAction
| HideControlAction
| LoadPreviousSimulationAction
| SetSituationBranchAction
| SetActiveTargetAction
type ThunkResult<R> = ThunkAction<
R,
RootState,
{ history: History; sitePaths: SitePaths },
Action
>
type StepAction = {
type: 'STEP_ACTION'
name: 'fold' | 'unfold'
step: string
}
type SetSimulationConfigAction = {
type: 'SET_SIMULATION'
url: string
config: Object
}
type DeletePreviousSimulationAction = {
type: 'DELETE_PREVIOUS_SIMULATION'
}
type SetExempleAction = {
type: 'SET_EXAMPLE'
name: null | string
situation?: object
dottedName?: string
}
type ResetSimulationAction = ReturnType<typeof resetSimulation>
type UpdateAction = ReturnType<typeof updateSituation>
type UpdatePeriodAction = ReturnType<typeof updatePeriod>
type LoadPreviousSimulationAction = ReturnType<typeof loadPreviousSimulation>
type SetSituationBranchAction = ReturnType<typeof setSituationBranch>
type SetActiveTargetAction = ReturnType<typeof setActiveTarget>
type HideControlAction = ReturnType<typeof hideControl>
type ExplainVariableAction = ReturnType<typeof explainVariable>
export const resetSimulation = () =>
({
type: 'RESET_SIMULATION'
} as const)
export const goToQuestion = (question: string) =>
({
type: 'STEP_ACTION',
name: 'unfold',
step: question
} as const)
export const validateStepWithValue = (
dottedName: DottedName,
value: any
): ThunkResult<void> => dispatch => {
dispatch(updateSituation(dottedName, value))
dispatch({
type: 'STEP_ACTION',
name: 'fold',
step: dottedName
})
}
export const setSituationBranch = (id: number) =>
({
type: 'SET_SITUATION_BRANCH',
id
} as const)
export const setSimulationConfig = (config: Object): ThunkResult<void> => (
dispatch,
_,
{ history }
): void => {
const url = history.location.pathname
dispatch({
type: 'SET_SIMULATION',
url,
config
})
}
export const setActiveTarget = (targetName: string) =>
({
type: 'SET_ACTIVE_TARGET_INPUT',
name: targetName
} as const)
export const deletePreviousSimulation = (): ThunkResult<void> => dispatch => {
dispatch({
type: 'DELETE_PREVIOUS_SIMULATION'
})
deletePersistedSimulation()
}
export const updateSituation = (fieldName: DottedName, value: any) =>
({
type: 'UPDATE_SITUATION',
fieldName,
value
} as const)
export const updatePeriod = (toPeriod: string) =>
({
type: 'UPDATE_PERIOD',
toPeriod
} as const)
export function setExample(name: string, situation, dottedName: string) {
return { type: 'SET_EXAMPLE', name, situation, dottedName } as const
}
export const goBackToSimulation = (): ThunkResult<void> => (
dispatch,
getState,
{ history }
) => {
dispatch({ type: 'SET_EXAMPLE', name: null })
history.push(getState().simulation.url)
}
export function loadPreviousSimulation() {
return {
type: 'LOAD_PREVIOUS_SIMULATION'
} as const
}
export function hideControl(id: string) {
return { type: 'HIDE_CONTROL', id } as const
}
export const explainVariable = (variableName = null) =>
({
type: 'EXPLAIN_VARIABLE',
variableName
} as const)

View File

@ -1,21 +0,0 @@
import type {
InitializeCompanyCreationChecklistAction,
CheckCompanyCreationItemAction
} from 'Types/companyCreationChecklistTypes'
export const initializeCompanyCreationChecklist = (
statusName: string,
checklistItems: Array<string>
) =>
({
type: 'INITIALIZE_COMPANY_CREATION_CHECKLIST',
checklistItems,
statusName
}: InitializeCompanyCreationChecklistAction)
export const checkCompanyCreationItem = (name: string, checked: boolean) =>
({
type: 'CHECK_COMPANY_CREATION_ITEM',
name,
checked
}: CheckCompanyCreationItemAction)

View File

@ -0,0 +1,29 @@
import { LegalStatus } from 'Selectors/companyStatusSelectors'
export type Action =
| InitializeCompanyCreationChecklistAction
| CheckCompanyCreationItemAction
type InitializeCompanyCreationChecklistAction = ReturnType<
typeof initializeCompanyCreationChecklist
>
type CheckCompanyCreationItemAction = ReturnType<
typeof checkCompanyCreationItem
>
export const initializeCompanyCreationChecklist = (
statusName: LegalStatus,
checklistItems: Array<string>
) =>
({
type: 'INITIALIZE_COMPANY_CREATION_CHECKLIST',
checklistItems,
statusName
} as const)
export const checkCompanyCreationItem = (name: string, checked: boolean) =>
({
type: 'CHECK_COMPANY_CREATION_ITEM',
name,
checked
} as const)

View File

@ -1,6 +1,6 @@
import { fetchCompanyDetails } from '../api/sirene'
const fetchCommuneDetails = function (codeCommune) {
const fetchCommuneDetails = function(codeCommune) {
return fetch(
`https://geo.api.gouv.fr/communes/${codeCommune}?fields=departement,region`
).then(response => {

View File

@ -1,17 +1,17 @@
import type {
InitializeHiringChecklistAction,
CheckHiringItemAction
} from 'Types/hiringChecklistTypes'
export type Action = InitializeHiringChecklistAction | CheckHiringItemAction
type InitializeHiringChecklistAction = ReturnType<typeof initializeHiringChecklist>
type CheckHiringItemAction = ReturnType<typeof checkHiringItem>
export const initializeHiringChecklist = (checklistItems: Array<string>) =>
({
type: 'INITIALIZE_HIRING_CHECKLIST',
checklistItems
}: InitializeHiringChecklistAction)
} as const)
export const checkHiringItem = (name: string, checked: boolean) =>
({
type: 'CHECK_HIRING_ITEM',
name,
checked
}: CheckHiringItemAction)
} as const)

View File

@ -1,7 +1,7 @@
const isSIREN = input => input.match(/^[\s]*([\d][\s]*){9}$/)
const isSIRET = input => input.match(/^[\s]*([\d][\s]*){14}$/)
const isSIREN = (input: string) => input.match(/^[\s]*([\d][\s]*){9}$/)
const isSIRET = (input: string) => input.match(/^[\s]*([\d][\s]*){14}$/)
export async function fetchCompanyDetails(siren) {
export async function fetchCompanyDetails(siren: string) {
const response = await fetch(
`https://entreprise.data.gouv.fr/api/sirene/v3/unites_legales/${siren.replace(
/[\s]/g,
@ -15,7 +15,7 @@ export async function fetchCompanyDetails(siren) {
return json.unite_legale
}
export async function searchDenominationOrSiren(value) {
export async function searchDenominationOrSiren(value: string) {
if (isSIRET(value)) {
value = value.replace(/[\s]/g, '').slice(0, 9)
}
@ -25,7 +25,12 @@ export async function searchDenominationOrSiren(value) {
return searchFullText(value)
}
async function searchFullText(text) {
export type Etablissement = {
siren: string
denomination?: string
}
async function searchFullText(text: string): Promise<Array<Etablissement>> {
const response = await fetch(
`https://entreprise.data.gouv.fr/api/sirene/v1/full_text/${text}?per_page=5`
)

View File

@ -1,30 +0,0 @@
import React from 'react'
import emoji from 'react-easy-emoji'
import { connect } from 'react-redux'
import { firstStepCompletedSelector } from 'Selectors/analyseSelectors'
import Animate from 'Ui/animate'
import './Banner.css'
import type { Node } from 'react'
import type { State } from 'Types/State'
type PropTypes = {
hidden: boolean,
children: Node,
icon?: string
}
let Banner = ({ hidden = false, children, icon }: PropTypes) =>
!hidden ? (
<Animate.fadeIn>
<div className="ui__ banner">
{icon && emoji(icon)}
<p>{children}</p>
</div>
</Animate.fadeIn>
) : null
export default connect(
(state: State, { hidden }: PropTypes) => ({
hidden: hidden || firstStepCompletedSelector(state)
}),
{}
)(Banner)

View File

@ -0,0 +1,29 @@
import React from 'react'
import emoji from 'react-easy-emoji'
import { useSelector } from 'react-redux'
import { firstStepCompletedSelector } from 'Selectors/analyseSelectors'
import Animate from 'Ui/animate'
import './Banner.css'
type BannerProps = {
children: React.ReactNode
hidden?: boolean
icon?: string
}
export default function Banner({
children,
hidden: hiddenProp = false,
icon
}: BannerProps) {
const hiddenState = useSelector(firstStepCompletedSelector)
const hidden = hiddenProp || hiddenState
return !hidden ? (
<Animate.fadeIn>
<div className="ui__ banner">
{icon && emoji(icon)}
<p>{children}</p>
</div>
</Animate.fadeIn>
) : null
}

View File

@ -2,9 +2,9 @@ import { T } from 'Components'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Skeleton from 'react-loading-skeleton'
import { fetchCompanyDetails } from '../api/sirene'
import { Etablissement, fetchCompanyDetails } from '../api/sirene'
export default function CompanyDetails({ siren, denomination }) {
export default function CompanyDetails({ siren, denomination }: Etablissement) {
const { i18n } = useTranslation()
const DateFormatter = useMemo(
() =>

View File

@ -1,10 +1,18 @@
import classnames from 'classnames'
import React, { useRef, useState } from 'react'
import { currencyFormat } from 'Engine/format'
import NumberFormat from 'react-number-format'
import React, { useRef, useState } from 'react'
import NumberFormat, { NumberFormatProps } from 'react-number-format'
import { debounce } from '../../utils'
import './CurrencyInput.css'
type CurrencyInputProps = NumberFormatProps & {
value?: string | number
debounce?: number
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
currencySymbol?: string
language?: Parameters<typeof currencyFormat>[0]
}
export default function CurrencyInput({
value: valueProp = '',
debounce: debounceTimeout,
@ -13,7 +21,7 @@ export default function CurrencyInput({
language,
className,
...forwardedProps
}) {
}: CurrencyInputProps) {
const [initialValue, setInitialValue] = useState(valueProp)
const [currentValue, setCurrentValue] = useState(valueProp)
const onChangeDebounced = useRef(
@ -23,7 +31,7 @@ export default function CurrencyInput({
// the DOM `event` in its custom `onValueChange` handler
const nextValue = useRef(null)
const inputRef = useRef()
const inputRef = useRef<HTMLInputElement>()
// When the component is rendered with a new "value" prop, we reset our local state
if (valueProp !== initialValue) {
@ -31,7 +39,7 @@ export default function CurrencyInput({
setInitialValue(valueProp)
}
const handleChange = event => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// Only trigger the `onChange` event if the value has changed -- and not
// only its formating, we don't want to call it when a dot is added in `12.`
// for instance
@ -63,7 +71,8 @@ export default function CurrencyInput({
<div
className={classnames(className, 'currencyInput__container')}
{...(valueLength > 5 ? { style: { width } } : {})}
onClick={() => inputRef.current.focus()}>
onClick={() => inputRef.current.focus()}
>
{!currentValue && isCurrencyPrefixed && currencySymbol}
<NumberFormat
{...forwardedProps}

View File

@ -1,21 +1,19 @@
import { ScrollToElement } from 'Components/utils/Scroll'
import withTracker from 'Components/utils/withTracker'
import React, { useRef } from 'react'
import { TrackerContext } from 'Components/utils/withTracker'
import React, { useContext, useRef } from 'react'
import { Trans } from 'react-i18next'
import type { Tracker } from 'Components/utils/withTracker'
type Props = { onEnd: () => void, tracker: Tracker, onCancel: () => void }
type Props = { onEnd: () => void; onCancel: () => void }
function FeedbackForm({ onEnd, onCancel, tracker }: Props) {
const formRef = useRef()
export default function FeedbackForm({ onEnd, onCancel }: Props) {
const formRef = useRef<HTMLFormElement>()
const tracker = useContext(TrackerContext)
const handleFormSubmit = e => {
const handleFormSubmit = (e: React.FormEvent): void => {
tracker.push(['trackEvent', 'Feedback', 'written feedback submitted'])
e.preventDefault()
fetch('/', {
method: 'POST',
// $FlowFixMe
body: new FormData(formRef.current)
})
onEnd()
@ -28,7 +26,8 @@ function FeedbackForm({ onEnd, onCancel, tracker }: Props) {
onClick={() => onCancel()}
className="ui__ link-button"
style={{ textDecoration: 'none', marginLeft: '0.3rem' }}
aria-label="close">
aria-label="close"
>
X
</button>
</div>
@ -37,7 +36,8 @@ function FeedbackForm({ onEnd, onCancel, tracker }: Props) {
style={{ flex: 1 }}
method="post"
ref={formRef}
onSubmit={handleFormSubmit}>
onSubmit={handleFormSubmit}
>
<input type="hidden" name="form-name" value="feedback" />
<label htmlFor="message">
<p>
@ -50,7 +50,7 @@ function FeedbackForm({ onEnd, onCancel, tracker }: Props) {
</label>
<textarea
name="message"
rows="5"
rows={5}
style={{ resize: 'none', width: '100%', padding: '0.6rem' }}
/>
<br />
@ -75,5 +75,3 @@ function FeedbackForm({ onEnd, onCancel, tracker }: Props) {
</ScrollToElement>
)
}
export default withTracker(FeedbackForm)

View File

@ -1,170 +0,0 @@
import withTracker from 'Components/utils/withTracker'
import React, { Component } from 'react'
import { Trans } from 'react-i18next'
import { withRouter } from 'react-router'
import { compose } from 'redux'
import safeLocalStorage from '../../storage/safeLocalStorage'
import './Feedback.css'
import Form from './FeedbackForm'
import type { Tracker } from 'Components/utils/withTracker'
import type { Location } from 'react-router-dom'
import type { Node } from 'react'
type OwnProps = {
blacklist: Array<string>,
customMessage?: Node,
customEventName?: string
}
type Props = OwnProps & {
location: Location,
tracker: Tracker
}
type State = {
showForm: boolean,
feedbackAlreadyGiven: boolean,
showThanks: boolean
}
const localStorageKey = (feedback: [string, string]) =>
`app::feedback::${feedback.join('::')}`
const saveFeedbackOccurrenceInLocalStorage = ([name, path, rating]: [
string,
string,
number
]) => {
safeLocalStorage.setItem(
localStorageKey([name, path]),
JSON.stringify(rating)
)
}
const feedbackAlreadyGiven = (feedback: [string, string]) => {
return !!safeLocalStorage.getItem(localStorageKey(feedback))
}
class PageFeedback extends Component<Props, State> {
static defaultProps = {
blacklist: []
}
feedbackAlreadyGiven: boolean
feedbackAlreadyGiven = false
constructor(props) {
super(props)
this.state = {
showForm: false,
showThanks: false,
feedbackAlreadyGiven: feedbackAlreadyGiven([
this.props.customEventName || 'rate page usefulness',
this.props.location.pathname
])
}
}
handleFeedback = ({ useful }) => {
this.props.tracker.push([
'trackEvent',
'Feedback',
useful ? 'positive rating' : 'negative rating',
this.props.location.pathname
])
const feedback = [
this.props.customEventName || 'rate page usefulness',
this.props.location.pathname,
useful ? 10 : 0.1
]
this.props.tracker.push(['trackEvent', 'Feedback', ...feedback])
saveFeedbackOccurrenceInLocalStorage(feedback)
this.setState({
showThanks: useful,
feedbackAlreadyGiven: true,
showForm: !useful
})
}
handleErrorReporting = () => {
this.props.tracker.push([
'trackEvent',
'Feedback',
'report error',
this.props.location.pathname
])
this.setState({ showForm: true })
}
render() {
if (
this.state.feedbackAlreadyGiven &&
!this.state.showForm &&
!this.state.showThanks
) {
return null
}
const pathname =
this.props.location.pathname === '/' ? '' : this.props.location.pathname
return (
!this.props.blacklist.includes(pathname) && (
<div
className="ui__ container"
style={{ display: 'flex', justifyContent: 'center' }}>
<div className="feedback-page ui__ notice ">
{!this.state.showForm && !this.state.showThanks && (
<>
<div style={{ flexShrink: 0 }}>
{this.props.customMessage || (
<Trans i18nKey="feedback.question">
Cette page vous est utile ?
</Trans>
)}{' '}
</div>
<div className="feedbackButtons">
<button
className="ui__ link-button"
onClick={() => this.handleFeedback({ useful: true })}>
<Trans>Oui</Trans>
</button>{' '}
<button
className="ui__ link-button"
onClick={() => this.handleFeedback({ useful: false })}>
<Trans>Non</Trans>
</button>
<button
className="ui__ link-button"
onClick={this.handleErrorReporting}>
<Trans i18nKey="feedback.reportError">
Faire une suggestion
</Trans>
</button>
</div>
</>
)}
{this.state.showThanks && (
<div>
<Trans i18nKey="feedback.thanks">
Merci pour votre retour ! Vous pouvez nous contacter
directement à{' '}
<a href="mailto:contact@mon-entreprise.beta.gouv.fr">
contact@mon-entreprise.beta.gouv.fr
</a>
</Trans>
</div>
)}
{this.state.showForm && (
<Form
onEnd={() =>
this.setState({ showThanks: true, showForm: false })
}
onCancel={() =>
this.setState({ showThanks: false, showForm: false })
}
/>
)}
</div>
</div>
)
)
}
}
const PageFeedbackWithRouter = ({ location, ...props }) => (
<PageFeedback {...props} location={location} key={location.pathname} />
)
export default compose(
withRouter,
withTracker
)(PageFeedbackWithRouter)

View File

@ -0,0 +1,144 @@
import { TrackerContext } from 'Components/utils/withTracker'
import React, { useCallback, useContext, useState } from 'react'
import { Trans } from 'react-i18next'
import { useLocation } from 'react-router'
import safeLocalStorage from '../../storage/safeLocalStorage'
import './Feedback.css'
import Form from './FeedbackForm'
type PageFeedbackProps = {
blacklist?: Array<string>
customMessage?: React.ReactNode
customEventName?: string
}
const localStorageKey = (feedback: [string, string]) =>
`app::feedback::${feedback.join('::')}`
const saveFeedbackOccurrenceInLocalStorage = ([name, path, rating]: [
string,
string,
number
]) => {
safeLocalStorage.setItem(
localStorageKey([name, path]),
JSON.stringify(rating)
)
}
const feedbackAlreadyGiven = (feedback: [string, string]) => {
return !!safeLocalStorage.getItem(localStorageKey(feedback))
}
export default function PageFeedback({
blacklist = [],
customMessage,
customEventName
}: PageFeedbackProps) {
const location = useLocation()
const tracker = useContext(TrackerContext)
const [state, setState] = useState({
showForm: false,
showThanks: false,
feedbackAlreadyGiven: feedbackAlreadyGiven([
customEventName || 'rate page usefulness',
location.pathname
])
})
const handleFeedback = useCallback(({ useful }: { useful: boolean }) => {
tracker.push([
'trackEvent',
'Feedback',
useful ? 'positive rating' : 'negative rating',
location.pathname
])
const feedback = [
customEventName || 'rate page usefulness',
location.pathname,
useful ? 10 : 0.1
] as [string, string, number]
tracker.push(['trackEvent', 'Feedback', ...feedback])
saveFeedbackOccurrenceInLocalStorage(feedback)
setState({
showThanks: useful,
feedbackAlreadyGiven: true,
showForm: !useful
})
}, [])
const handleErrorReporting = useCallback(() => {
tracker.push(['trackEvent', 'Feedback', 'report error', location.pathname])
setState({ ...state, showForm: true })
}, [])
if (state.feedbackAlreadyGiven && !state.showForm && !state.showThanks) {
return null
}
const pathname = location.pathname === '/' ? '' : location.pathname
if (blacklist.includes(pathname)) {
return null
}
return (
<div
className="ui__ container"
style={{ display: 'flex', justifyContent: 'center' }}
>
<div className="feedback-page ui__ notice ">
{!state.showForm && !state.showThanks && (
<>
<div style={{ flexShrink: 0 }}>
{customMessage || (
<Trans i18nKey="feedback.question">
Cette page vous est utile ?
</Trans>
)}{' '}
</div>
<div className="feedbackButtons">
<button
className="ui__ link-button"
onClick={() => handleFeedback({ useful: true })}
>
<Trans>Oui</Trans>
</button>{' '}
<button
className="ui__ link-button"
onClick={() => handleFeedback({ useful: false })}
>
<Trans>Non</Trans>
</button>
<button
className="ui__ link-button"
onClick={handleErrorReporting}
>
<Trans i18nKey="feedback.reportError">
Faire une suggestion
</Trans>
</button>
</div>
</>
)}
{state.showThanks && (
<div>
<Trans i18nKey="feedback.thanks">
Merci pour votre retour ! Vous pouvez nous contacter directement à{' '}
<a href="mailto:contact@mon-entreprise.beta.gouv.fr">
contact@mon-entreprise.beta.gouv.fr
</a>
</Trans>
</div>
)}
{state.showForm && (
<Form
onEnd={() =>
setState({ ...state, showThanks: true, showForm: false })
}
onCancel={() =>
setState({ ...state, showThanks: false, showForm: false })
}
/>
)}
</div>
</div>
)
}

View File

@ -3,11 +3,11 @@ import { T } from 'Components'
import CompanyDetails from 'Components/CompanyDetails'
import React, { useCallback, useMemo, useState } from 'react'
import { useDispatch } from 'react-redux'
import { searchDenominationOrSiren } from '../api/sirene'
import { Etablissement, searchDenominationOrSiren } from '../api/sirene'
import { debounce } from '../utils'
export default function Search() {
const [searchResults, setSearchResults] = useState()
const [searchResults, setSearchResults] = useState<Array<Etablissement>>()
const [isLoading, setLoadingState] = useState(false)
const handleSearch = useCallback(
@ -85,7 +85,8 @@ export default function Search() {
:focus {
background-color: var(--lighterColour);
}
`}>
`}
>
<CompanyDetails siren={siren} denomination={denomination} />
</button>
))}

View File

@ -43,7 +43,8 @@ export default function Newsletter() {
onSubmit={onSubmit}
id="mc-embedded-subscribe-form"
name="mc-embedded-subscribe-form"
target="_blank">
target="_blank"
>
<div>
<label htmlFor="mce-EMAIL">
<T>Votre adresse e-mail</T>
@ -53,7 +54,7 @@ export default function Newsletter() {
<input
className="ui__ plain small button"
type="submit"
value={t("S'inscrire")}
value={t("S'inscrire") as string}
name="subscribe"
id="mc-embedded-subscribe"
/>

View File

@ -3,15 +3,24 @@ import React, { useEffect } from 'react'
import * as animate from 'Ui/animate'
import { LinkButton } from 'Ui/Button'
import './Overlay.css'
export default function Overlay({ onClose, children, ...otherProps }) {
type OverlayProps = React.HTMLAttributes<HTMLDivElement> & {
onClose?: () => void
children: React.ReactNode
}
export default function Overlay({
onClose,
children,
...otherProps
}: OverlayProps) {
useEffect(() => {
const body = document.getElementsByTagName('body')[0]
body.classList.add('no-scroll');
body.classList.add('no-scroll')
return () => {
body.classList.remove('no-scroll')
}
}
, [])
}, [])
return (
<div id="overlayWrapper">
<animate.fromBottom>
@ -19,18 +28,21 @@ export default function Overlay({ onClose, children, ...otherProps }) {
focusTrapOptions={{
onDeactivate: onClose,
clickOutsideDeactivates: !!onClose
}}>
}}
>
<div
aria-modal="true"
id="overlayContent"
{...otherProps}
className={'ui__ card ' + otherProps.className}>
className={'ui__ card ' + otherProps?.className}
>
{children}
{onClose && (
<LinkButton
aria-label="close"
onClick={onClose}
id="overlayCloseButton">
id="overlayCloseButton"
>
×
</LinkButton>
)}

View File

@ -2,6 +2,7 @@ import { updatePeriod } from 'Actions/actions'
import React from 'react'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { situationSelector } from 'Selectors/analyseSelectors'
import './PeriodSwitch.css'
@ -9,7 +10,8 @@ export default function PeriodSwitch() {
const dispatch = useDispatch()
const situation = useSelector(situationSelector)
const defaultPeriod = useSelector(
state => state.simulation?.config?.situation?.période || 'année'
(state: RootState) =>
state.simulation?.config?.situation?.période || 'année'
)
const currentPeriod = situation.période
let periods = ['année', 'mois']

View File

@ -1,54 +0,0 @@
import {
deletePreviousSimulation,
loadPreviousSimulation
} from 'Actions/actions'
import { compose } from 'ramda'
import React from 'react'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { noUserInputSelector } from 'Selectors/analyseSelectors'
import { LinkButton } from 'Ui/Button'
import Banner from './Banner'
import type { SavedSimulation } from 'Types/State'
type ConnectedPropTypes = {
previousSimulation: SavedSimulation,
loadPreviousSimulation: () => void,
newSimulationStarted: boolean,
deletePreviousSimulation: () => void
}
const PreviousSimulationBanner = ({
previousSimulation,
deletePreviousSimulation,
newSimulationStarted,
loadPreviousSimulation
}: ConnectedPropTypes) => (
<Banner hidden={!previousSimulation || newSimulationStarted} icon="💾">
<Trans i18nKey="previousSimulationBanner.info">
Votre précédente simulation a été sauvegardée.
</Trans>{' '}
<LinkButton onClick={loadPreviousSimulation}>
<Trans i18nKey="previousSimulationBanner.retrieveButton">
Retrouver ma simulation
</Trans>
</LinkButton>
.{' '}
<LinkButton onClick={deletePreviousSimulation}>
<Trans>Effacer</Trans>
</LinkButton>
</Banner>
)
export default compose(
connect(
state => ({
previousSimulation: state.previousSimulation,
newSimulationStarted: !noUserInputSelector(state)
}),
{
loadPreviousSimulation,
deletePreviousSimulation
}
)
)(PreviousSimulationBanner)

View File

@ -0,0 +1,36 @@
import {
deletePreviousSimulation,
loadPreviousSimulation
} from 'Actions/actions'
import React from 'react'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { noUserInputSelector } from 'Selectors/analyseSelectors'
import { LinkButton } from 'Ui/Button'
import Banner from './Banner'
export default function PreviousSimulationBanner() {
const previousSimulation = useSelector(
(state: RootState) => state.previousSimulation
)
const newSimulationStarted = !useSelector(noUserInputSelector)
const dispatch = useDispatch()
return (
<Banner hidden={!previousSimulation || newSimulationStarted} icon="💾">
<Trans i18nKey="previousSimulationBanner.info">
Votre précédente simulation a été sauvegardée.
</Trans>{' '}
<LinkButton onClick={() => dispatch(loadPreviousSimulation())}>
<Trans i18nKey="previousSimulationBanner.retrieveButton">
Retrouver ma simulation
</Trans>
</LinkButton>
.{' '}
<LinkButton onClick={() => dispatch(deletePreviousSimulation())}>
<Trans>Effacer</Trans>
</LinkButton>
</Banner>
)
}

View File

@ -1,72 +0,0 @@
import { goToQuestion } from 'Actions/actions'
import { T } from 'Components'
import { compose, contains, filter, reject, toPairs } from 'ramda'
import React from 'react'
import { connect } from 'react-redux'
import {
currentQuestionSelector,
nextStepsSelector
} from 'Selectors/analyseSelectors'
import type { Location } from 'react-router'
type OwnProps = {
quickLinks: { [string]: string }
}
type Props = OwnProps & {
goToQuestion: string => void,
location: Location,
currentQuestion: string,
nextSteps: Array<string>,
quickLinksToHide: Array<string>,
show: boolean
}
const QuickLinks = ({
goToQuestion,
currentQuestion,
nextSteps,
quickLinks,
quickLinksToHide
}: Props) => {
if (!quickLinks) {
return null
}
const links = compose(
toPairs,
filter(dottedName => contains(dottedName, nextSteps)),
reject(dottedName => contains(dottedName, quickLinksToHide))
)(quickLinks)
return (
!!links.length && (
<span>
<small>Questions :</small>
{links.map(([label, dottedName]) => (
<button
key={dottedName}
className={`ui__ link-button ${
dottedName === currentQuestion ? 'active' : ''
}`}
css="margin: 0 0.4rem !important"
onClick={() => goToQuestion(dottedName)}>
<T k={'quicklinks.' + label}>{label}</T>
</button>
))}{' '}
{/* <button className="ui__ link-button">Voir la liste</button> */}
</span>
)
)
}
export default connect(
(state, props) => ({
key: props.language,
currentQuestion: currentQuestionSelector(state),
nextSteps: nextStepsSelector(state),
quickLinks: state.simulation?.config.questions?.["à l'affiche"],
quickLinksToHide: state.conversationSteps.foldedSteps
}),
{
goToQuestion
}
)(QuickLinks)

View File

@ -0,0 +1,52 @@
import { goToQuestion } from 'Actions/actions'
import { T } from 'Components'
import { compose, contains, filter, reject, toPairs } from 'ramda'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
currentQuestionSelector,
nextStepsSelector
} from 'Selectors/analyseSelectors'
export default function QuickLinks() {
const currentQuestion = useSelector(currentQuestionSelector)
const nextSteps = useSelector(nextStepsSelector)
const quickLinks = useSelector(
(state: RootState) => state.simulation?.config.questions?.["à l'affiche"]
)
const quickLinksToHide = useSelector(
(state: RootState) => state.conversationSteps.foldedSteps
)
const dispatch = useDispatch()
if (!quickLinks) {
return null
}
const links = compose(
toPairs,
filter(dottedName => contains(dottedName, nextSteps)) as any,
reject(dottedName => contains(dottedName, quickLinksToHide))
)(quickLinks) as any
return (
!!links.length && (
<span>
<small>Questions :</small>
{links.map(([label, dottedName]) => (
<button
key={dottedName}
className={`ui__ link-button ${
dottedName === currentQuestion ? 'active' : ''
}`}
css="margin: 0 0.4rem !important"
onClick={() => dispatch(goToQuestion(dottedName))}
>
<T k={'quicklinks.' + label}>{label}</T>
</button>
))}{' '}
{/* <button className="ui__ link-button">Voir la liste</button> */}
</span>
)
)
}

View File

@ -3,11 +3,11 @@ import { SitePathsContext } from 'Components/utils/withSitePaths'
import { encodeRuleName, nameLeaf } from 'Engine/rules'
import React, { useContext } from 'react'
import { Link } from 'react-router-dom'
import { Règle } from 'Types/RegleTypes'
import { Rule } from 'Types/rule'
import './RuleLink.css'
type RuleLinkProps = Règle & {
style: React.CSSProperties
type RuleLinkProps = Rule & {
style?: React.CSSProperties
children: React.ReactNode
}
@ -27,7 +27,8 @@ export default function RuleLink({
to={newPath}
className="rule-link"
title={title}
style={{ color: colour, ...style }}>
style={{ color: colour, ...style }}
>
{children || title || nameLeaf(dottedName)}
</Link>
)

View File

@ -1,28 +1,28 @@
import { goBackToSimulation } from 'Actions/actions'
import { ScrollToTop } from 'Components/utils/Scroll'
import { decodeRuleName, findRuleByDottedName } from 'Engine/rules.js'
import { compose } from 'ramda'
import React from 'react'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { connect, useSelector } from 'react-redux'
import { Redirect } from 'react-router-dom'
import { flatRulesSelector, noUserInputSelector, situationBranchNameSelector } from 'Selectors/analyseSelectors'
import {
flatRulesSelector,
noUserInputSelector,
situationBranchNameSelector
} from 'Selectors/analyseSelectors'
import { DottedName } from 'Types/rule'
import Rule from './rule/Rule'
import './RulePage.css'
import SearchButton from './SearchButton'
export default compose(
connect(state => ({
valuesToShow: !noUserInputSelector(state),
flatRules: flatRulesSelector(state),
brancheName: situationBranchNameSelector(state)
}))
)(function RulePage({ flatRules, match, valuesToShow, brancheName }) {
let name = match ?.params ?.name,
export default function RulePage({ match }) {
const flatRules = useSelector(flatRulesSelector)
const brancheName = useSelector(situationBranchNameSelector)
const valuesToShow = !useSelector(noUserInputSelector)
let name = match?.params?.name,
decodedRuleName = decodeRuleName(name)
const renderRule = dottedName => {
const renderRule = (dottedName: DottedName) => {
return (
<div id="RulePage">
<ScrollToTop key={brancheName + dottedName} />
@ -40,20 +40,16 @@ export default compose(
return <Redirect to="/404" />
return renderRule(decodedRuleName)
})
}
const BackToSimulation = compose(
connect(
null,
{ goBackToSimulation }
)
)(
const BackToSimulation = connect(null, { goBackToSimulation })(
// Triggers rerender when the language changes
function BackToSimulation({ goBackToSimulation }) {
return (
<button
className="ui__ simple small push-left button"
onClick={goBackToSimulation}>
onClick={goBackToSimulation}
>
<Trans i18nKey="back">Reprendre la simulation</Trans>
</button>
)

View File

@ -4,11 +4,11 @@ import PaySlip from 'Components/PaySlip'
import StackedBarChart from 'Components/StackedBarChart'
import { ThemeColoursContext } from 'Components/utils/withColours'
import { getRuleFromAnalysis } from 'Engine/rules'
import React, { useRef, useContext } from 'react'
import React, { useContext, useRef } from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
usePeriod
@ -16,7 +16,7 @@ import {
import * as Animate from 'Ui/animate'
class ErrorBoundary extends React.Component {
state = {}
state = {} as { error?: string }
static getDerivedStateFromError() {
return {
error:
@ -32,12 +32,12 @@ class ErrorBoundary extends React.Component {
export default function SalaryExplanation() {
const showDistributionFirst = useSelector(
state => !state.conversationSteps.foldedSteps.length
(state: RootState) => !state.conversationSteps.foldedSteps.length
)
const distributionRef = useRef({})
const distributionRef = useRef<HTMLDivElement>()
return (
<ErrorBoundary>
<Animate.fromTop key={showDistributionFirst}>
<Animate.fromTop key={showDistributionFirst.toString()}>
{showDistributionFirst ? (
<>
<RevenueRepatitionSection />
@ -55,7 +55,8 @@ export default function SalaryExplanation() {
behavior: 'smooth',
block: 'start'
})
}>
}
>
{emoji('📊')} <T>Voir la répartition des cotisations</T>
</button>
</div>
@ -71,9 +72,7 @@ export default function SalaryExplanation() {
Le simulateur vous aide à comprendre votre bulletin de paie, sans
lui être opposable. Pour plus d&apos;informations, rendez vous
sur&nbsp;
<a
alt="service-public.fr"
href="https://www.service-public.fr/particuliers/vosdroits/F559">
<a href="https://www.service-public.fr/particuliers/vosdroits/F559">
service-public.fr
</a>
.

View File

@ -8,57 +8,55 @@ import { T } from 'Components'
import Conversation from 'Components/conversation/Conversation'
import SeeAnswersButton from 'Components/conversation/SeeAnswersButton'
import PeriodSwitch from 'Components/PeriodSwitch'
// $FlowFixMe
import ComparaisonConfig from 'Components/simulationConfigs/rémunération-dirigeant.yaml'
import withSimulationConfig from 'Components/simulationConfigs/withSimulationConfig'
import withSitePaths from 'Components/utils/withSitePaths'
import { useSimulationConfig } from 'Components/simulationConfigs/useSimulationConfig'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import { encodeRuleName, getRuleFromAnalysis } from 'Engine/rules.js'
import revenusSVG from 'Images/revenus.svg'
import { compose } from 'ramda'
import React, { useCallback, useState } from 'react'
import React, { useCallback, useContext, useState } from 'react'
import emoji from 'react-easy-emoji'
import { connect } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
branchAnalyseSelector
} from 'Selectors/analyseSelectors'
import { DottedName } from 'Types/rule'
import Animate from 'Ui/animate'
import InfoBulle from 'Ui/InfoBulle'
import './SchemeComparaison.css'
type OwnProps = {
hideAutoEntrepreneur?: boolean,
hideAssimiléSalarié?: boolean
}
type Props = OwnProps & {
setSituationBranch: number => void,
defineDirectorStatus: string => void,
sitePaths: any,
isAutoentrepreneur: boolean => void,
plafondAutoEntrepreneurDépassé: boolean
}
let getBranchIndex = branch =>
let getBranchIndex = (branch: string) =>
({ assimilé: 0, indépendant: 1, 'auto-entrepreneur': 2 }[branch])
let getRuleFrom = analyses => (branch, dottedName) => {
let getRuleFrom = analyses => (branch: string, dottedName: DottedName) => {
let i = getBranchIndex(branch)
return getRuleFromAnalysis(analyses[i])(dottedName)
}
const SchemeComparaison = ({
/* Own Props */
hideAutoEntrepreneur = false,
hideAssimiléSalarié = false,
/* Injected Props */
type SchemeComparaisonProps = {
hideAutoEntrepreneur?: boolean
hideAssimiléSalarié?: boolean
}
export default function SchemeComparaison({
hideAutoEntrepreneur = false,
hideAssimiléSalarié = false
}: SchemeComparaisonProps) {
useSimulationConfig(ComparaisonConfig)
const dispatch = useDispatch()
const analyses = useSelector(analysisWithDefaultsSelector)
const plafondAutoEntrepreneurDépassé = useSelector((state: RootState) =>
branchAnalyseSelector(state, {
situationBranchName: 'Auto-entrepreneur'
}).controls?.find(
({ test }) =>
test.includes && test.includes('base des cotisations > plafond')
)
)
plafondAutoEntrepreneurDépassé,
defineDirectorStatus,
isAutoentrepreneur,
analyses
}: Props) => {
let getRule = getRuleFrom(analyses)
const [showMore, setShowMore] = useState(false)
const [conversationStarted, setConversationStarted] = useState(
@ -74,7 +72,8 @@ const SchemeComparaison = ({
className={classnames('comparaison-grid', {
hideAutoEntrepreneur,
hideAssimiléSalarié
})}>
})}
>
<h2 className="AS">
{emoji('☂')} <T>Assimilé salarié</T>
<small>
@ -289,7 +288,8 @@ const SchemeComparaison = ({
<div className="all">
<button
onClick={() => setShowMore(true)}
className="ui__ simple small button">
className="ui__ simple small button"
>
Afficher plus d'informations
</button>
</div>
@ -315,7 +315,8 @@ const SchemeComparaison = ({
<img src={revenusSVG} css="height: 8rem" />
<button
className="ui__ cta plain button"
onClick={startConversation}>
onClick={startConversation}
>
Lancer la simulation
</button>
</T>
@ -354,7 +355,8 @@ const SchemeComparaison = ({
className={classnames(
'ui__ plain card',
plafondAutoEntrepreneurDépassé && 'disabled'
)}>
)}
>
{plafondAutoEntrepreneurDépassé ? (
'Plafond de CA dépassé'
) : (
@ -563,18 +565,21 @@ const SchemeComparaison = ({
<button
className="ui__ button"
onClick={() => {
defineDirectorStatus('SALARIED')
!hideAutoEntrepreneur && isAutoentrepreneur(false)
}}>
dispatch(defineDirectorStatus('SALARIED'))
!hideAutoEntrepreneur && dispatch(isAutoentrepreneur(false))
}}
>
<T k="comparaisonRégimes.choix.AS">Assimilé&nbsp;salarié</T>
</button>
)}
<button
className="ui__ button"
onClick={() => {
!hideAssimiléSalarié && defineDirectorStatus('SELF_EMPLOYED')
!hideAutoEntrepreneur && isAutoentrepreneur(false)
}}>
!hideAssimiléSalarié &&
dispatch(defineDirectorStatus('SELF_EMPLOYED'))
!hideAutoEntrepreneur && dispatch(isAutoentrepreneur(false))
}}
>
{hideAssimiléSalarié ? (
<T k="comparaisonRégimes.choix.EI">Entreprise individuelle</T>
) : (
@ -585,9 +590,11 @@ const SchemeComparaison = ({
<button
className="ui__ button"
onClick={() => {
!hideAssimiléSalarié && defineDirectorStatus('SELF_EMPLOYED')
isAutoentrepreneur(true)
}}>
!hideAssimiléSalarié &&
dispatch(defineDirectorStatus('SELF_EMPLOYED'))
dispatch(isAutoentrepreneur(true))
}}
>
<T k="comparaisonRégimes.choix.auto">Auto-entrepreneur</T>
</button>
)}
@ -597,63 +604,37 @@ const SchemeComparaison = ({
)
}
const RuleValueLink = compose(
withSitePaths,
connect(
state => ({
analyses: analysisWithDefaultsSelector(state)
}),
{
setSituationBranch
}
)
)(
({
analyses,
branch,
rule: dottedName,
sitePaths,
appendText,
setSituationBranch,
unit
}) => {
let rule = getRuleFrom(analyses)(branch, dottedName)
return !rule ? null : (
<Link
onClick={() => setSituationBranch(getBranchIndex(branch))}
to={
sitePaths.documentation.index + '/' + encodeRuleName(rule.dottedName)
}>
<Value
maximumFractionDigits={0}
{...rule}
unit={
/* //TODO the unit should be integrated in the leaf rules of base.yaml and infered by mecanisms. Will be done in a future release*/
unit !== undefined ? unit : '€'
}
/>
{appendText && <> {appendText}</>}
</Link>
)
}
)
type RuleValueLinkProps = {
branch: string
rule: string
appendText?: React.ReactNode
unit?: null | string
}
export default compose(
withSimulationConfig(ComparaisonConfig),
connect(
state => ({
analyses: analysisWithDefaultsSelector(state),
plafondAutoEntrepreneurDépassé: branchAnalyseSelector(state, {
situationBranchName: 'Auto-entrepreneur'
}).controls?.find(
({ test }) =>
test.includes && test.includes('base des cotisations > plafond')
)
}),
{
defineDirectorStatus,
isAutoentrepreneur,
setSituationBranch
}
function RuleValueLink({
branch,
rule: dottedName,
appendText,
unit
}: RuleValueLinkProps) {
const dispatch = useDispatch()
const analyses = useSelector(analysisWithDefaultsSelector)
const sitePaths = useContext(SitePathsContext)
let rule = getRuleFrom(analyses)(branch, dottedName)
return !rule ? null : (
<Link
onClick={() => dispatch(setSituationBranch(getBranchIndex(branch)))}
to={sitePaths.documentation.index + '/' + encodeRuleName(rule.dottedName)}
>
<Value
maximumFractionDigits={0}
{...rule}
unit={
/* //TODO the unit should be integrated in the leaf rules of base.yaml and infered by mecanisms. Will be done in a future release*/
unit !== undefined ? unit : '€'
}
/>
{appendText && <> {appendText}</>}
</Link>
)
)(SchemeComparaison)
}

View File

@ -1,7 +1,7 @@
import withSitePaths from 'Components/utils/withSitePaths'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { encodeRuleName, parentName } from 'Engine/rules.js'
import { compose, pick, sortBy, take } from 'ramda'
import React, { useEffect, useState } from 'react'
import { pick, sortBy, take } from 'ramda'
import React, { useContext, useEffect, useState } from 'react'
import Highlighter from 'react-highlight-words'
import { useTranslation } from 'react-i18next'
import { Link, Redirect } from 'react-router-dom'
@ -10,12 +10,12 @@ import { capitalise0 } from '../utils'
const worker = new Worker()
let SearchBar = ({
export default function SearchBar({
rules,
showDefaultList,
finally: finallyCallback,
sitePaths
}) => {
finally: finallyCallback
}) {
const sitePaths = useContext(SitePathsContext)
const [input, setInput] = useState('')
const [selectedOption, setSelectedOption] = useState(null)
const [results, setResults] = useState([])
@ -29,7 +29,7 @@ let SearchBar = ({
})
worker.onmessage = ({ data: results }) => setResults(results)
}, [])
}, [rules])
let renderOptions = rules => {
let options =
@ -120,5 +120,3 @@ let SearchBar = ({
</>
)
}
export default compose(withSitePaths)(SearchBar)

View File

@ -1,20 +1,20 @@
import { compose } from 'ramda'
import React, { useEffect, useState } from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { useSelector } from 'react-redux'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import Overlay from './Overlay'
import SearchBar from './SearchBar'
export default compose(
connect(state => ({
flatRules: flatRulesSelector(state)
}))
)(function SearchButton({ flatRules, invisibleButton }) {
type SearchButtonProps = {
invisibleButton?: boolean
}
export default function SearchButton({ invisibleButton }: SearchButtonProps) {
const flatRules = useSelector(flatRulesSelector)
const [visible, setVisible] = useState(false)
useEffect(() => {
const handleKeyDown = e => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!(e.ctrlKey && e.key === 'k')) return
setVisible(true)
e.preventDefault()
@ -40,8 +40,9 @@ export default compose(
) : invisibleButton ? null : (
<button
className="ui__ simple small button"
onClick={() => setVisible(true)}>
onClick={() => setVisible(true)}
>
{emoji('🔍')} <Trans>Rechercher</Trans>
</button>
)
})
}

View File

@ -3,7 +3,13 @@ import { usePersistingState } from 'Components/utils/persistState'
import React from 'react'
import emoji from 'react-easy-emoji'
export default function SimulateurWarning({ simulateur }) {
type SimulateurWarningProps = {
simulateur: string
}
export default function SimulateurWarning({
simulateur
}: SimulateurWarningProps) {
let [folded, fold] = usePersistingState(
'app::simulateurs:warning-folded:v1:' + simulateur,
false
@ -13,7 +19,8 @@ export default function SimulateurWarning({ simulateur }) {
id="SimulateurWarning"
css={`
margin-bottom: 1rem;
`}>
`}
>
<p>
{emoji('🚩 ')}
<strong>
@ -22,7 +29,8 @@ export default function SimulateurWarning({ simulateur }) {
{folded && (
<button
className="ui__ button simple small"
onClick={() => fold(false)}>
onClick={() => fold(false)}
>
<T k="simulateurs.warning.plus">Lire les précisions</T>
</button>
)}
@ -30,7 +38,8 @@ export default function SimulateurWarning({ simulateur }) {
{!folded && (
<div
className="ui__ card light-bg"
css="padding-top: 1rem; padding-bottom: 0.4rem">
css="padding-top: 1rem; padding-bottom: 0.4rem"
>
<ul>
{simulateur == 'auto-entreprise' && (
<li>
@ -66,7 +75,8 @@ export default function SimulateurWarning({ simulateur }) {
<div className="ui__ answer-group">
<button
className="ui__ button simple small"
onClick={() => fold(true)}>
onClick={() => fold(true)}
>
<T>J'ai compris</T>
</button>
</div>

View File

@ -1,6 +1,8 @@
import { T } from 'Components'
import Controls from 'Components/Controls'
import Conversation from 'Components/conversation/Conversation'
import Conversation, {
ConversationProps
} from 'Components/conversation/Conversation'
import SeeAnswersButton from 'Components/conversation/SeeAnswersButton'
import PageFeedback from 'Components/Feedback/PageFeedback'
import SearchButton from 'Components/SearchButton'
@ -12,7 +14,15 @@ import { simulationProgressSelector } from 'Selectors/progressSelectors'
import * as Animate from 'Ui/animate'
import Progress from 'Ui/Progress'
export default function Simulation({ explanations, customEndMessages }) {
type SimulationProps = {
explanations: React.ReactNode
customEndMessages?: ConversationProps['customEndMessages']
}
export default function Simulation({
explanations,
customEndMessages
}: SimulationProps) {
const firstStepCompleted = useSelector(firstStepCompletedSelector)
const progress = useSelector(simulationProgressSelector)
return (
@ -29,7 +39,8 @@ export default function Simulation({ explanations, customEndMessages }) {
marginTop: '1.2rem',
marginBottom: '0.6rem',
alignItems: 'baseline'
}}>
}}
>
{progress < 1 ? (
<small css="padding: 0.4rem 0">
<T k="simulateurs.précision.défaut">
@ -37,8 +48,8 @@ export default function Simulation({ explanations, customEndMessages }) {
</T>
</small>
) : (
<span />
)}
<span />
)}
<SeeAnswersButton />
</div>
<section className="ui__ full-width lighter-bg">

View File

@ -1,8 +1,9 @@
import React from 'react'
import styled from 'styled-components'
import RuleLink from 'Components/RuleLink'
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
import React from 'react'
import { animated, useSpring } from 'react-spring'
import styled from 'styled-components'
import { Rule } from 'Types/rule'
import { capitalise0 } from '../utils'
const BarStack = styled.div`
@ -51,7 +52,7 @@ const SmallCircle = styled.span`
border-radius: 100%;
`
function integerAndDecimalParts(value) {
function integerAndDecimalParts(value: number) {
const integer = Math.floor(value)
const decimal = value - integer
return { integer, decimal }
@ -59,8 +60,8 @@ function integerAndDecimalParts(value) {
// This function calculates rounded percentages so that the sum of all
// returned values is always 100. For instance: [60, 30, 10].
export function roundedPercentages(values) {
const sum = (a = 0, b) => a + b
export function roundedPercentages(values: Array<number>) {
const sum = (a: number = 0, b: number) => a + b
const total = values.reduce(sum)
const percentages = values.map(value =>
integerAndDecimalParts((value / total) * 100)
@ -78,7 +79,11 @@ export function roundedPercentages(values) {
)
}
export default function StackedBarChart({ data }) {
type StackedBarChartProps = {
data: Array<{ color?: string } & Rule>
}
export default function StackedBarChart({ data }: StackedBarChartProps) {
const [intersectionRef, displayChart] = useDisplayOnIntersecting({
threshold: 0.5
})

View File

@ -1,10 +1,10 @@
import { updateSituation } from 'Actions/actions'
import { setActiveTarget, updateSituation } from 'Actions/actions'
import { T } from 'Components'
import InputSuggestions from 'Components/conversation/InputSuggestions'
import PeriodSwitch from 'Components/PeriodSwitch'
import RuleLink from 'Components/RuleLink'
import { ThemeColoursContext } from 'Components/utils/withColours'
import withSitePaths from 'Components/utils/withSitePaths'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { formatCurrency } from 'Engine/format'
import { encodeRuleName } from 'Engine/rules'
import { isEmpty, isNil } from 'ramda'
@ -13,12 +13,13 @@ import emoji from 'react-easy-emoji'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
useSituation,
useSituationValue,
situationSelector,
useTarget
} from 'Selectors/analyseSelectors'
import { Rule } from 'Types/rule'
import Animate from 'Ui/animate'
import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
import CurrencyInput from './CurrencyInput/CurrencyInput'
@ -28,12 +29,13 @@ export default function TargetSelection() {
const [initialRender, setInitialRender] = useState(true)
const analysis = useSelector(analysisWithDefaultsSelector)
const objectifs = useSelector(
state => state.simulation?.config.objectifs || []
(state: RootState) => state.simulation?.config.objectifs || []
)
const secondaryObjectives = useSelector(
state => state.simulation?.config['objectifs secondaires'] || []
(state: RootState) =>
state.simulation?.config['objectifs secondaires'] || []
)
const situation = useSituation()
const situation = useSelector(situationSelector)
const dispatch = useDispatch()
const colours = useContext(ThemeColoursContext)
@ -73,7 +75,9 @@ export default function TargetSelection() {
return (
<div id="targetSelection">
{(typeof objectifs[0] === 'string' ? [{ objectifs }] : objectifs).map(
{((typeof objectifs[0] === 'string'
? [{ objectifs }]
: objectifs) as any).map(
({ icône, objectifs: groupTargets, nom }, index) => (
<React.Fragment key={nom || '0'}>
<div style={{ display: 'flex', alignItems: 'end' }}>
@ -96,7 +100,8 @@ export default function TargetSelection() {
${colours.darkColour} 0%,
${colours.colour} 100%
)`
}}>
}}
>
<Targets
{...{
targets: targets.filter(({ dottedName }) =>
@ -138,7 +143,7 @@ let Targets = ({ targets, initialRender }) => (
)
const Target = ({ target, initialRender }) => {
const activeInput = useSelector(state => state.activeTargetInput)
const activeInput = useSelector((state: RootState) => state.activeTargetInput)
const dispatch = useDispatch()
const isActiveInput = activeInput === target.dottedName
@ -147,7 +152,8 @@ const Target = ({ target, initialRender }) => {
return (
<li
key={target.name}
className={isSmallTarget ? 'small-target' : undefined}>
className={isSmallTarget ? 'small-target' : undefined}
>
<Animate.appear unless={initialRender}>
<div>
<div className="main">
@ -192,7 +198,8 @@ const Target = ({ target, initialRender }) => {
)
}
let Header = withSitePaths(({ target, sitePaths }) => {
let Header = ({ target }) => {
const sitePaths = useContext(SitePathsContext)
const ruleLink =
sitePaths.documentation.index + '/' + encodeRuleName(target.dottedName)
return (
@ -205,26 +212,40 @@ let Header = withSitePaths(({ target, sitePaths }) => {
</span>
</span>
)
})
}
let TargetInputOrValue = ({ target, isActiveInput, isSmallTarget }) => {
type TargetInputOrValueProps = {
target: Rule
isActiveInput: boolean
isSmallTarget: boolean
}
let TargetInputOrValue = ({
target,
isActiveInput,
isSmallTarget
}: TargetInputOrValueProps) => {
const { language } = useTranslation().i18n
const colors = useContext(ThemeColoursContext)
const dispatch = useDispatch()
const situationValue = Math.round(useSituationValue(target.dottedName))
const situationValue = Math.round(
useSelector(situationSelector)[target.dottedName]
)
const targetWithValue = useTarget(target.dottedName)
const value = targetWithValue?.nodeValue
? Math.round(targetWithValue?.nodeValue)
: undefined
const inversionFail = useSelector(
state => analysisWithDefaultsSelector(state)?.cache.inversionFail
(state: RootState) =>
analysisWithDefaultsSelector(state)?.cache.inversionFail
)
const blurValue = inversionFail && !isActiveInput && value
return (
<span
className="targetInputOrValue"
style={blurValue ? { filter: 'blur(3px)' } : {}}>
style={blurValue ? { filter: 'blur(3px)' } : {}}
>
{target.question ? (
<>
{!isActiveInput && <AnimatedTargetValue value={value} />}
@ -246,10 +267,7 @@ let TargetInputOrValue = ({ target, isActiveInput, isSmallTarget }) => {
}
onFocus={() => {
if (isSmallTarget) return
dispatch({
type: 'SET_ACTIVE_TARGET_INPUT',
name: target.dottedName
})
dispatch(setActiveTarget(target.dottedName))
}}
language={language}
/>
@ -269,6 +287,7 @@ let TargetInputOrValue = ({ target, isActiveInput, isSmallTarget }) => {
function AidesGlimpse() {
const aides = useTarget('contrat salarié . aides employeur')
const { language } = useTranslation().i18n
// Dans le cas où il n'y a qu'une seule aide à l'embauche qui s'applique, nous
// faisons un lien direct vers cette aide, plutôt qu'un lien vers la liste qui
@ -286,7 +305,7 @@ function AidesGlimpse() {
<T>en incluant</T>{' '}
<strong>
<AnimatedTargetValue value={aides.nodeValue}>
<span>{formatCurrency(aides.nodeValue)}</span>
<span>{formatCurrency(aides.nodeValue, language)}</span>
</AnimatedTargetValue>
</strong>{' '}
<T>d'aides</T> {emoji(aides.explanation.icons)}

View File

@ -1,17 +1,15 @@
import withColours from 'Components/utils/withColours'
import withSitePaths from 'Components/utils/withSitePaths'
import { compose } from 'ramda'
import React from 'react'
import { ThemeColoursContext } from 'Components/utils/withColours'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
import './Targets.css'
export default compose(
withColours,
withSitePaths
)(function Targets({ colours, sitePaths }) {
export default function Targets() {
const colours = useContext(ThemeColoursContext)
const sitePaths = useContext(SitePathsContext)
const analysis = useSelector(analysisWithDefaultsSelector)
let { nodeValue, unité: unit, dottedName } = analysis.targets[0]
return (
@ -26,10 +24,11 @@ export default compose(
title="Quel est calcul ?"
style={{ color: colours.colour }}
to={sitePaths.documentation.index + '/' + dottedName}
className="explanation">
className="explanation"
>
{emoji('📖')}
</Link>
</span>
</div>
)
})
}

View File

@ -1,7 +1,8 @@
import { T } from 'Components'
import { formatValue } from 'Engine/format'
import { formatValue, formatValueOptions } from 'Engine/format'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Rule } from 'Types/rule'
// let booleanTranslations = { true: '✅', false: '❌' }
@ -16,6 +17,19 @@ let style = customStyle => `
${customStyle}
`
type ValueProps = Partial<
Pick<Rule, 'nodeValue' | 'unit'> &
Pick<
formatValueOptions,
'maximumFractionDigits' | 'minimumFractionDigits'
> & {
nilValueSymbol: string
children: number
negative: boolean
customCSS: string
}
>
export default function Value({
nodeValue: value,
unit,
@ -25,7 +39,7 @@ export default function Value({
children,
negative,
customCSS = ''
}) {
}: ValueProps) {
const { language } = useTranslation().i18n
/* Either an entire rule object is passed, or just the right attributes and the value as a JSX child*/
@ -46,7 +60,7 @@ export default function Value({
valueType === 'string' ? (
<T>{nodeValue}</T>
) : valueType === 'object' ? (
nodeValue.nom
(nodeValue as any).nom
) : valueType === 'boolean' ? (
booleanTranslations[language][nodeValue]
) : (

View File

@ -1,64 +0,0 @@
import { EXPLAIN_VARIABLE } from 'Actions/actions'
import Animate from 'Components/ui/animate'
import { Markdown } from 'Components/utils/markdown'
import withColours from 'Components/utils/withColours'
import { findRuleByDottedName } from 'Engine/rules'
import { compose } from 'ramda'
import React from 'react'
import emoji from 'react-easy-emoji'
import { connect } from 'react-redux'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import References from '../rule/References'
import './Aide.css'
export default compose(
connect(
state => ({
explained: state.explainedVariable,
flatRules: flatRulesSelector(state)
}),
dispatch => ({
stopExplaining: () => dispatch({ type: EXPLAIN_VARIABLE })
})
),
withColours
)(function Aide({ flatRules, explained, stopExplaining, colours }) {
if (!explained) return <section id="helpWrapper" />
let rule = findRuleByDottedName(flatRules, explained),
text = rule.description,
refs = rule.références
return (
<Animate.fromTop><div css={`
display: flex;
align-items: center;
img {
margin: 0 1em 0 !important;
width: 1.6em !important;
height: 1.6em !important;
}
`}>
{emoji('')}
<div className="controlText ui__ card" css="padding: 0.6rem 0; flex: 1;">
<h4>{rule.title}</h4>
<p>
<Markdown source={text} />
</p>
{refs && (
<div>
<p>Pour en savoir plus: </p>
<References refs={refs} />
</div>
)}
<button
className="hide"
aria-label="close"
onClick={stopExplaining}
>
×
</button>
</div></div></Animate.fromTop>
)
})

View File

@ -0,0 +1,62 @@
import { explainVariable } from 'Actions/actions'
import Animate from 'Components/ui/animate'
import { Markdown } from 'Components/utils/markdown'
import { findRuleByDottedName } from 'Engine/rules'
import React from 'react'
import emoji from 'react-easy-emoji'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import References from '../rule/References'
import './Aide.css'
export default function Aide() {
const explained = useSelector((state: RootState) => state.explainedVariable)
const flatRules = useSelector(flatRulesSelector)
const dispatch = useDispatch()
const stopExplaining = () => dispatch(explainVariable())
if (!explained) return <section id="helpWrapper" />
let rule = findRuleByDottedName(flatRules, explained),
text = rule.description,
refs = rule.références
return (
<Animate.fromTop>
<div
css={`
display: flex;
align-items: center;
img {
margin: 0 1em 0 !important;
width: 1.6em !important;
height: 1.6em !important;
}
`}
>
{emoji('')}
<div
className="controlText ui__ card"
css="padding: 0.6rem 0; flex: 1;"
>
<h4>{rule.title}</h4>
<p>
<Markdown source={text} />
</p>
{refs && (
<div>
<p>Pour en savoir plus: </p>
<References refs={refs} />
</div>
)}
<button className="hide" aria-label="close" onClick={stopExplaining}>
×
</button>
</div>
</div>
</Animate.fromTop>
)
}

View File

@ -1,22 +1,31 @@
import { goToQuestion, validateStepWithValue } from 'Actions/actions'
import { T } from 'Components'
import QuickLinks from 'Components/QuickLinks'
import getInputComponent from 'Engine/getInputComponent'
import getInputComponent from 'Engine/getInputComponent'
import { findRuleByDottedName } from 'Engine/rules'
import React from 'react'
import emoji from 'react-easy-emoji'
import { useDispatch, useSelector } from 'react-redux'
import { currentQuestionSelector, flatRulesSelector, nextStepsSelector } from 'Selectors/analyseSelectors'
import { RootState } from 'Reducers/rootReducer'
import {
currentQuestionSelector,
flatRulesSelector,
nextStepsSelector
} from 'Selectors/analyseSelectors'
import * as Animate from 'Ui/animate'
import Aide from './Aide'
import './conversation.css'
export default function Conversation({ customEndMessages }) {
export type ConversationProps = {
customEndMessages?: React.ReactNode
}
export default function Conversation({ customEndMessages }: ConversationProps) {
const dispatch = useDispatch()
const flatRules = useSelector(flatRulesSelector)
const currentQuestion = useSelector(currentQuestionSelector)
const previousAnswers = useSelector(
state => state.conversationSteps.foldedSteps
(state: RootState) => state.conversationSteps.foldedSteps
)
const nextSteps = useSelector(nextStepsSelector)
@ -29,7 +38,7 @@ export default function Conversation({ customEndMessages }) {
)
const goToPrevious = () =>
dispatch(goToQuestion(previousAnswers.slice(-1)[0]))
const handleKeyDown = ({ key }) => {
const handleKeyDown = ({ key }: React.KeyboardEvent) => {
if (['Escape'].includes(key)) {
setDefault()
}
@ -38,7 +47,7 @@ export default function Conversation({ customEndMessages }) {
return nextSteps.length ? (
<>
<Aide />
<div tabIndex="0" style={{ outline: 'none' }} onKeyDown={handleKeyDown}>
<div tabIndex={0} style={{ outline: 'none' }} onKeyDown={handleKeyDown}>
{currentQuestion && (
<React.Fragment key={currentQuestion}>
<Animate.fadeIn>
@ -49,14 +58,16 @@ export default function Conversation({ customEndMessages }) {
<>
<button
onClick={goToPrevious}
className="ui__ simple small push-left button">
className="ui__ simple small push-left button"
>
<T>Précédent</T>
</button>
</>
)}
<button
onClick={setDefault}
className="ui__ simple small push-right button">
className="ui__ simple small push-right button"
>
<T>Passer</T>
</button>
</div>
@ -66,20 +77,20 @@ export default function Conversation({ customEndMessages }) {
<QuickLinks />
</>
) : (
<div style={{ textAlign: 'center' }}>
<h3>
{emoji('🌟')}{' '}
<T k="simulation-end.title">Vous avez complété cette simulation</T>{' '}
</h3>
<p>
{customEndMessages ? (
customEndMessages
) : (
<T k="simulation-end.text">
Vous avez maintenant accès à l'estimation la plus précise possible.
<div style={{ textAlign: 'center' }}>
<h3>
{emoji('🌟')}{' '}
<T k="simulation-end.title">Vous avez complété cette simulation</T>{' '}
</h3>
<p>
{customEndMessages ? (
customEndMessages
) : (
<T k="simulation-end.text">
Vous avez maintenant accès à l'estimation la plus précise possible.
</T>
)}
</p>
</div>
)
)}
</p>
</div>
)
}

View File

@ -1,27 +1,21 @@
import { EXPLAIN_VARIABLE } from 'Actions/actions'
import { explainVariable } from 'Actions/actions'
import classNames from 'classnames'
import { findRuleByDottedName } from 'Engine/rules'
import { compose } from 'ramda'
import React from 'react'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { connect } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import withTracker from '../utils/withTracker'
import { DottedName } from 'Types/rule'
import { TrackerContext } from '../utils/withTracker'
import './Explicable.css'
export default compose(
connect(
state => ({
explained: state.explainedVariable,
flatRules: flatRulesSelector(state)
}),
dispatch => ({
explain: variableName =>
dispatch({ type: EXPLAIN_VARIABLE, variableName })
})
),
withTracker
)(function Explicable({ flatRules, dottedName, explain, explained, tracker }) {
export default function Explicable({ dottedName }: { dottedName: DottedName }) {
const tracker = useContext(TrackerContext)
const dispatch = useDispatch()
const explained = useSelector((state: RootState) => state.explainedVariable)
const flatRules = useSelector(flatRulesSelector)
// Rien à expliquer ici, ce n'est pas une règle
if (dottedName == null) return null
@ -35,17 +29,19 @@ export default compose(
<span
className={classNames('explicable', {
explained: dottedName === explained
})}>
})}
>
<span
className="icon"
onClick={e => {
tracker.push(['trackEvent', 'help', dottedName])
explain(dottedName)
dispatch(explainVariable(dottedName))
e.preventDefault()
e.stopPropagation()
}}>
}}
>
{emoji('')}
</span>
</span>
)
})
}

View File

@ -1,23 +1,26 @@
import { T } from 'Components'
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import Answers from './AnswerList'
import './conversation.css'
export default connect(state => ({
arePreviousAnswers: !!state.conversationSteps.foldedSteps.length
}))(function SeeAnswersButton({ arePreviousAnswers }) {
export default function SeeAnswersButton() {
const arePreviousAnswers = !!useSelector(
(state: RootState) => state.conversationSteps.foldedSteps.length
)
const [showAnswerModal, setShowAnswerModal] = useState(false)
return (
<>
{arePreviousAnswers && (
<button
className="ui__ small simple button "
onClick={() => setShowAnswerModal(true)}>
onClick={() => setShowAnswerModal(true)}
>
<T>Modifier mes réponses</T>
</button>
)}
{showAnswerModal && <Answers onClose={() => setShowAnswerModal(false)} />}
</>
)
})
}

View File

@ -1,13 +1,18 @@
import React, { useCallback, useEffect } from 'react'
import { Trans } from 'react-i18next'
export default function SendButton({ disabled, submit }) {
type SendButtonProps = {
disabled: boolean
submit: (cause: string) => void
}
export default function SendButton({ disabled, submit }: SendButtonProps) {
const getAction = useCallback(cause => (!disabled ? submit(cause) : null), [
disabled,
submit
])
useEffect(() => {
const handleKeyDown = ({ key }) => {
const handleKeyDown = ({ key }: KeyboardEvent) => {
if (key !== 'Enter') return
getAction('enter')
}
@ -23,7 +28,8 @@ export default function SendButton({ disabled, submit }) {
className="ui__ button plain"
css="margin-left: 1.2rem"
disabled={disabled}
onClick={() => getAction('accept')}>
onClick={() => getAction('accept')}
>
<span className="text">
<Trans>Suivant</Trans>
</span>

View File

@ -1,6 +0,0 @@
import React from 'react'
import { Trans } from 'react-i18next'
let T = ({ k, ...props }) => <Trans i18nKey={k} {...props} />
export { T }

View File

@ -0,0 +1,8 @@
import React from 'react'
import { Trans, TransProps } from 'react-i18next'
type TProps = { k?: TransProps['i18nKey'] } & TransProps
const T = ({ k, ...props }: TProps) => <Trans i18nKey={k} {...props} />
export { T }

View File

@ -1,12 +1,13 @@
import withSitePaths from 'Components/utils/withSitePaths'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { encodeRuleName, findRuleByDottedName } from 'Engine/rules'
import React from 'react'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { Link } from 'react-router-dom'
import { capitalise0 } from '../../utils'
import './Namespace.css'
let Namespace = ({ dottedName, flatRules, colour, sitePaths }) => {
export default function Namespace({ dottedName, flatRules, colour }) {
const sitePaths = useContext(SitePathsContext)
return (
<ul id="namespace">
{dottedName
@ -36,7 +37,8 @@ let Namespace = ({ dottedName, flatRules, colour, sitePaths }) => {
style={style}
to={
sitePaths.documentation.index + '/' + encodeRuleName(ruleName)
}>
}
>
{rule.icons && <span>{emoji(rule.icons)} </span>}
{ruleText}
</Link>
@ -47,4 +49,3 @@ let Namespace = ({ dottedName, flatRules, colour, sitePaths }) => {
</ul>
)
}
export default withSitePaths(Namespace)

View File

@ -32,8 +32,8 @@ function Ref({ name, link }) {
)
}
interface ReferencesProps {
refs: { [key: string]: string }
type ReferencesProps = {
refs: { [name: string]: string }
}
export default function References({ refs }: ReferencesProps) {

View File

@ -1,7 +1,7 @@
import { T } from 'Components'
import PeriodSwitch from 'Components/PeriodSwitch'
import withColours from 'Components/utils/withColours'
import withSitePaths from 'Components/utils/withSitePaths'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import knownMecanisms from 'Engine/known-mecanisms.yaml'
import {
@ -10,7 +10,7 @@ import {
findRuleByNamespace
} from 'Engine/rules'
import { compose, isEmpty } from 'ramda'
import React, { Suspense, useState } from 'react'
import React, { Suspense, useContext, useState } from 'react'
import emoji from 'react-easy-emoji'
import { Helmet } from 'react-helmet'
import { Trans, useTranslation } from 'react-i18next'
@ -42,17 +42,16 @@ export default compose(
analysedRule: ruleAnalysisSelector(state, props),
analysedExample: exampleAnalysisSelector(state, props)
})),
AttachDictionary(knownMecanisms),
withSitePaths
AttachDictionary(knownMecanisms)
)(function Rule({
dottedName,
currentExample,
flatRules,
valuesToShow,
sitePaths,
analysedExample,
analysedRule
}) {
const sitePaths = useContext(SitePathsContext)
const [viewSource, setViewSource] = useState(false)
const { t } = useTranslation()
@ -60,13 +59,14 @@ export default compose(
let { type, name, acronyme, title, description, question, icon } = flatRule,
namespaceRules = findRuleByNamespace(flatRules, dottedName)
let displayedRule = analysedExample || analysedRule
const renderToggleSourceButton = () => {
return (
<button
id="toggleRuleSource"
className="ui__ link-button"
onClick={() => setViewSource(!viewSource)}>
onClick={() => setViewSource(!viewSource)}
>
{emoji(
viewSource
? `📖 ${t('Revenir à la documentation')}`
@ -140,7 +140,8 @@ export default compose(
> * {
margin: 0 0.6em;
}
`}>
`}
>
<Value
{...displayedRule}
nilValueSymbol={
@ -177,7 +178,8 @@ export default compose(
? sitePaths.simulateurs.indépendant
: // otherwise
sitePaths.simulateurs.index
}>
}
>
<T>Faire une simulation</T>
</Link>
</div>
@ -224,10 +226,8 @@ export default compose(
)
})
let NamespaceRulesList = compose(
withColours,
withSitePaths
)(({ namespaceRules, colours, sitePaths }) => {
let NamespaceRulesList = compose(withColours)(({ namespaceRules, colours }) => {
const sitePaths = useContext(SitePathsContext)
return (
<section>
<h2>
@ -245,7 +245,8 @@ let NamespaceRulesList = compose(
sitePaths.documentation.index +
'/' +
encodeRuleName(r.dottedName)
}>
}
>
{r.title || r.name}
</Link>
</li>
@ -264,7 +265,8 @@ let Period = ({ period, valuesToShow }) =>
<span
className="name"
data-term-definition="période"
style={{ background: '#8e44ad' }}>
style={{ background: '#8e44ad' }}
>
{period}
</span>
</span>

View File

@ -2,9 +2,12 @@ import { safeDump } from 'js-yaml'
import React from 'react'
import emoji from 'react-easy-emoji'
import rules from 'Règles/base.yaml'
import { Rule } from 'Types/rule'
import ColoredYaml from './ColoredYaml'
export default function RuleSource({ dottedName }) {
type RuleSourceProps = Pick<Rule, 'dottedName'>
export default function RuleSource({ dottedName }: RuleSourceProps) {
let source = rules[dottedName]
return (

View File

@ -0,0 +1,16 @@
import { resetSimulation, setSimulationConfig } from 'Actions/actions'
import { useDispatch, useSelector } from 'react-redux'
import { RootState, SimulationConfig } from 'Reducers/rootReducer'
export function useSimulationConfig(config: SimulationConfig) {
const dispatch = useDispatch()
const stateConfig = useSelector(
(state: RootState) => state.simulation?.config
)
if (config !== stateConfig) {
dispatch(setSimulationConfig(config))
if (stateConfig) {
dispatch(resetSimulation())
}
}
}

View File

@ -1,28 +0,0 @@
import { resetSimulation, setSimulationConfig } from 'Actions/actions'
import { compose } from 'ramda'
import React from 'react'
import { connect } from 'react-redux'
import { noUserInputSelector } from 'Selectors/analyseSelectors'
export default config => SimulationComponent =>
compose(
connect(
state => ({
config: state.simulation?.config,
noUserInput: noUserInputSelector(state)
}),
{
setSimulationConfig,
resetSimulation
}
)
)(function DecoratedSimulation(props) {
if (config !== props.config) {
props.setSimulationConfig(config)
if (props.config) {
props.resetSimulation()
}
}
if (!config) return null
return <SimulationComponent {...props} />
})

View File

@ -5,12 +5,12 @@ import { useTranslation } from 'react-i18next'
import { usePeriod } from 'Selectors/analyseSelectors'
import './AnimatedTargetValue.css'
interface AnimatedTargetValueProps {
type AnimatedTargetValueProps = {
value?: number
children: React.ReactNode
children?: React.ReactNode
}
function formatDifference(difference, language) {
const formatDifference: typeof formatCurrency = (difference, language) => {
const prefix = difference > 0 ? '+' : ''
return prefix + formatCurrency(difference, language)
}
@ -46,7 +46,8 @@ export default function AnimatedTargetValue({
style={{
color: difference > 0 ? 'chartreuse' : 'red',
pointerEvents: 'none'
}}>
}}
>
{formatDifference(difference, language)}
</Evaporate>
)}{' '}
@ -61,7 +62,8 @@ const Evaporate = React.memo(
<ReactCSSTransitionGroup
transitionName="evaporate"
transitionEnterTimeout={2500}
transitionLeaveTimeout={1}>
transitionLeaveTimeout={1}
>
<span key={children} style={style} className="evaporate">
{children}
</span>

View File

@ -1,9 +1,9 @@
import classnames from 'classnames'
import Animate from 'Ui/animate'
import { Markdown } from 'Components/utils/markdown'
import { ScrollToElement } from 'Components/utils/Scroll'
import { TrackerContext } from 'Components/utils/withTracker'
import React, { Component, useContext, useState } from 'react'
import Animate from 'Ui/animate'
import Checkbox from '../Checkbox'
import './index.css'
@ -58,7 +58,8 @@ export function CheckItem({
className={classnames('ui__ checklist-button', {
opened: displayExplanations
})}
onClick={handleClick}>
onClick={handleClick}
>
{title}
</button>
</div>
@ -78,7 +79,7 @@ export function CheckItem({
)
}
type ChecklistProps = {
export type ChecklistProps = {
children: React.ReactNode
onItemCheck: (string, boolean) => void
onInitialization: (arg: Array<string>) => void

View File

@ -1,7 +1,17 @@
import React from 'react'
import './Progress.css'
export default function Progress({ progress, style, className }) {
type ProgressProps = {
progress: number
style?: React.CSSProperties
className: string
}
export default function Progress({
progress,
style,
className
}: ProgressProps) {
return (
<div className={'progress__container ' + className} style={style}>
<div className="progress__bar" style={{ width: `${progress * 100}%` }} />

View File

@ -1,13 +1,5 @@
import withColours, { ThemeColours } from 'Components/utils/withColours'
import React from 'react'
type OwnProps = {
media: 'email' | 'facebook' | 'linkedin' | 'github' | 'twitter'
}
type Props = {
colours: ThemeColours
} & OwnProps
import { ThemeColoursContext } from 'Components/utils/withColours'
import React, { useContext } from 'react'
const icons = {
facebook: {
@ -42,10 +34,12 @@ const icons = {
}
}
export default withColours(function withSocialMedia({
media,
colours: { colour }
}: Props) {
export default function withSocialMedia({
media
}: {
media: keyof typeof icons
}) {
const { colour } = useContext(ThemeColoursContext)
return (
<svg
viewBox="0 0 64 64"
@ -55,7 +49,8 @@ export default withColours(function withSocialMedia({
height: '2rem',
margin: '0.6rem',
fillRule: 'evenodd'
}}>
}}
>
<g
style={{
display: 'inline-block',
@ -64,7 +59,8 @@ export default withColours(function withSocialMedia({
position: 'relative',
overflow: 'hidden',
verticalAlign: 'middle'
}}>
}}
>
<circle cx="32" cy="32" r="31" />
</g>
<g>
@ -75,4 +71,4 @@ export default withColours(function withSocialMedia({
</g>
</svg>
)
})
}

View File

@ -14,18 +14,16 @@ function LinkRenderer({ href, children }) {
}
}
interface MarkdownProps {
type MarkdownProps = ReactMarkdownProps & {
source: string
className?: string
renderers?: ReactMarkdownProps['renderers']
[other_props: string]: any
}
export const Markdown = ({
source,
className = '',
renderers = {},
otherProps
...otherProps
}: MarkdownProps) => (
<ReactMarkdown
source={source}

View File

@ -1,11 +1,12 @@
import { useEffect, useState } from 'react'
import safeLocalStorage from '../../storage/safeLocalStorage'
export const persistState = key => ([state, changeState]) => {
export const persistState = (key: string) => ([state, changeState]) => {
useEffect(() => safeLocalStorage.setItem(key, JSON.stringify(state)), [state])
return [state, changeState]
}
export const getInitialState = key => {
export const getInitialState = (key: string) => {
const value = safeLocalStorage.getItem(key)
if (!value) {
return

View File

@ -1,7 +1,11 @@
import { useEffect, useRef, useState } from 'react'
export default function({ root = null, rootMargin, threshold = 0 }) {
const ref = useRef()
export default function({
root = null,
rootMargin,
threshold = 0
}: IntersectionObserverInit): [React.RefObject<HTMLDivElement>, boolean] {
const ref = useRef<HTMLDivElement>()
const [wasOnScreen, setWasOnScreen] = useState(false)
useEffect(() => {
@ -25,7 +29,7 @@ export default function({ root = null, rootMargin, threshold = 0 }) {
return () => {
observer.unobserve(node)
}
}, [root, rootMargin, threshold]) // Empty array ensures that effect is only run on mount and unmount
}, [root, rootMargin, threshold])
return [ref, wasOnScreen]
}

View File

@ -111,7 +111,7 @@ export function ThemeColoursProvider({ colour, children }: ProviderProps) {
)
}
interface WithColoursProps {
type WithColoursProps = {
colours: ThemeColours
}

View File

@ -1,30 +1,10 @@
import React, { createContext } from 'react'
import { createContext } from 'react'
import { SitePathsType } from 'sites/mon-entreprise.fr/sitePaths'
export const SitePathsContext = createContext<Partial<SitePathsType>>({})
export const SitePathsContext = createContext<SitePathsType>(
{} as SitePathsType
)
export const SitePathProvider = SitePathsContext.Provider
export interface WithSitePathsProps {
sitePaths: SitePathsType
}
export default function withSitePaths<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
class WithSitePaths extends React.Component<P & WithSitePathsProps> {
displayName = `withSitePaths(${WrappedComponent.displayName || ''})`
render() {
return (
<SitePathsContext.Consumer>
{sitePaths => (
<WrappedComponent {...(this.props as P)} sitePaths={sitePaths} />
)}
</SitePathsContext.Consumer>
)
}
}
return WithSitePaths
}
export type SitePaths = SitePathsType

View File

@ -1,19 +1,5 @@
import React, { createContext } from 'react'
import { createContext } from 'react'
import Tracker, { devTracker } from '../../Tracker'
export const TrackerContext = createContext(devTracker)
export const TrackerContext = createContext<Tracker>(devTracker)
export const TrackerProvider = TrackerContext.Provider
export interface WithTrackerProps {
tracker: Tracker
}
export default function withTracker(Component: React.ComponentType) {
return function ConnectTracker(props) {
return (
<TrackerContext.Consumer>
{(tracker: Tracker) => <Component {...props} tracker={tracker} />}
</TrackerContext.Consumer>
)
}
}

View File

@ -1,5 +1,6 @@
import { serialiseUnit } from 'Engine/units'
import { memoizeWith } from 'ramda'
import { Unit } from './units'
const NumberFormat = memoizeWith(
(...args) => JSON.stringify(args),
@ -11,6 +12,11 @@ export let numberFormatter = ({
maximumFractionDigits = 2,
minimumFractionDigits = 0,
language
}: {
style?: string
maximumFractionDigits?: number
minimumFractionDigits?: number
language?: string
}) => value => {
// When we format currency we don't want to display a single decimal digit
// ie 8,1€ but we want to display 8,10€
@ -29,7 +35,7 @@ export let numberFormatter = ({
}).format(value)
}
export const currencyFormat = language => ({
export const currencyFormat = (language: string) => ({
isCurrencyPrefixed: !!numberFormatter({ language, style: 'currency' })(
12
).match(/^€/),
@ -37,7 +43,7 @@ export const currencyFormat = language => ({
decimalSeparator: numberFormatter({ language })(0.1).charAt(1)
})
export const formatCurrency = (value, language) => {
export const formatCurrency = (value: number, language: string) => {
return value == null
? ''
: formatValue({ unit: '€', language, value }).replace(/^(-)?€/, '$1€\u00A0')
@ -48,13 +54,21 @@ export const formatPercentage = value =>
? ''
: formatValue({ unit: '%', value, maximumFractionDigits: 2 })
export type formatValueOptions = {
maximumFractionDigits?: number
minimumFractionDigits?: number
language?: string
unit: Unit | string
value: number
}
export function formatValue({
maximumFractionDigits,
minimumFractionDigits,
language,
unit,
value
}) {
}: formatValueOptions) {
if (typeof value !== 'number') {
return value
}
@ -69,7 +83,11 @@ export function formatValue({
language
})(value)
case '%':
return numberFormatter({ style: 'percent', maximumFractionDigits })(value)
return numberFormatter({
style: 'percent',
maximumFractionDigits,
language
})(value)
default:
return (
numberFormatter({

View File

@ -1,10 +1,10 @@
import { default as classNames, default as classnames } from 'classnames'
import withSitePaths from 'Components/utils/withSitePaths'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import { compose, contains, isNil, pipe, sort, toPairs } from 'ramda'
import React from 'react'
import { contains, isNil, pipe, sort, toPairs } from 'ramda'
import React, { useContext } from 'react'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import { LinkButton } from 'Ui/Button'
@ -26,7 +26,8 @@ export let NodeValuePointer = ({ data, unit }) => (
box-shadow: 2px 2px 4px 1px #d9d9d9, 0 0 0 1px #d9d9d9;
line-height: 1.6em;
border-radius: 0.2rem;
`}>
`}
>
<Value nodeValue={data} unit={unit} />
</span>
)
@ -38,13 +39,15 @@ export function Node({ classes, name, value, child, inline, unit }) {
return (
<div
className={classNames(classes, 'node', { inline })}
style={termDefinition ? { borderColor: mecanismColours(name) } : {}}>
style={termDefinition ? { borderColor: mecanismColours(name) } : {}}
>
{name && !inline && (
<div className="nodeHead" css="margin-bottom: 1em">
<LinkButton
className="name"
style={termDefinition ? { background: mecanismColours(name) } : {}}
data-term-definition={termDefinition}>
data-term-definition={termDefinition}
>
<Trans>{name}</Trans>
</LinkButton>
</div>
@ -64,7 +67,8 @@ export function Node({ classes, name, value, child, inline, unit }) {
width: 100%;
text-align: right;
}
`}>
`}
>
{value !== true && value !== false && !isNil(value) && (
<span className="operator"> =&nbsp;</span>
)}
@ -81,7 +85,8 @@ export function InlineMecanism({ name }) {
<LinkButton
className="name"
data-term-definition={name}
style={{ background: mecanismColours(name) }}>
style={{ background: mecanismColours(name) }}
>
<Trans>{name}</Trans>
</LinkButton>
</span>
@ -89,19 +94,9 @@ export function InlineMecanism({ name }) {
}
// Un élément du graphe de calcul qui a une valeur interprétée (à afficher)
export const Leaf = compose(
withSitePaths,
connect(state => ({ flatRules: flatRulesSelector(state) }))
)(function Leaf({
classes,
dottedName,
name,
nodeValue,
flatRules,
filter,
sitePaths,
unit
}) {
export function Leaf({ classes, dottedName, name, nodeValue, filter, unit }) {
const sitePaths = useContext(SitePathsContext)
const flatRules = useSelector(flatRulesSelector)
let rule = findRuleByDottedName(flatRules, dottedName)
return (
@ -111,7 +106,8 @@ export const Leaf = compose(
<Link
to={
sitePaths.documentation.index + '/' + encodeRuleName(dottedName)
}>
}
>
<span className="name">
{rule.title || capitalise0(name)} {filter}
</span>
@ -120,7 +116,8 @@ export const Leaf = compose(
<span
css={`
margin: 0 0.3rem;
`}>
`}
>
<NodeValuePointer data={nodeValue} unit={unit} />
</span>
)}
@ -128,7 +125,7 @@ export const Leaf = compose(
)}
</span>
)
})
}
export function SimpleRuleLink({ rule: { dottedName, title, name } }) {
return (
@ -139,7 +136,7 @@ export function SimpleRuleLink({ rule: { dottedName, title, name } }) {
}
export let sortObjectByKeys = pipe(
toPairs,
toPairs as any,
// we don't rely on the sorting of objects
sort(([k1], [k2]) => k1 - k2)
sort(([k1]: [number], [k2]: [number]) => k1 - k2)
)

View File

@ -1,8 +1,15 @@
import { remove, isEmpty, unnest } from 'ramda'
import { isEmpty, remove, unnest } from 'ramda'
import i18n from '../i18n'
type BaseUnit = string
export type Unit = {
numerators: Array<BaseUnit>
denominators: Array<BaseUnit>
}
//TODO this function does not handle complex units like passenger-kilometer/flight
export let parseUnit = string => {
export let parseUnit = (string: string): Unit => {
let [a, b = ''] = string.split('/'),
result = {
numerators: a !== '' ? [getUnitKey(a)] : [],
@ -14,21 +21,25 @@ export let parseUnit = string => {
const translations = Object.entries(
i18n.getResourceBundle(i18n.language, 'units')
)
function getUnitKey(unit) {
function getUnitKey(unit: string): string {
const key = translations
.find(([, trans]) => trans === unit)?.[0]
.replace(/_plural$/, '')
return key || unit
}
let printUnits = (units, count) =>
let printUnits = (units: Array<string>, count: number): string =>
units
.filter(unit => unit !== '%')
.map(unit => i18n.t(`units:${unit}`, { count }))
.join('-')
const plural = 2
export let serialiseUnit = (rawUnit, count = plural, lng = undefined) => {
export let serialiseUnit = (
rawUnit: Unit | null | string,
count: number = plural,
lng?: string
) => {
if (rawUnit === null || typeof rawUnit !== 'object') {
return typeof rawUnit === 'string'
? i18n.t(`units:${rawUnit}`, { count, lng })
@ -54,8 +65,13 @@ export let serialiseUnit = (rawUnit, count = plural, lng = undefined) => {
return string
}
type SupportedOperators = '*' | '/' | '+' | '-'
let noUnit = { numerators: [], denominators: [] }
export let inferUnit = (operator, rawUnits) => {
export let inferUnit = (
operator: SupportedOperators,
rawUnits: Array<Unit>
): Unit => {
let units = rawUnits.map(u => u || noUnit)
if (operator === '*')
return simplify({
@ -80,13 +96,14 @@ export let inferUnit = (operator, rawUnits) => {
return null
}
export let removeOnce = element => list => {
export let removeOnce = <T>(element: T) => (list: Array<T>): Array<T> => {
let index = list.indexOf(element)
if (index > -1) return remove(index, 1)(list)
if (index > -1) return remove<T>(index, 1)(list)
else return list
}
let simplify = unit =>
let simplify = (unit: Unit): Unit =>
[...unit.numerators, ...unit.denominators].reduce(
({ numerators, denominators }, next) =>
numerators.includes(next) && denominators.includes(next)

View File

@ -4,6 +4,8 @@ import enTranslations from './locales/en.yaml'
import unitsTranslations from './locales/units.yaml'
import { getSessionStorage } from './utils'
export type AvailableLangs = 'fr' | 'en'
let lang =
(typeof document !== 'undefined' &&
new URLSearchParams(document.location.search.substring(1)).get('lang')) ||

View File

@ -1,13 +1,13 @@
import { Action as CreationChecklistAction } from 'Actions/companyCreationChecklistActions'
import { Action as HiringChecklist } from 'Actions/hiringChecklistAction'
import { omit } from 'ramda'
import { combineReducers } from 'redux'
import type {
import { LegalStatus } from 'Selectors/companyStatusSelectors'
import {
Action as CompanyStatusAction,
LegalStatusRequirements,
State
LegalStatusRequirements
} from 'Types/companyTypes'
import type { Action as CreationChecklistAction } from 'Types/companyCreationChecklistTypes'
import type { Action as HiringChecklist } from 'Types/hiringChecklistTypes'
type Action = CompanyStatusAction | CreationChecklistAction | HiringChecklist
function companyLegalStatus(
@ -32,7 +32,10 @@ function companyLegalStatus(
return state
}
function hiringChecklist(state: { [string]: boolean } = {}, action: Action) {
function hiringChecklist(
state: { [key: string]: boolean } = {},
action: Action
) {
switch (action.type) {
case 'CHECK_HIRING_ITEM':
return {
@ -43,16 +46,16 @@ function hiringChecklist(state: { [string]: boolean } = {}, action: Action) {
return Object.keys(state).length
? state
: action.checklistItems.reduce(
(checklist, item) => ({ ...checklist, [item]: false }),
{}
)
(checklist, item) => ({ ...checklist, [item]: false }),
{}
)
default:
return state
}
}
function companyCreationChecklist(
state: { [string]: boolean } = {},
state: { [key: string]: boolean } = {},
action: Action
) {
switch (action.type) {
@ -65,9 +68,9 @@ function companyCreationChecklist(
return Object.keys(state).length
? state
: action.checklistItems.reduce(
(checklist, item) => ({ ...checklist, [item]: false }),
{}
)
(checklist, item) => ({ ...checklist, [item]: false }),
{}
)
case 'RESET_COMPANY_STATUS_CHOICE':
return {}
default:
@ -75,7 +78,10 @@ function companyCreationChecklist(
}
}
function companyStatusChoice(state: ?string = null, action: Action) {
function companyStatusChoice(
state: LegalStatus = null,
action: Action
): LegalStatus {
if (action.type === 'RESET_COMPANY_STATUS_CHOICE') {
return null
}
@ -85,7 +91,9 @@ function companyStatusChoice(state: ?string = null, action: Action) {
return action.statusName
}
const infereLegalStatusFromCategorieJuridique = catégorieJuridique => {
const infereLegalStatusFromCategorieJuridique = (
catégorieJuridique: string
) => {
/*
Nous utilisons le code entreprise pour connaitre le statut juridique
(voir https://www.insee.fr/fr/information/2028129)
@ -114,14 +122,16 @@ const infereLegalStatusFromCategorieJuridique = catégorieJuridique => {
}
return 'NON_IMPLÉMENTÉ'
}
function existingCompany(
state: ?{
siren: string,
catégorieJuridique: ?string,
statutJuridique: string
} = null,
action
) {
export type Company = {
siren: string
catégorieJuridique?: string
statutJuridique?: string
isAutoEntrepreneur?: boolean
isDirigeantMajoritaire?: boolean
}
function existingCompany(state: Company = null, action): Company {
if (!action.type.startsWith('EXISTING_COMPANY::')) {
return state
}
@ -150,11 +160,10 @@ function existingCompany(
return state
}
// $FlowFixMe
export default (combineReducers({
export default combineReducers({
companyLegalStatus,
companyStatusChoice,
companyCreationChecklist,
existingCompany,
hiringChecklist
}): (State, Action) => State)
})

View File

@ -1,19 +1,33 @@
import { Action } from 'Actions/actions'
import { findRuleByDottedName } from 'Engine/rules'
import { compose, defaultTo, dissoc, identity, lensPath, omit, over, set, uniq, without } from 'ramda'
import {
compose,
defaultTo,
dissoc,
identity,
lensPath,
omit,
over,
set,
uniq,
without
} from 'ramda'
import reduceReducers from 'reduce-reducers'
import { combineReducers } from 'redux'
import { combineReducers, Reducer } from 'redux'
import { targetNamesSelector } from 'Selectors/analyseSelectors'
import i18n from '../i18n'
import { SavedSimulation } from 'Selectors/storageSelectors'
import { DottedName, Rule } from 'Types/rule'
import i18n, { AvailableLangs } from '../i18n'
import inFranceAppReducer from './inFranceAppReducer'
import storageRootReducer from './storageReducer'
import type { Action } from 'Types/ActionsTypes'
function explainedVariable(state = null, { type, variableName = null }) {
switch (type) {
function explainedVariable(
state: DottedName = null,
action: Action
): DottedName {
switch (action.type) {
case 'EXPLAIN_VARIABLE':
return variableName
return action.variableName
case 'STEP_ACTION':
return null
default:
@ -21,28 +35,32 @@ function explainedVariable(state = null, { type, variableName = null }) {
}
}
function currentExample(state = null, { type, situation, name, dottedName }) {
switch (type) {
function currentExample(state = null, action: Action) {
switch (action.type) {
case 'SET_EXAMPLE':
const { situation, name, dottedName } = action
return name != null ? { name, situation, dottedName } : null
default:
return state
}
}
function situationBranch(state = null, { type, id }) {
switch (type) {
function situationBranch(state: number = null, action: Action): number {
switch (action.type) {
case 'SET_SITUATION_BRANCH':
return id
return action.id
default:
return state
}
}
function activeTargetInput(state = null, { type, name }) {
switch (type) {
function activeTargetInput(
state: DottedName | null = null,
action: Action
): DottedName | null {
switch (action.type) {
case 'SET_ACTIVE_TARGET_INPUT':
return name
return action.name
case 'RESET_SIMULATION':
return null
default:
@ -50,7 +68,10 @@ function activeTargetInput(state = null, { type, name }) {
}
}
function lang(state = i18n.language, { type, lang }) {
function lang(
state = i18n.language as AvailableLangs,
{ type, lang }
): AvailableLangs {
switch (type) {
case 'SWITCH_LANG':
return lang
@ -59,10 +80,10 @@ function lang(state = i18n.language, { type, lang }) {
}
}
type ConversationSteps = {|
+foldedSteps: Array < string >,
+unfoldedStep: ?string
|}
type ConversationSteps = {
foldedSteps: Array<string>
unfoldedStep?: string
}
function conversationSteps(
state: ConversationSteps = {
@ -91,7 +112,7 @@ function conversationSteps(
}
function updateSituation(situation, { fieldName, value, config, rules }) {
const goals = targetNamesSelector({ simulation: { config } }).filter(
const goals = targetNamesSelector({ simulation: { config } } as any).filter(
dottedName => {
const target = rules.find(r => r.dottedName === dottedName)
const isSmallTarget = !target.question || !target.formule
@ -115,14 +136,16 @@ function updatePeriod(situation, { toPeriod, rules }) {
const needConversion = Object.keys(situation).filter(dottedName => {
const rule = findRuleByDottedName(rules, dottedName)
return rule ?.période === 'flexible'
return rule?.période === 'flexible'
})
const updatedSituation = Object.entries(situation)
.filter(([fieldName]) => needConversion.includes(fieldName))
.map(([fieldName, value]) => [
fieldName,
currentPeriod === 'mois' && toPeriod === 'année' ? value * 12 : value / 12
currentPeriod === 'mois' && toPeriod === 'année'
? (value as number) * 12
: (value as number) / 12
])
return {
@ -132,7 +155,34 @@ function updatePeriod(situation, { toPeriod, rules }) {
}
}
function simulation(state = null, action, rules) {
type QuestionsKind =
| "à l'affiche"
| 'non prioritaires'
| 'uniquement'
| 'liste noire'
export type SimulationConfig = Partial<{
objectifs:
| Array<DottedName>
| Array<{ icône: string; nom: string; objectifs: Array<DottedName> }>
questions: Partial<Record<QuestionsKind, Array<DottedName>>>
bloquant: Array<DottedName>
situation: Simulation['situation']
branches: Array<{ nom: string; situation: SimulationConfig['situation'] }>
}>
export type Simulation = {
config: SimulationConfig
url: string
hiddenControls: Array<string>
situation: Record<DottedName, any>
}
function simulation(
state: Simulation = null,
action: Action,
rules: Array<Rule>
): Simulation | null {
if (action.type === 'SET_SIMULATION') {
const { config, url } = action
return { config, url, hiddenControls: [], situation: {} }
@ -167,23 +217,33 @@ function simulation(state = null, action, rules) {
return state
}
const addAnswerToSituation = (dottedName, value, state) => {
return compose(
const addAnswerToSituation = (
dottedName: DottedName,
value: any,
state: RootState
) => {
return (compose(
set(lensPath(['simulation', 'situation', dottedName]), value),
over(lensPath(['conversationSteps', 'foldedSteps']), (steps = []) =>
uniq([...steps, dottedName])
)
)(state)
) as any
) as any)(state)
}
const removeAnswerFromSituation = (dottedName, state) => {
return compose(
const removeAnswerFromSituation = (
dottedName: DottedName,
state: RootState
) => {
return (compose(
over(lensPath(['simulation', 'situation']), dissoc(dottedName)),
over(lensPath(['conversationSteps', 'foldedSteps']), without([dottedName]))
)(state)
over(
lensPath(['conversationSteps', 'foldedSteps']),
without([dottedName])
) as any
) as any)(state)
}
const existingCompanyRootReducer = (state, action) => {
const existingCompanyRootReducer = (state: RootState, action): RootState => {
if (!action.type.startsWith('EXISTING_COMPANY::')) {
return state
}
@ -200,21 +260,26 @@ const existingCompanyRootReducer = (state, action) => {
return state
}
const mainReducer = (state, action: Action) =>
combineReducers({
conversationSteps,
lang,
rules: defaultTo(null) as Reducer<Array<Rule>>,
explainedVariable,
// We need to access the `rules` in the simulation reducer
simulation: (a: Simulation | null, b: Action) =>
simulation(a, b, state.rules),
previousSimulation: defaultTo(null) as Reducer<SavedSimulation>,
currentExample,
situationBranch,
activeTargetInput,
inFranceApp: inFranceAppReducer
})(state, action)
export default reduceReducers(
existingCompanyRootReducer,
storageRootReducer,
(state, action) =>
combineReducers({
conversationSteps,
lang,
rules: defaultTo(null),
explainedVariable,
// We need to access the `rules` in the simulation reducer
simulation: (a, b) => simulation(a, b, state.rules),
previousSimulation: defaultTo(null),
currentExample,
situationBranch,
activeTargetInput,
inFranceApp: inFranceAppReducer
})(state, action)
mainReducer
)
export type RootState = ReturnType<typeof mainReducer>

View File

@ -1,9 +1,8 @@
import type { State } from 'Types/State'
import type { Action } from 'Types/ActionsTypes'
import { Action } from 'Actions/actions'
import { createStateFromSavedSimulation } from 'Selectors/storageSelectors'
import { RootState } from './rootReducer'
export default (state: State, action: Action): State => {
export default (state: RootState, action: Action): RootState => {
switch (action.type) {
case 'LOAD_PREVIOUS_SIMULATION':
return {

View File

@ -1,63 +1,38 @@
import {
collectMissingVariablesByTarget,
getNextSteps
} from 'Engine/generateQuestions'
import {
collectDefaults,
disambiguateExampleSituation,
findRuleByDottedName
} from 'Engine/rules'
import { collectMissingVariablesByTarget, getNextSteps } from 'Engine/generateQuestions'
import { collectDefaults, disambiguateExampleSituation, findRuleByDottedName } from 'Engine/rules'
import { analyse, analyseMany, parseAll } from 'Engine/traverse'
import {
add,
defaultTo,
difference,
dissoc,
equals,
head,
intersection,
isEmpty,
isNil,
last,
length,
map,
mergeDeepWith,
negate,
pick,
pipe,
sortBy,
split,
takeWhile,
zipWith
} from 'ramda'
import { add, defaultTo, difference, dissoc, equals, head, intersection, isEmpty, isNil, last, length, map, mergeDeepWith, negate, pick, pipe, sortBy, split, takeWhile, zipWith } from 'ramda'
import { useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'
import { DottedName } from "Types/rule"
import { mapOrApply } from '../utils'
// create a "selector creator" that uses deep equal instead of ===
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, equals)
// We must here compute parsedRules, flatRules, analyse which contains both targets and cache objects
export let flatRulesSelector = (state, props) => {
export let flatRulesSelector = (
state: RootState,
props?: { rules: RootState['rules'] }
) => {
return props?.rules || state?.rules
}
export let parsedRulesSelector = createSelector(
[flatRulesSelector],
rules => parseAll(rules)
export let parsedRulesSelector = createSelector([flatRulesSelector], rules =>
parseAll(rules)
)
export let ruleDefaultsSelector = createSelector(
[flatRulesSelector],
rules => collectDefaults(rules)
export let ruleDefaultsSelector = createSelector([flatRulesSelector], rules =>
collectDefaults(rules)
)
export let targetNamesSelector = state => {
export let targetNamesSelector = (state: RootState) => {
let objectifs = state.simulation?.config.objectifs
if (!objectifs || !Array.isArray(objectifs)) {
return null
}
const targetNames = [].concat(
...objectifs.map(objectifOrGroup =>
...(objectifs as any).map(objectifOrGroup =>
typeof objectifOrGroup === 'string'
? [objectifOrGroup]
: objectifOrGroup.objectifs
@ -70,17 +45,16 @@ export let targetNamesSelector = state => {
return [...targetNames, ...secondaryTargetNames]
}
export let situationSelector = state => state.simulation?.situation || {}
type SituationSelectorType = typeof situationSelector
export const useSituation = () => useSelector(situationSelector)
export const situationSelector = (state: RootState) =>
state.simulation?.situation || {}
export const useSituationValue = fieldName => useSituation()?.[fieldName]
export const usePeriod = () => useSelector(situationSelector)['période']
export const usePeriod = () => useSituationValue('période')
export const useTarget = dottedName => {
export const useTarget = (dottedName: DottedName) => {
const targets = useSelector(
state => analysisWithDefaultsSelector(state).targets
(state: RootState) => analysisWithDefaultsSelector(state).targets
)
return targets?.find(t => t.dottedName === dottedName)
}
@ -119,10 +93,13 @@ let validatedStepsSelector = createSelector(
[state => state.conversationSteps.foldedSteps, targetNamesSelector],
(foldedSteps, targetNames) => [...foldedSteps, ...targetNames]
)
let branchesSelector = state => state.simulation?.config.branches
let configSituationSelector = state => state.simulation?.config.situation || {}
let branchesSelector = (state: RootState) => state.simulation?.config.branches
let configSituationSelector = (state: RootState) =>
state.simulation?.config.situation || {}
const createSituationBrancheSelector = situationSelector =>
const createSituationBrancheSelector = (
situationSelector: SituationSelectorType
) =>
createSelector(
[situationSelector, branchesSelector, configSituationSelector],
(situation, branches, configSituation) => {
@ -209,13 +186,16 @@ export let exampleAnalysisSelector = createSelector(
analyseRule(rules, dottedName, dottedName => situation[dottedName])
)
let makeAnalysisSelector = situationSelector =>
let makeAnalysisSelector = (situationSelector: SituationSelectorType) =>
createDeepEqualSelector(
[parsedRulesSelector, targetNamesSelector, situationSelector],
(parsedRules, targetNames, situations) =>
mapOrApply(
situation =>
analyseMany(parsedRules, targetNames)(dottedName => {
analyseMany(
parsedRules,
targetNames
)(dottedName => {
return situation[dottedName]
}),
situations
@ -259,11 +239,11 @@ let currentMissingVariablesByTargetSelector = createSelector(
}
)
const similarity = (rule1, rule2) =>
const similarity = (rule1: DottedName, rule2: DottedName) =>
pipe(
map(defaultTo('')),
map(split(' . ')),
rules => zipWith(equals, ...rules),
(rules: [string[], string[]]) => zipWith(equals, ...rules),
takeWhile(Boolean),
length,
negate

View File

@ -1,12 +1,20 @@
/* @flow */
import { SitePaths } from 'Components/utils/withSitePaths'
import {
add,
any,
countBy,
difference,
flatten,
isNil,
keys,
map,
mergeAll,
mergeWith,
sortBy
} from 'ramda'
import { LegalStatusRequirements, State } from 'Types/companyTypes'
import type { State, LegalStatusRequirements } from 'Types/companyTypes'
import type { SitePaths } from 'Components/utils/withSitePaths'
import { add, any, countBy, difference, flatten, isNil, map, mergeAll, mergeWith, sortBy } from 'ramda'
const LEGAL_STATUS_DETAILS: {
[status: string]: Array<LegalStatusRequirements> | LegalStatusRequirements
} = {
const LEGAL_STATUS_DETAILS = {
'auto-entrepreneur': {
soleProprietorship: true,
directorStatus: 'SELF_EMPLOYED',
@ -48,7 +56,7 @@ const LEGAL_STATUS_DETAILS: {
multipleAssociates: true,
autoEntrepreneur: false
},
SARL: ([
SARL: [
{
soleProprietorship: false,
directorStatus: 'SELF_EMPLOYED',
@ -63,7 +71,7 @@ const LEGAL_STATUS_DETAILS: {
minorityDirector: true,
autoEntrepreneur: false
}
]: Array<LegalStatusRequirements>),
] as Array<LegalStatusRequirements>,
EURL: {
soleProprietorship: false,
directorStatus: 'SELF_EMPLOYED',
@ -79,18 +87,14 @@ const LEGAL_STATUS_DETAILS: {
}
}
export type LegalStatus = $Keys<typeof LEGAL_STATUS_DETAILS>
type Question = $Keys<LegalStatusRequirements>
export type LegalStatus = keyof typeof LEGAL_STATUS_DETAILS
type Question = keyof LegalStatusRequirements
// $FlowFixMe
const QUESTION_LIST: Array<Question> = Object.keys(
// $FlowFixMe
const QUESTION_LIST: Array<Question> = keys(
mergeAll(flatten(Object.values(LEGAL_STATUS_DETAILS)))
)
const isCompatibleStatusWith = (answers: LegalStatusRequirements) => (
statusRequirements: LegalStatusRequirements
): boolean => {
const isCompatibleStatusWith = answers => (statusRequirements): boolean => {
const stringify = map(x => (!isNil(x) ? JSON.stringify(x) : x))
const answerCompatibility = Object.values(
mergeWith(
@ -105,46 +109,49 @@ const isCompatibleStatusWith = (answers: LegalStatusRequirements) => (
}
const possibleStatus = (
answers: LegalStatusRequirements
): { [LegalStatus]: boolean } =>
): Record<LegalStatus, boolean> =>
map(
statusRequirements =>
Array.isArray(statusRequirements)
? any(isCompatibleStatusWith(answers), statusRequirements)
: isCompatibleStatusWith(answers)(statusRequirements),
: isCompatibleStatusWith(answers)(
statusRequirements as LegalStatusRequirements
),
LEGAL_STATUS_DETAILS
)
export const possibleStatusSelector = (state: {
inFranceApp: State
}): { [LegalStatus]: boolean } =>
}): Record<LegalStatus, boolean> =>
possibleStatus(state.inFranceApp.companyLegalStatus)
export const nextQuestionSelector = (state: {
inFranceApp: State
}): ?Question => {
}): Question => {
const legalStatusRequirements = state.inFranceApp.companyLegalStatus
const questionAnswered = Object.keys(legalStatusRequirements)
const questionAnswered = Object.keys(legalStatusRequirements) as Array<
Question
>
const possibleStatusList = flatten(
Object.values(LEGAL_STATUS_DETAILS)
// $FlowFixMe
).filter(isCompatibleStatusWith(legalStatusRequirements))
const unansweredQuestions = difference(QUESTION_LIST, questionAnswered)
const shannonEntropyByQuestion = unansweredQuestions.map(question => {
const shannonEntropyByQuestion = unansweredQuestions.map((question): [
typeof question,
number
] => {
const answerPopulation = Object.values(possibleStatusList).map(
// $FlowFixMe
status => status[question]
)
const frequencyOfAnswers = Object.values(
countBy(x => x, answerPopulation.filter(x => x !== undefined))
).map(
numOccurrence =>
// $FlowFixMe
numOccurrence / answerPopulation.length
)
countBy(
x => x,
answerPopulation.filter(x => x !== undefined)
)
).map(numOccurrence => numOccurrence / answerPopulation.length)
const shannonEntropy = -frequencyOfAnswers
.map(p => p * Math.log2(p))
// $FlowFixMe
.reduce(add, 0)
return [question, shannonEntropy]
})

View File

@ -1,6 +1,7 @@
import { RootState } from 'Reducers/rootReducer'
import { nextStepsSelector } from './analyseSelectors'
export const simulationProgressSelector = state => {
export const simulationProgressSelector = (state: RootState) => {
const numberQuestionAnswered = state.conversationSteps.foldedSteps.length
const numberQuestionLeft = nextStepsSelector(state).length
return numberQuestionAnswered / (numberQuestionAnswered + numberQuestionLeft)

View File

@ -1,23 +0,0 @@
/* @flow */
import type { SavedSimulation, State } from 'Types/State.js'
export const currentSimulationSelector: State => SavedSimulation = state => {
return {
situation: state.simulation.situation,
activeTargetInput: state.activeTargetInput,
foldedSteps: state.conversationSteps.foldedSteps
}
}
export const createStateFromSavedSimulation = state =>
state.previousSimulation && {
activeTargetInput: state.previousSimulation.activeTargetInput,
simulation: {
...state.simulation,
situation: state.previousSimulation.situation || {}
},
conversationSteps: {
foldedSteps: state.previousSimulation.foldedSteps
},
previousSimulation: null
}

View File

@ -0,0 +1,33 @@
import { RootState } from 'Reducers/rootReducer'
// Note: it is currently not possible to define SavedSimulation as the return
// type of the currentSimulationSelector function because the type would then
// circulary reference itself.
export type SavedSimulation = {
situation: RootState['simulation']['situation']
activeTargetInput: RootState['activeTargetInput']
foldedSteps: RootState['conversationSteps']['foldedSteps']
}
export const currentSimulationSelector = (
state: RootState
): SavedSimulation => {
return {
situation: state.simulation.situation,
activeTargetInput: state.activeTargetInput,
foldedSteps: state.conversationSteps.foldedSteps
}
}
export const createStateFromSavedSimulation = (state: RootState) =>
state.previousSimulation && {
activeTargetInput: state.previousSimulation.activeTargetInput,
simulation: {
...state.simulation,
situation: state.previousSimulation.situation || {}
},
conversationSteps: {
foldedSteps: state.previousSimulation.foldedSteps
},
previousSimulation: null
}

View File

@ -15,7 +15,7 @@ const rewrite = basename => ({
app.get('/', function(req, res) {
res.send(`<ul><li><a href="/mon-entreprise">mon-entreprise [fr]</a></li>
<li><a href="/infrance">infrance [en]</a></li>
<li><a href="/mon-entreprise/dev/integration-test">intégration du simulateur sur site tiers [iframe fr]</a></li><li><a href="/publicodes">publicodes</a></li><</ul>`)
<li><a href="/mon-entreprise/dev/integration-test">intégration du simulateur sur site tiers [iframe fr]</a></li><li><a href="/publicodes">publicodes</a></li></ul>`)
})
app.use(

View File

@ -5,5 +5,4 @@ import 'regenerator-runtime/runtime'
import App from './App'
let anchor = document.querySelector('#js')
render(<App language="en" basename="infrance" />, anchor)

View File

@ -8,7 +8,7 @@ import emoji from 'react-easy-emoji'
import { Helmet } from 'react-helmet'
import { Link } from 'react-router-dom'
import SocialIcon from 'Ui/SocialIcon'
import i18n from '../../../../i18n'
import i18n, { AvailableLangs } from '../../../../i18n'
import { hrefLangLink } from '../../sitePaths'
import './Footer.css'
import Privacy from './Privacy'
@ -25,7 +25,7 @@ const feedbackBlacklist = [
const Footer = () => {
const sitePaths = useContext(SitePathsContext)
const hrefLink =
hrefLangLink[i18n.language][
hrefLangLink[i18n.language as AvailableLangs][
decodeURIComponent(
(process.env.NODE_ENV === 'production'
? window.location.protocol + '//' + window.location.host
@ -99,7 +99,8 @@ const Footer = () => {
<a
href={href}
key={hrefLang}
style={{ textDecoration: 'underline' }}>
style={{ textDecoration: 'underline' }}
>
{hrefLang === 'fr' ? (
<> Passer en français {emoji('🇫🇷')}</>
) : hrefLang === 'en' ? (

View File

@ -30,7 +30,7 @@ export default function Privacy() {
)
}
export let PrivacyContent = ({ language }) => (
export let PrivacyContent = ({ language }: { language: string }) => (
<>
<T k="privacyContent">
<h1>Vie privée</h1>

View File

@ -46,7 +46,7 @@ export default (tracker: Tracker) => {
...(action.type === 'UPDATE_PERIOD'
? ['période', action.toPeriod]
: [action.fieldName, action.value])
] as any)
])
}
if (action.type === 'START_CONVERSATION') {
tracker.push([

View File

@ -5,13 +5,14 @@ import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { RootState } from 'Reducers/rootReducer'
import Animate from 'Ui/animate'
import siret from './siret.jpg'
export default function AfterRegistration() {
const sitePaths = useContext(SitePathsContext)
const statutChoisi = useSelector<any, any>(
state => state.inFranceApp.companyStatusChoice
const statutChoisi = useSelector(
(state: RootState) => state.inFranceApp.companyStatusChoice
)
const { t } = useTranslation()
const isAutoentrepreneur = statutChoisi.match('auto-entrepreneur')
@ -24,7 +25,8 @@ export default function AfterRegistration() {
to={sitePaths.créer.index}
exact
activeClassName="ui__ hide"
className="ui__ simple small button">
className="ui__ simple small button"
>
<T>Retour à la création</T>
</NavLink>
</div>
@ -76,7 +78,8 @@ export default function AfterRegistration() {
statutChoisi && statutChoisi.match(/auto-entrepreneur|EI/)
? { display: 'none' }
: {}
}>
}
>
Il détermine aussi la convention collective applicable à
l'entreprise, et en partie le taux de la cotisation accidents du
travail et maladies professionnelles à payer.

View File

@ -12,6 +12,7 @@ import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { connect, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { RootState } from 'Reducers/rootReducer'
import * as Animate from 'Ui/animate'
import { CheckItem, Checklist } from 'Ui/Checklist'
import StatutDescription from './StatutDescription'
@ -24,8 +25,8 @@ function CreateCompany({
}) {
const { t, i18n } = useTranslation()
const sitePaths = useContext(SitePathsContext)
const companyCreationChecklist = useSelector<any, any>(
state => state.inFranceApp.companyCreationChecklist
const companyCreationChecklist = useSelector(
(state: RootState) => state.inFranceApp.companyCreationChecklist
)
// TODO : add this logic inside selector
@ -75,7 +76,8 @@ function CreateCompany({
<div css="transform: translateY(2rem);">
<button
onClick={onStatusChange}
className="ui__ simple small push-left button">
className="ui__ simple small push-left button"
>
<T k="entreprise.retour"> Choisir un autre statut</T>
</button>
</div>
@ -100,7 +102,8 @@ function CreateCompany({
key={statut}
onInitialization={items => onChecklistInitialization(statut, items)}
onItemCheck={x => onItemCheck}
defaultChecked={companyCreationChecklist}>
defaultChecked={companyCreationChecklist}
>
<CheckItem
name="legalStatus"
defaultChecked={true}
@ -191,7 +194,8 @@ function CreateCompany({
<span
style={{
display: multipleAssociates ? 'visible' : 'none'
}}>
}}
>
Dans le cas d'une création d'entreprise avec plusieurs
associés, il est recommandé de faire appel à un juriste pour
les rédiger.{' '}
@ -365,7 +369,8 @@ function CreateCompany({
? 'https://www.autoentrepreneur.urssaf.fr/portail/accueil/creer-mon-auto-entreprise.html'
: 'https://account.guichet-entreprises.fr/user/create'
}
target="blank">
target="blank"
>
Faire la démarche en ligne
</a>
</div>
@ -437,14 +442,16 @@ function CreateCompany({
> * {
flex: 1;
}
`}>
`}
>
{isAutoentrepreneur && (
<Link
className="ui__ interactive card button-choice lighter-bg"
to={{
pathname: sitePaths.simulateurs['auto-entrepreneur'],
state: { fromCréer: true }
}}>
}}
>
<T k="entreprise.ressources.simu.autoEntrepreneur">
<p>Simulateur de revenus auto-entrepreneur</p>
<small>
@ -460,7 +467,8 @@ function CreateCompany({
to={{
pathname: sitePaths.simulateurs.indépendant,
state: { fromCréer: true }
}}>
}}
>
<T k="entreprise.ressources.simu.indépendant">
<p>Simulateur de cotisations indépendant</p>
<small>
@ -476,7 +484,8 @@ function CreateCompany({
to={{
pathname: sitePaths.simulateurs['assimilé-salarié'],
state: { fromCréer: true }
}}>
}}
>
<T k="entreprise.ressources.simu.assimilé">
<p>Simulateur de cotisations assimilé-salarié</p>
<small>
@ -488,7 +497,8 @@ function CreateCompany({
)}
<Link
className="ui__ interactive card button-choice lighter-bg"
to={sitePaths.créer.après}>
to={sitePaths.créer.après}
>
<T k="entreprise.ressources.après">
<p>Après la création</p>
<small>
@ -501,7 +511,8 @@ function CreateCompany({
<a
target="_blank"
className="ui__ interactive card button-choice lighter-bg"
href="https://www.urssaf.fr/portail/files/live/sites/urssaf/files/documents/SSI-Guide-Objectif-Entreprise.pdf">
href="https://www.urssaf.fr/portail/files/live/sites/urssaf/files/documents/SSI-Guide-Objectif-Entreprise.pdf"
>
<p>Guide de création URSSAF </p>
<small>
Des conseils sur comment préparer son projet pour se lancer dans
@ -519,14 +530,11 @@ function CreateCompany({
)
}
export default connect(
null,
{
onChecklistInitialization: initializeCompanyCreationChecklist,
onItemCheck: checkCompanyCreationItem,
onStatusChange: goToCompanyStatusChoice
}
)(CreateCompany)
export default connect(null, {
onChecklistInitialization: initializeCompanyCreationChecklist,
onItemCheck: checkCompanyCreationItem,
onStatusChange: goToCompanyStatusChoice
})(CreateCompany)
let StatutsExample = ({ statut }) => {
const links = {

View File

@ -12,7 +12,11 @@ import {
} from 'Selectors/companyStatusSelectors'
import StatutDescription from '../StatutDescription'
const StatutButton = ({ statut }: { statut: LegalStatus }) => {
type StatutButtonProps = {
statut: LegalStatus
}
const StatutButton = ({ statut }: StatutButtonProps) => {
const sitePaths = useContext(SitePathsContext)
const { t } = useTranslation()
return (
@ -29,7 +33,12 @@ const StatutButton = ({ statut }: { statut: LegalStatus }) => {
)
}
const StatutTitle = ({ statut, language }) =>
type StatutTitleProps = {
statut: LegalStatus
language: string
}
const StatutTitle = ({ statut, language }: StatutTitleProps) =>
statut === 'EI' ? (
<>
Entreprise individuelle {language !== 'fr' && '(Individual business)'}:{' '}
@ -61,7 +70,7 @@ const StatutTitle = ({ statut, language }) =>
</>
) : statut === 'SA' ? (
<>SA - Société anonyme {language !== 'fr' && '(Anonymous company)'}: </>
) : statut === 'SNC' ? (
) : (statut as string) === 'SNC' ? (
<>SNC - Société en nom collectif {language !== 'fr' && '(Partnership)'}: </>
) : statut === 'auto-entrepreneur' ? (
<>
@ -98,7 +107,7 @@ export default function SetMainStatus() {
</h2>
<ul>
{Object.keys(filter(Boolean, possibleStatus as any)).map(
{Object.keys(filter(Boolean, possibleStatus)).map(
(statut: keyof typeof possibleStatus) => (
<li key={statut}>
<strong>

View File

@ -4,6 +4,7 @@ import { isNil } from 'ramda'
import React, { useContext } from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { RootState } from 'Reducers/rootReducer'
import { LegalStatusRequirements } from 'Types/companyTypes'
const requirementToText = (
@ -15,9 +16,7 @@ const requirementToText = (
return value ? <T>Plusieurs associés</T> : <T>Un seul associé</T>
case 'soleProprietorship':
return value ? (
<T T k="responsabilité.bouton2">
Entreprise individuelle
</T>
<T k="responsabilité.bouton2">Entreprise individuelle</T>
) : (
<T k="responsabilité.bouton1">Société</T>
)
@ -36,8 +35,8 @@ const requirementToText = (
export default function PreviousAnswers() {
const sitePaths = useContext(SitePathsContext)
const legalStatus = useSelector<any, any>(
state => state.inFranceApp.companyLegalStatus
const legalStatus = useSelector(
(state: RootState) => state.inFranceApp.companyLegalStatus
)
return (
!!Object.values(legalStatus).length && (

View File

@ -70,14 +70,16 @@ const SoleProprietorship = ({ isSoleProprietorship }) => {
onClick={() => {
isSoleProprietorship(true)
}}
className="ui__ button">
className="ui__ button"
>
<T k="responsabilité.bouton2">Entreprise individuelle</T>
</button>
<button
onClick={() => {
isSoleProprietorship(false)
}}
className="ui__ button">
className="ui__ button"
>
<T k="responsabilité.bouton1">Société</T>
</button>
</div>
@ -87,9 +89,6 @@ const SoleProprietorship = ({ isSoleProprietorship }) => {
)
}
export default compose(
connect(
null,
{ isSoleProprietorship }
)
)(SoleProprietorship)
export default compose(connect(null, { isSoleProprietorship }))(
SoleProprietorship
)

View File

@ -5,6 +5,7 @@ import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { RootState } from 'Reducers/rootReducer'
import { nextQuestionUrlSelector } from 'Selectors/companyStatusSelectors'
import Animate from 'Ui/animate'
import créerSvg from './créer.svg'
@ -12,11 +13,12 @@ import créerSvg from './créer.svg'
export default function Créer() {
const { t } = useTranslation()
const sitePaths = useContext(SitePathsContext)
const nextQuestionUrl = useSelector(state =>
const nextQuestionUrl = useSelector((state: RootState) =>
nextQuestionUrlSelector(state, { sitePaths })
)
const guideAlreadyStarted = useSelector<any, any>(
state => !!Object.keys(state.inFranceApp.companyLegalStatus).length
const guideAlreadyStarted = useSelector(
(state: RootState) =>
!!Object.keys(state.inFranceApp.companyLegalStatus).length
)
return (
<Animate.fromBottom>
@ -42,7 +44,8 @@ export default function Créer() {
guideAlreadyStarted && nextQuestionUrl
? nextQuestionUrl
: sitePaths.créer.guideStatut.multipleAssociates
}>
}
>
{!guideAlreadyStarted
? t('créer.cta.default', 'Trouver le bon statut')
: t('créer.cta.continue', 'Continuer le guide')}
@ -72,10 +75,12 @@ export default function Créer() {
> * {
flex: 1;
}
`}>
`}
>
<Link
className="ui__ interactive card button-choice lighter-bg"
to={sitePaths.créer.guideStatut.liste}>
to={sitePaths.créer.guideStatut.liste}
>
<T k="créer.ressources.listeStatuts">
<p>Liste des statuts juridiques </p>
<small>
@ -89,7 +94,8 @@ export default function Créer() {
to={{
pathname: sitePaths.simulateurs.comparaison,
state: { fromCréer: true }
}}>
}}
>
<T k="créer.ressources.comparaison">
<p>Comparateur de régimes</p>
<small>
@ -101,7 +107,8 @@ export default function Créer() {
<Link
className="ui__ interactive card button-choice lighter-bg"
to={sitePaths.créer['auto-entrepreneur']}>
to={sitePaths.créer['auto-entrepreneur']}
>
<T k="créer.ressources.autoEntrepreneur">
<p>Démarche auto-entrepreneur</p>
<small>

View File

@ -56,7 +56,7 @@ const StatutDescription = ({ statut }: Props) =>
d'être coté en bourse (à partir de 7 actionnaires). Le capital social
minimum est de 37.000 .
</T>
) : statut === 'SNC' ? (
) : (statut as string) === 'SNC' ? (
<T k="formeJuridique.SNC">
La responsabilité des associés pour les dettes de la société est solidaire
(un seul associé peut être poursuivi pour la totalité de la dette) et

View File

@ -1,10 +1,13 @@
import withColours, { ThemeColoursProvider } from 'Components/utils/withColours'
import React, { Suspense, useState } from 'react'
import {
ThemeColoursContext,
ThemeColoursProvider
} from 'Components/utils/withColours'
import React, { Suspense, useContext, useState } from 'react'
import Home from '../Iframes/SimulateurEmbauche'
let LazyColorPicker = React.lazy(() => import('./ColorPicker'))
const Couleur = ({ colours: { colour: defaultColour } }) => {
export default function Couleur() {
const { colour: defaultColour } = useContext(ThemeColoursContext)
const [colour, setColour] = useState(defaultColour)
return (
<>
@ -28,5 +31,3 @@ const Couleur = ({ colours: { colour: defaultColour } }) => {
</>
)
}
export default withColours(Couleur)

View File

@ -14,7 +14,7 @@ export default function IntegrationTest() {
)
const [colour, setColour] = React.useState('#005aa1')
const [version, setVersion] = React.useState(0)
const domNode = React.useRef(null)
const domNode = React.useRef<HTMLDivElement>(null)
React.useEffect(() => {
const script = document.createElement('script')
script.id = 'script-monentreprise'
@ -47,14 +47,16 @@ export default function IntegrationTest() {
<button
className="ui__ button plain"
onClick={() => setVersion(version + 1)}>
onClick={() => setVersion(version + 1)}
>
{!version ? 'Visualiser le module' : 'Valider les changements'}
</button>
<div
css={`
display: ${version > 0 ? 'block' : 'none'};
`}>
`}
>
<p>Code d'intégration </p>
<IntegrationCode colour={colour} module={currentModule} />
<div style={{ border: '2px dashed blue' }}>
@ -99,7 +101,8 @@ export let IntegrationCode = ({
#scriptColor {
color: #2975d1;
}
`}>
`}
>
<span>{'<'}</span>
<em>
script

View File

@ -1,19 +1,20 @@
import withSitePaths from 'Components/utils/withSitePaths'
import React from 'react'
import { generateSiteMap } from '../../sitePaths'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import React, { useContext } from 'react'
import { generateSiteMap, SitePathsType } from '../../sitePaths'
const SiteMap = ({ sitePaths }) => (
<>
<h1>Sitemap</h1>
<pre>
{generateSiteMap(sitePaths).map(path => (
<span key={path}>
{path}
<br />
</span>
))}
</pre>
</>
)
export default withSitePaths(SiteMap)
export default function SiteMap() {
const sitePaths = useContext(SitePathsContext)
return (
<>
<h1>Sitemap</h1>
<pre>
{generateSiteMap(sitePaths as SitePathsType).map(path => (
<span key={path}>
{path}
<br />
</span>
))}
</pre>
</>
)
}

View File

@ -6,10 +6,8 @@ import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { useSelector } from 'react-redux'
import examples from 'Règles/cas-types.yaml'
import {
parsedRulesSelector,
ruleDefaultsSelector
} from 'Selectors/analyseSelectors'
import { parsedRulesSelector, ruleDefaultsSelector } from 'Selectors/analyseSelectors'
import { DottedName } from "Types/rule"
import './ExampleSituations.css'
export default function ExampleSituations() {
@ -20,7 +18,7 @@ export default function ExampleSituations() {
<T>Quelques exemples de salaires</T>
</h1>
<ul>
{examples.map(ex => (
{examples.map((ex: any) => (
<Example ex={ex} key={ex.nom} />
))}
</ul>
@ -29,14 +27,16 @@ export default function ExampleSituations() {
}
const Example = function Example({ ex: { nom, situation } }) {
const defaults = useSelector(ruleDefaultsSelector) as object
const defaults = useSelector(ruleDefaultsSelector)
const parsedRules = useSelector(parsedRulesSelector)
const colours = useContext(ThemeColoursContext)
let [total, net, netAprèsImpôts] = analyseMany(parsedRules, [
'total',
'net',
'net après impôt'
])(dottedName => ({ ...defaults, ...situation }[dottedName])).targets,
])(
(dottedName: DottedName) => ({ ...defaults, ...situation }[dottedName])
).targets,
figures = [
total,
{
@ -60,7 +60,8 @@ const Example = function Example({ ex: { nom, situation } }) {
<h3>{t.title}</h3>
<span
style={{ color: colours.textColourOnWhite }}
className="figure">
className="figure"
>
{Math.round(t.nodeValue)}
</span>
</li>

View File

@ -3,208 +3,217 @@ import {
initializeHiringChecklist
} from 'Actions/hiringChecklistAction'
import { T } from 'Components'
import { compose } from 'ramda'
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { connect, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import Animate from 'Ui/animate'
import { CheckItem, Checklist } from 'Ui/Checklist'
import { CheckItem, Checklist, ChecklistProps } from 'Ui/Checklist'
const Embaucher = ({
onChecklistInitialization,
onItemCheck,
hiringChecklist,
t
}) => (
<Animate.fromBottom>
<Helmet>
<title>
{t(['embauche.tâches.page.titre', 'Les formalités pour embaucher'])}
</title>
<meta
name="description"
content={t(
'embauche.tâches.page.description',
"Toutes les démarches nécessaires à l'embauche de votre premier salarié."
)}
/>
</Helmet>
<h1>
<T k="embauche.tâches.titre">Les formalités pour embaucher</T>
</h1>
<p>
<T k="embauche.tâches.description">
Toutes les étapes nécessaires à l'embauche de votre premier employé.
</T>
</p>
<Checklist
onInitialization={onChecklistInitialization}
onItemCheck={onItemCheck}
defaultChecked={hiringChecklist}>
<CheckItem
name="contract"
title={
<T k="embauche.tâches.contrat.titre">
Signer un contrat de travail avec votre employé
</T>
}
explanations={
<p>
<a
className="ui__ button"
href="https://www.service-public.fr/particuliers/vosdroits/N19871"
target="_blank">
{' '}
<T>Plus d'informations</T>
</a>
</p>
}
/>
<CheckItem
name="dpae"
title={
<T k="embauche.tâches.dpae.titre">
Déclarer l'embauche à l'administration sociale
</T>
}
explanations={
<p>
<T k="embauche.tâches.dpae.description">
Ceci peut être fait par le biais du formulaire appelé DPAE, doit
être complété dans les 8 jours avant toute embauche, et peut{' '}
<a href="https://www.due.urssaf.fr" target="_blank">
être effectué en ligne
</a>
.
</T>
</p>
}
/>
<CheckItem
name="paySoftware"
title={
<T k="embauche.tâches.logiciel de paie.titre">
Choisir un logiciel de paie
</T>
}
explanations={
<p>
<T k="embauche.tâches.logiciel de paie.description">
Les fiches de paie et les déclarations peuvent être traitées en
ligne gratuitement par le{' '}
<a href="http://www.letese.urssaf.fr" target="_blank">
Tese
</a>
. Vous pouvez aussi utiliser un{' '}
<a
href="http://www.dsn-info.fr/convention-charte.htm"
target="_blank">
logiciel de paie privé.
</a>
</T>
</p>
}
/>
<CheckItem
name="registre"
title={
<T k="embauche.tâches.registre.titre">
Tenir un registre des employés à jour
</T>
}
explanations={
<p>
<a
href="https://www.service-public.fr/professionnels-entreprises/vosdroits/F1784"
className="ui__ button"
target="_blank">
<T>Plus d'informations</T>
</a>
</p>
}
/>
<CheckItem
name="complementaryPension"
title={
<T k="embauche.tâches.pension.titre">
Prendre contact avec l'institution de prévoyance complémentaire
obligatoire qui vous est assignée
</T>
}
explanations={
<p>
<a
href="https://www.espace-entreprise.agirc-arrco.fr/simape/#/donneesDep"
className="ui__ button"
target="_blank">
<T k="embauche.tâches.pension.description">
Trouver mon institution de prévoyance
</T>
</a>
{/* // The AGIRC-ARRCO complementary pension is mandatory. Those are only federations,{' '} */}
</p>
}
/>
<CheckItem
name="complementaryHealth"
title={
<T k="embauche.tâches.complémentaire santé.titre">
Choisir une complémentaire santé
</T>
}
explanations={
<p>
<T k="embauche.tâches.complémentaire santé.description">
Vous devez couvrir vos salariés avec l'assurance complémentaire
santé privée de votre choix (aussi appelée "mutuelle"), pour
autant qu'elle offre un ensemble de garanties minimales.
L'employeur doit payer au moins la moitié du forfait.
</T>
</p>
}
/>
<CheckItem
name="workMedicine"
title={
<T k="embauche.tâches.medecine.titre">
S'inscrire à un bureau de médecine du travail
</T>
}
explanations={
<p>
<T k="embauche.tâches.medecine.description">
N'oubliez pas de planifier un rendez-vous initial pour chaque
nouvelle embauche.{' '}
<a href="https://www.service-public.fr/particuliers/vosdroits/F2211">
Plus d'infos.
</a>
</T>
</p>
}
/>
</Checklist>
<T k="embauche.chaque mois">
<h2>Tous les mois</h2>
<ul>
<li>
Utiliser un logiciel de paie pour calculer les cotisations sociales et
les transmettre via la déclaration sociale nominative (DSN)
</li>
<li>Remettre la fiche de paie à votre employé</li>
</ul>
</T>
</Animate.fromBottom>
)
type EmbaucherProps = {
onChecklistInitialization: ChecklistProps['onInitialization']
onItemCheck: ChecklistProps['onItemCheck']
}
export default compose(
withTranslation(),
connect(
state => ({ hiringChecklist: (state as any).inFranceApp.hiringChecklist }),
{
onChecklistInitialization: initializeHiringChecklist,
onItemCheck: checkHiringItem
}
function Embaucher({ onChecklistInitialization, onItemCheck }: EmbaucherProps) {
const { t } = useTranslation()
const hiringChecklist = useSelector(
(state: RootState) => state.inFranceApp.hiringChecklist
)
return (
<Animate.fromBottom>
<Helmet>
<title>
{t(['embauche.tâches.page.titre', 'Les formalités pour embaucher'])}
</title>
<meta
name="description"
content={t(
'embauche.tâches.page.description',
"Toutes les démarches nécessaires à l'embauche de votre premier salarié."
)}
/>
</Helmet>
<h1>
<T k="embauche.tâches.titre">Les formalités pour embaucher</T>
</h1>
<p>
<T k="embauche.tâches.description">
Toutes les étapes nécessaires à l'embauche de votre premier employé.
</T>
</p>
<Checklist
onInitialization={onChecklistInitialization}
onItemCheck={onItemCheck}
defaultChecked={hiringChecklist}
>
<CheckItem
name="contract"
title={
<T k="embauche.tâches.contrat.titre">
Signer un contrat de travail avec votre employé
</T>
}
explanations={
<p>
<a
className="ui__ button"
href="https://www.service-public.fr/particuliers/vosdroits/N19871"
target="_blank"
>
{' '}
<T>Plus d'informations</T>
</a>
</p>
}
/>
<CheckItem
name="dpae"
title={
<T k="embauche.tâches.dpae.titre">
Déclarer l'embauche à l'administration sociale
</T>
}
explanations={
<p>
<T k="embauche.tâches.dpae.description">
Ceci peut être fait par le biais du formulaire appelé DPAE, doit
être complété dans les 8 jours avant toute embauche, et peut{' '}
<a href="https://www.due.urssaf.fr" target="_blank">
être effectué en ligne
</a>
.
</T>
</p>
}
/>
<CheckItem
name="paySoftware"
title={
<T k="embauche.tâches.logiciel de paie.titre">
Choisir un logiciel de paie
</T>
}
explanations={
<p>
<T k="embauche.tâches.logiciel de paie.description">
Les fiches de paie et les déclarations peuvent être traitées en
ligne gratuitement par le{' '}
<a href="http://www.letese.urssaf.fr" target="_blank">
Tese
</a>
. Vous pouvez aussi utiliser un{' '}
<a
href="http://www.dsn-info.fr/convention-charte.htm"
target="_blank"
>
logiciel de paie privé.
</a>
</T>
</p>
}
/>
<CheckItem
name="registre"
title={
<T k="embauche.tâches.registre.titre">
Tenir un registre des employés à jour
</T>
}
explanations={
<p>
<a
href="https://www.service-public.fr/professionnels-entreprises/vosdroits/F1784"
className="ui__ button"
target="_blank"
>
<T>Plus d'informations</T>
</a>
</p>
}
/>
<CheckItem
name="complementaryPension"
title={
<T k="embauche.tâches.pension.titre">
Prendre contact avec l'institution de prévoyance complémentaire
obligatoire qui vous est assignée
</T>
}
explanations={
<p>
<a
href="https://www.espace-entreprise.agirc-arrco.fr/simape/#/donneesDep"
className="ui__ button"
target="_blank"
>
<T k="embauche.tâches.pension.description">
Trouver mon institution de prévoyance
</T>
</a>
{/* // The AGIRC-ARRCO complementary pension is mandatory. Those are only federations,{' '} */}
</p>
}
/>
<CheckItem
name="complementaryHealth"
title={
<T k="embauche.tâches.complémentaire santé.titre">
Choisir une complémentaire santé
</T>
}
explanations={
<p>
<T k="embauche.tâches.complémentaire santé.description">
Vous devez couvrir vos salariés avec l'assurance complémentaire
santé privée de votre choix (aussi appelée "mutuelle"), pour
autant qu'elle offre un ensemble de garanties minimales.
L'employeur doit payer au moins la moitié du forfait.
</T>
</p>
}
/>
<CheckItem
name="workMedicine"
title={
<T k="embauche.tâches.medecine.titre">
S'inscrire à un bureau de médecine du travail
</T>
}
explanations={
<p>
<T k="embauche.tâches.medecine.description">
N'oubliez pas de planifier un rendez-vous initial pour chaque
nouvelle embauche.{' '}
<a href="https://www.service-public.fr/particuliers/vosdroits/F2211">
Plus d'infos.
</a>
</T>
</p>
}
/>
</Checklist>
<T k="embauche.chaque mois">
<h2>Tous les mois</h2>
<ul>
<li>
Utiliser un logiciel de paie pour calculer les cotisations sociales
et les transmettre via la déclaration sociale nominative (DSN)
</li>
<li>Remettre la fiche de paie à votre employé</li>
</ul>
</T>
</Animate.fromBottom>
)
}
export default connect(
(state: RootState) => ({
hiringChecklist: state.inFranceApp.hiringChecklist
}),
{
onChecklistInitialization: initializeHiringChecklist,
onItemCheck: checkHiringItem
}
)(Embaucher)

View File

@ -1,19 +1,25 @@
import { resetEntreprise, specifyIfAutoEntrepreneur, specifyIfDirigeantMajoritaire } from 'Actions/existingCompanyActions'
import {
resetEntreprise,
specifyIfAutoEntrepreneur,
specifyIfDirigeantMajoritaire
} from 'Actions/existingCompanyActions'
import { T } from 'Components'
import CompanyDetails from 'Components/CompanyDetails'
import FindCompany from 'Components/FindCompany'
import Overlay from 'Components/Overlay'
import { ScrollToTop } from 'Components/utils/Scroll'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import React, { useContext, useEffect, useRef, useState } from "react"
import React, { useContext, useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { Company } from 'Reducers/inFranceAppReducer'
import { RootState } from 'Reducers/rootReducer'
import * as Animate from 'Ui/animate'
import businessPlan from './businessPlan.svg'
const infereRégimeFromCompanyDetails = company => {
const infereRégimeFromCompanyDetails = (company: Company) => {
if (!company) {
return null
}
@ -39,10 +45,12 @@ const infereRégimeFromCompanyDetails = company => {
export default function SocialSecurity() {
const { t } = useTranslation()
const company = useSelector<any, any>(state => state.inFranceApp.existingCompany)
const company = useSelector(
(state: RootState) => state.inFranceApp.existingCompany
)
const sitePaths = useContext(SitePathsContext)
const régime = infereRégimeFromCompanyDetails(company)
return (
<>
<Helmet>
@ -76,7 +84,9 @@ export default function SocialSecurity() {
</div>
<>
<h2><T k="gérer.choix.titre">Que souhaitez-vous faire ?</T></h2>
<h2>
<T k="gérer.choix.titre">Que souhaitez-vous faire ?</T>
</h2>
{!!régime && (
<Link
className="ui__ interactive card button-choice lighter-bg"
@ -86,11 +96,10 @@ export default function SocialSecurity() {
state: {
fromGérer: true
}
}}>
}}
>
<T k="gérer.choix.revenus">
<p>
Calculer mon revenu net
</p>
<p>Calculer mon revenu net</p>
<small>
Estimez précisément le montant de vos cotisations grâce au
simulateur {{ régime }} de lURSSAF
@ -107,19 +116,20 @@ export default function SocialSecurity() {
state: {
fromGérer: true
}
}}>
}}
>
<T k="gérer.choix.embauche">
<p>
Estimer le montant dune embauche
</p>
<p>Estimer le montant dune embauche</p>
<small>
Calculez le montant total que votre entreprise devra dépenser pour
rémunérer votre prochain employé
</small>
Calculez le montant total que votre entreprise devra dépenser
pour rémunérer votre prochain employé
</small>
</T>
</Link>
)}
<h2><T>Ressources utiles</T></h2>
<h2>
<T>Ressources utiles</T>
</h2>
<div
css={`
display: flex;
@ -128,45 +138,49 @@ export default function SocialSecurity() {
> * {
flex: 1;
}
`}>
{!company ?.isAutoEntrepreneur && (
`}
>
{!company?.isAutoEntrepreneur && (
<Link
className="ui__ interactive card button-choice lighter-bg"
to={sitePaths.gérer.embaucher}>
to={sitePaths.gérer.embaucher}
>
<T k="gérer.ressources.embaucher">
<p>Découvrir les démarches dembauche </p>
<small>
La liste des choses à faire pour être sûr de ne rien oublier
lors de lembauche dun nouveau salarié
</small>
</small>
</T>
</Link>
)}
{company ?.isAutoEntrepreneur && (
{company?.isAutoEntrepreneur && (
<a
className="ui__ interactive card button-choice lighter-bg"
href="https://autoentrepreneur.urssaf.fr">
href="https://autoentrepreneur.urssaf.fr"
>
<T k="gérer.ressources.autoEntrepreneur">
<p>Accéder au site officiel auto-entrepreneur</p>
<small>
Vous pourrez effectuer votre déclaration de chiffre d'affaire,
payer vos cotisations, et plus largement trouver toutes les
informations relatives au statut d'auto-entrepreneur
</small>
Vous pourrez effectuer votre déclaration de chiffre
d'affaire, payer vos cotisations, et plus largement trouver
toutes les informations relatives au statut
d'auto-entrepreneur
</small>
</T>
</a>
)}
<Link
className="ui__ interactive card button-choice lighter-bg"
to={sitePaths.gérer.sécuritéSociale}>
to={sitePaths.gérer.sécuritéSociale}
>
<T k="gérer.ressources.sécuritéSociale">
<p>Comprendre la sécurité sociale </p>
<small>
A quoi servent les cotisations sociales ? Le point sur le
système de protection sociale dont bénéficient tous les
travailleurs en France
</small>
</small>
</T>
</Link>
</div>
@ -176,7 +190,11 @@ export default function SocialSecurity() {
)
}
const CompanySection = ({ company }) => {
type CompanySectionProps = {
company: Company
}
const CompanySection = ({ company }: CompanySectionProps) => {
const [searchModal, showSearchModal] = useState(false)
const [autoEntrepreneurModal, showAutoEntrepreneurModal] = useState(false)
const [DirigeantMajoritaireModal, showDirigeantMajoritaireModal] = useState(
@ -191,14 +209,14 @@ const CompanySection = ({ company }) => {
showSearchModal(false)
}
if (
company ?.statutJuridique === 'EI' &&
company ?.isAutoEntrepreneur == null
company?.statutJuridique === 'EI' &&
company?.isAutoEntrepreneur == null
) {
showAutoEntrepreneurModal(true)
}
if (
company ?.statutJuridique === 'SARL' &&
company ?.isDirigeantMajoritaire == null
company?.statutJuridique === 'SARL' &&
company?.isDirigeantMajoritaire == null
) {
showDirigeantMajoritaireModal(true)
}
@ -206,11 +224,11 @@ const CompanySection = ({ company }) => {
}, [company, searchModal])
const dispatch = useDispatch()
const handleAnswerAutoEntrepreneur = isAutoEntrepreneur => {
const handleAnswerAutoEntrepreneur = (isAutoEntrepreneur: boolean) => {
dispatch(specifyIfAutoEntrepreneur(isAutoEntrepreneur))
showAutoEntrepreneurModal(false)
}
const handleAnswerDirigeantMajoritaire = DirigeantMajoritaire => {
const handleAnswerDirigeantMajoritaire = (DirigeantMajoritaire: boolean) => {
dispatch(specifyIfDirigeantMajoritaire(DirigeantMajoritaire))
showDirigeantMajoritaireModal(false)
}
@ -221,16 +239,20 @@ const CompanySection = ({ company }) => {
<>
<ScrollToTop />
<Overlay>
<h2><T k="gérer.entreprise.auto">Êtes-vous auto-entrepreneur ? </T></h2>
<h2>
<T k="gérer.entreprise.auto">Êtes-vous auto-entrepreneur ? </T>
</h2>
<div className="ui__ answer-group">
<button
className="ui__ button"
onClick={() => handleAnswerAutoEntrepreneur(true)}>
onClick={() => handleAnswerAutoEntrepreneur(true)}
>
<T>Oui</T>
</button>
<button
className="ui__ button"
onClick={() => handleAnswerAutoEntrepreneur(false)}>
onClick={() => handleAnswerAutoEntrepreneur(false)}
>
<T>Non</T>
</button>
</div>
@ -252,12 +274,14 @@ const CompanySection = ({ company }) => {
<div className="ui__ answer-group">
<button
className="ui__ button"
onClick={() => handleAnswerDirigeantMajoritaire(true)}>
onClick={() => handleAnswerDirigeantMajoritaire(true)}
>
<T>Oui</T>
</button>
<button
className="ui__ button"
onClick={() => handleAnswerDirigeantMajoritaire(false)}>
onClick={() => handleAnswerDirigeantMajoritaire(false)}
>
<T>Non</T>
</button>
</div>
@ -286,9 +310,15 @@ const CompanySection = ({ company }) => {
</span>
{company.isDirigeantMajoritaire != null && (
<span css="margin-left: 1rem" className="ui__ label">
{company.isDirigeantMajoritaire
? <T k="gérer.entreprise.majoritaire">Dirigeant majoritaire</T>
: <T k="gérer.entreprise.minoritaire">Dirigeant minoritaire</T>}
{company.isDirigeantMajoritaire ? (
<T k="gérer.entreprise.majoritaire">
Dirigeant majoritaire
</T>
) : (
<T k="gérer.entreprise.minoritaire">
Dirigeant minoritaire
</T>
)}
</span>
)}
</>
@ -299,19 +329,23 @@ const CompanySection = ({ company }) => {
onClick={() => {
dispatch(resetEntreprise())
showSearchModal(true)
}}>
<T k="gérer.entreprise.changer">Changer l'entreprise sélectionnée</T>
}}
>
<T k="gérer.entreprise.changer">
Changer l'entreprise sélectionnée
</T>
</button>
</>
) : (
<p>
<button
onClick={() => showSearchModal(true)}
className="ui__ plain cta button">
<T k="gérer.cta">Renseigner mon entreprise</T>
</button>
</p>
)}
<p>
<button
onClick={() => showSearchModal(true)}
className="ui__ plain cta button"
>
<T k="gérer.cta">Renseigner mon entreprise</T>
</button>
</p>
)}
</>
)
}

View File

@ -1,11 +1,11 @@
import { T } from 'Components'
import animate from 'Ui/animate'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import animate from 'Ui/animate'
export default function SchemeChoice() {
const sitePaths = useContext(SitePathsContext)
@ -21,7 +21,8 @@ export default function SchemeChoice() {
<p>
<Link
to={sitePaths.simulateurs['assimilé-salarié']}
className="ui__ interactive card light-bg button-choice">
className="ui__ interactive card light-bg button-choice"
>
{emoji('☂')}
<span>
<T>Assimilé salarié</T>
@ -36,7 +37,8 @@ export default function SchemeChoice() {
</Link>
<Link
to={sitePaths.simulateurs.indépendant}
className="ui__ interactive card light-bg button-choice">
className="ui__ interactive card light-bg button-choice"
>
{emoji('👩‍🔧')}
<span>
<T>Indépendant</T>
@ -51,7 +53,8 @@ export default function SchemeChoice() {
</Link>
<Link
to={sitePaths.simulateurs['auto-entrepreneur']}
className="ui__ interactive card light-bg button-choice">
className="ui__ interactive card light-bg button-choice"
>
{emoji('🚶‍♂️')}
Auto-entrepreneur
</Link>
@ -64,7 +67,8 @@ export default function SchemeChoice() {
<p style={{ textAlign: 'center', marginTop: '1rem' }}>
<Link
className="ui__ plain cta button"
to={sitePaths.simulateurs.comparaison}>
to={sitePaths.simulateurs.comparaison}
>
<T k="selectionRégime.comparer.cta">Comparer les régimes</T>
</Link>
</p>

View File

@ -19,7 +19,8 @@ export default function Gérer() {
to={sitePaths.gérer.index}
exact
activeClassName="ui__ hide"
className="ui__ simple push-left small button">
className="ui__ simple push-left small button"
>
<T>Retour à mon activité</T>
</NavLink>
</div>

View File

@ -7,7 +7,6 @@ import screenfull from 'screenfull'
import { isIE } from '../../../../utils'
import Privacy from '../../layout/Footer/Privacy'
export default function IframeFooter() {
const [isFullscreen, setIsFullscreen] = useState(screenfull.isFullscreen)
useEffect(() => {
@ -25,14 +24,16 @@ export default function IframeFooter() {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
}}
>
<LangSwitcher className="ui__ button simple" />
{screenfull.enabled && !isFullscreen && !isIE() && (
<button
className="ui__ button small"
onClick={() => {
screenfull.toggle()
}}>
}}
>
{emoji('🖵')}&nbsp;
<Trans>Plein écran</Trans>
</button>

View File

@ -1,10 +1,10 @@
import { SitePathsContext } from 'Components/utils/withSitePaths';
import React, { useContext } from 'react';
import { Helmet } from 'react-helmet';
import { SalarySimulation } from '../Simulateurs/Salarié';
import { SitePathsContext } from 'Components/utils/withSitePaths'
import React, { useContext } from 'react'
import { Helmet } from 'react-helmet'
import { SalarySimulation } from '../Simulateurs/Salarié'
export default function IframeSimulateurEmbauche() {
const sitePaths = useContext(SitePathsContext);
const sitePaths = useContext(SitePathsContext)
return (
<>
<Helmet>

View File

@ -5,14 +5,15 @@ import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { RootState } from 'Reducers/rootReducer'
import Footer from '../../layout/Footer/Footer'
import illustrationSvg from './illustration.svg'
import './Landing.css'
export default function Landing() {
const sitePaths = useContext(SitePathsContext)
const statutChoisi = useSelector<any, any>(
state => state.inFranceApp.companyStatusChoice
const statutChoisi = useSelector(
(state: RootState) => state.inFranceApp.companyStatusChoice
)
return (
<div className="app-content">
@ -42,7 +43,8 @@ export default function Landing() {
className="ui__ interactive card box"
to={
statutChoisi ? sitePaths.créer[statutChoisi] : sitePaths.créer.index
}>
}
>
<div className="ui__ big box-icon">{emoji('💡')}</div>
<T k="landing.choice.create">
<h3>Créer une entreprise</h3>
@ -70,7 +72,8 @@ export default function Landing() {
</Link>
<Link
className="ui__ interactive card box"
to={sitePaths.économieCollaborative.index}>
to={sitePaths.économieCollaborative.index}
>
<div className="ui__ big box-icon">{emoji('🙋')}</div>
<T k="landing.choice.declare">
<h3>Que dois-je déclarer ?</h3>
@ -88,7 +91,8 @@ export default function Landing() {
<div style={{ width: '100%', textAlign: 'center' }}>
<Link
to={sitePaths.simulateurs.index}
className="ui__ simple small button ">
className="ui__ simple small button "
>
{emoji('🧮')}{' '}
<T k="landing.seeSimulators">Voir la liste des simulateurs</T>
</Link>

Some files were not shown because too many files have changed in this diff Show More